From 52c26ff9d1d4a0969fcb51fcf23f2063aefa6b42 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 22 Apr 2024 13:52:08 +0200 Subject: [PATCH 001/134] Disable PermissionsTests from UI Tests scheme --- DuckDuckGo.xcodeproj/project.pbxproj | 3 +- .../xcshareddata/xcschemes/UI Tests.xcscheme | 14 +++++- UITests/UI Tests.xctestplan | 49 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 UITests/UI Tests.xctestplan diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 06e4ce3ba6..29bdfb5c7f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1414,7 +1414,6 @@ 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 */; }; - 560C40012BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; 561D66662B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 561D66672B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; @@ -2870,6 +2869,7 @@ 376C4DB828A1A48A00CC0F5B /* FirePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewModelTests.swift; sourceTree = ""; }; 376CC8B4296EB630006B63A7 /* AppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppStore.xcconfig; sourceTree = ""; }; 376CC8B5296EBA8F006B63A7 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = ""; }; + 376E708D2BD686260082B7EB /* UI Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "UI Tests.xctestplan"; path = "UITests/UI Tests.xctestplan"; sourceTree = ""; }; 37717E66296B5A20002FAEDF /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = ""; }; 3775912C29AAC72700E26367 /* SyncPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferences.swift; sourceTree = ""; }; 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManagementDialogViewController.swift; sourceTree = ""; }; @@ -6349,6 +6349,7 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( + 376E708D2BD686260082B7EB /* UI Tests.xctestplan */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme index eea4f50141..3c5d6efd81 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme @@ -26,8 +26,7 @@ buildConfiguration = "Review" selectedDebuggerIdentifier = "" selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -55,6 +54,12 @@ + + + + @@ -65,6 +70,11 @@ BlueprintName = "UI Tests" ReferencedContainer = "container:DuckDuckGo.xcodeproj"> + + + + diff --git a/UITests/UI Tests.xctestplan b/UITests/UI Tests.xctestplan new file mode 100644 index 0000000000..e1394ca735 --- /dev/null +++ b/UITests/UI Tests.xctestplan @@ -0,0 +1,49 @@ +{ + "configurations" : [ + { + "id" : "3B3274F0-3353-49A0-B607-6F17F519C2E2", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "-NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints YES" + }, + { + "argument" : "-com.apple.CoreData.ConcurrencyDebug 1" + } + ], + "environmentVariableEntries" : [ + { + "key" : "OS_ACTIVITY_DT_MODE", + "value" : "YES" + }, + { + "key" : "OS_ACTIVITY_MODE", + "value" : "debug" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:DuckDuckGo.xcodeproj", + "identifier" : "AA585D7D248FD31100E9A3E2", + "name" : "DuckDuckGo Privacy Browser" + } + }, + "testTargets" : [ + { + "skippedTests" : [ + "PermissionsTests" + ], + "target" : { + "containerPath" : "container:DuckDuckGo.xcodeproj", + "identifier" : "7B4CE8D926F02108009134B1", + "name" : "UI Tests" + } + } + ], + "version" : 1 +} From ba8da49ca6988b4f64cd51e9a9f60a38e82c5640 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 22 Apr 2024 14:13:15 +0200 Subject: [PATCH 002/134] Move UI Tests.xctestplan to UITests group --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 29bdfb5c7f..82841761e4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2869,7 +2869,7 @@ 376C4DB828A1A48A00CC0F5B /* FirePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewModelTests.swift; sourceTree = ""; }; 376CC8B4296EB630006B63A7 /* AppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppStore.xcconfig; sourceTree = ""; }; 376CC8B5296EBA8F006B63A7 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = ""; }; - 376E708D2BD686260082B7EB /* UI Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "UI Tests.xctestplan"; path = "UITests/UI Tests.xctestplan"; sourceTree = ""; }; + 376E708D2BD686260082B7EB /* UI Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "UI Tests.xctestplan"; sourceTree = ""; }; 37717E66296B5A20002FAEDF /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = ""; }; 3775912C29AAC72700E26367 /* SyncPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferences.swift; sourceTree = ""; }; 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManagementDialogViewController.swift; sourceTree = ""; }; @@ -5730,6 +5730,7 @@ 7B4CE8DB26F02108009134B1 /* UITests */ = { isa = PBXGroup; children = ( + 376E708D2BD686260082B7EB /* UI Tests.xctestplan */, EEBCE6802BA444FA00B9DF00 /* Common */, EEC7BE2D2BC6C09400F86835 /* AddressBarKeyboardShortcutsTests.swift */, EED735352BB46B6000F173D6 /* AutocompleteTests.swift */, @@ -6349,7 +6350,6 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( - 376E708D2BD686260082B7EB /* UI Tests.xctestplan */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, From 00dccece42058f2afa53c0e1a1ed3d7ee6b16baf Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 22 Apr 2024 09:03:30 -0700 Subject: [PATCH 003/134] [Release PR] Link Lottie with the NetworkProtectionUI library (#2676) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207133509501091/f Tech Design URL: CC: Description: This PR fixes the DuckDuckGo Notifications target crashing on launch. --- LocalPackages/NetworkProtectionMac/Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index c08127a1af..ad131b9869 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,6 +32,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), + .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), @@ -73,6 +74,7 @@ let package = Package( .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), .product(name: "LoginItems", package: "LoginItems"), .product(name: "PixelKit", package: "PixelKit"), + .product(name: "Lottie", package: "lottie-spm") ], resources: [ .copy("Resources/Assets.xcassets") From d1cdb61ca7638b11a2ab4706c5d262830ea85a60 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Mon, 22 Apr 2024 17:15:53 +0100 Subject: [PATCH 004/134] Update AdvancedBackgroundChecks selectors (#2674) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207137555703948/f Tech Design URL: CC: **Description**: See parent task, fixes some selectors that were pulling in phone numbers instead of relatives. --- .../Resources/JSON/advancedbackgroundchecks.com.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index e14795a195..d514b04052 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,7 +1,7 @@ { "name": "AdvancedBackgroundChecks", "url": "advancedbackgroundchecks.com", - "version": "0.1.6", + "version": "0.1.7", "parent": "peoplefinders.com", "addedDatetime": 1678060800000, "steps": [ @@ -11,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "7967f064-e3c5-442d-8380-99cf752fb8df", + "id": "070b6417-8ccd-4111-b5ae-7ae470b0399a", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "6f6bb616-a4cb-4231-9abb-522722208f95", + "id": "d760163d-4a1f-476a-bc3e-4f3ccedc29f1", "selector": ".card-block", "profile": { "name": { @@ -24,7 +24,7 @@ "beforeText": "Age" }, "alternativeNamesList": { - "selector": "(.//p[@class='card-text max-lines-1'])[1]", + "selector": ".//p[contains(@class, 'card-text') and strong[contains(text(), 'AKA')]]", "afterText": "AKA:", "separator": "," }, @@ -40,7 +40,7 @@ "selector": "(.//p[@class='card-text'])[1]" }, "relativesList": { - "selector": "(.//p[@class='card-text max-lines-1'])[2]", + "selector": ".//p[contains(@class, 'card-text') and strong[contains(text(), 'Related to')]]", "afterText": "Related to:", "separator": "," }, From 829f9529ebcb94408f20133be741ee17f07a15e5 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Mon, 22 Apr 2024 17:24:57 +0100 Subject: [PATCH 005/134] Local PixelKit package replaced with BSK's PixelKit (#2656) Task/Issue URL: https://app.asana.com/0/1205842942115003/1206999268603553/f **Description**: Replaced local PixelKit with BSK's PixelKit --- DuckDuckGo.xcodeproj/project.pbxproj | 379 +++++++------- .../xcshareddata/swiftpm/Package.resolved | 6 +- ...kDuckGo Privacy Browser App Store.xcscheme | 20 - .../DuckDuckGo Privacy Browser.xcscheme | 20 - .../DataBrokerProtection/Package.swift | 5 +- .../NetworkProtectionMac/Package.swift | 10 +- LocalPackages/PixelKit/.gitignore | 9 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - LocalPackages/PixelKit/Package.swift | 50 -- LocalPackages/PixelKit/README.md | 11 - .../Extensions/String+StaticString.swift | 27 - .../PixelKit/Extensions/URL+PixelKit.swift | 30 -- .../PixelKit/PixelKit+Parameters.swift | 137 ----- .../PixelKit/Sources/PixelKit/PixelKit.swift | 476 ------------------ .../Sources/PixelKit/PixelKitEvent.swift | 100 ---- .../Sources/PixelKit/PixelKitEventV2.swift | 58 --- .../PixelFireExpectations.swift | 66 --- .../ValidatePixel.swift | 50 -- .../XCTestCase+PixelKit.swift | 169 ------- .../PixelKitParametersTests.swift | 75 --- .../Tests/PixelKitTests/PixelKitTests.swift | 385 -------------- LocalPackages/SubscriptionUI/Package.swift | 8 +- 22 files changed, 192 insertions(+), 1907 deletions(-) delete mode 100644 LocalPackages/PixelKit/.gitignore delete mode 100644 LocalPackages/PixelKit/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 LocalPackages/PixelKit/Package.swift delete mode 100644 LocalPackages/PixelKit/README.md delete mode 100644 LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift delete mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift delete mode 100644 LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift delete mode 100644 LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 06e4ce3ba6..b706baead4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -134,7 +134,6 @@ 1DFAB5232A8983E100A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; 1E0C72062ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */; }; - 1E46E1A02BD029BD0007273A /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1E46E19F2BD029BD0007273A /* Subscription */; }; 1E559BB12BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */; }; @@ -947,7 +946,6 @@ 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373A1AB128451ED400586521 /* BookmarksHTMLImporterTests.swift */; }; 373D9B4829EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */; }; 373D9B4929EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */; }; - 373FB4B12B4D6C42004C88D6 /* PreferencesViews in Frameworks */ = {isa = PBXBuildFile; productRef = 373FB4B02B4D6C42004C88D6 /* PreferencesViews */; }; 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */ = {isa = PBXBuildFile; productRef = 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */; }; 37445F992A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; 37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; @@ -1028,8 +1026,6 @@ 37CD54D027F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C827F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift */; }; 37CEFCA92A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEFCA82A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift */; }; 37CEFCAA2A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CEFCA82A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift */; }; - 37CF91592BB416A500BADCAE /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF91582BB416A500BADCAE /* Crashes */; }; - 37CF915B2BB416AC00BADCAE /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF915A2BB416AC00BADCAE /* Crashes */; }; 37D2377A287EB8CA00BCE03B /* TabIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D23779287EB8CA00BCE03B /* TabIndex.swift */; }; 37D2377C287EBDA300BCE03B /* TabIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2377B287EBDA300BCE03B /* TabIndexTests.swift */; }; 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2377F287EFEE200BCE03B /* PinnedTabsManagerTests.swift */; }; @@ -1080,7 +1076,6 @@ 4B1E6EF227AB5E5D00F51793 /* PasswordManagementItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EF027AB5E5D00F51793 /* PasswordManagementItemList.swift */; }; 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; - 4B2537772A11BFE100610219 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2537762A11BFE100610219 /* PixelKit */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B29759728281F0900187C4E /* FirefoxEncryptionKeyReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */; }; 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2975982828285900187C4E /* FirefoxKeyReaderTests.swift */; }; @@ -1143,7 +1138,6 @@ 4B4BEC482A11B61F001D9AC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */; }; 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; - 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B4D60972A0B2A5C00BCD287 /* PixelKit */; }; 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; @@ -1210,8 +1204,6 @@ 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A57CE279A4EF300B1C70E /* ChromiumPreferences.swift */; }; 4B7A60A1273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */; }; - 4B81AD352B29512B00706C96 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 4B81AD342B29512B00706C96 /* PixelKitTestingUtilities */; }; - 4B81AD372B29513100706C96 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 4B81AD362B29513100706C96 /* PixelKitTestingUtilities */; }; 4B85A48028821CC500FC4C39 /* NSPasteboardItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */; }; 4B8A4DFF27C83B29005F40E8 /* SaveIdentityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8A4DFE27C83B29005F40E8 /* SaveIdentityViewController.swift */; }; 4B8A4E0127C8447E005F40E8 /* SaveIdentityPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8A4E0027C8447E005F40E8 /* SaveIdentityPopover.swift */; }; @@ -1414,7 +1406,6 @@ 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 */; }; - 560C40012BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; 561D66662B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 561D66672B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; @@ -1484,8 +1475,6 @@ 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; - 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5DD6992AE51FFA001DE99C /* PixelKit */; }; - 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */; }; 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */; }; 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; @@ -1500,7 +1489,6 @@ 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; - 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD612B7E0C4B004FEF43 /* PixelKit */; }; 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; @@ -1539,10 +1527,6 @@ 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; - 7BBE650D2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */; }; - 7BBE650E2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */; }; - 7BBE65102BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */; }; - 7BBE65112BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */; }; 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; @@ -1558,8 +1542,6 @@ 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5112AD1235B00A9E72B /* NetworkProtectionIPC */; }; 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */; }; 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */; }; - 7BFCB74E2ADE7E1A00DA3EA7 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */; }; - 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */; }; 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFE95542A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95552A9DF2990081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; @@ -1684,8 +1666,6 @@ 98A50964294B691800D10880 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 98A50963294B691800D10880 /* Persistence */; }; 98A95D88299A2DF900B9B81A /* BookmarkMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */; }; 98EB5D1027516A4800681FE6 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98EB5D0F27516A4800681FE6 /* AppPrivacyConfigurationTests.swift */; }; - 9D6983F92AC773C3002C02FC /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9D6983F82AC773C3002C02FC /* PixelKit */; }; - 9D6983FB2AC773C8002C02FC /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9D6983FA2AC773C8002C02FC /* PixelKit */; }; 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */; }; 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; 9D9AE86C2AA76D1B0026E7DC /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; @@ -1702,7 +1682,6 @@ 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */; }; 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; - 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DB6E7232AA0DC5800A17F3C /* LoginItems */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; @@ -2527,6 +2506,9 @@ EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; + 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 */; }; 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 */; }; @@ -2539,6 +2521,14 @@ F18826912BC0105800D9AC4F /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; F18826922BC0105900D9AC4F /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; }; F18826932BC0105900D9AC4F /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; + F198C7122BD18A28000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7112BD18A28000BF24D /* PixelKit */; }; + F198C7142BD18A30000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7132BD18A30000BF24D /* PixelKit */; }; + F198C7162BD18A44000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7152BD18A44000BF24D /* PixelKit */; }; + F198C7182BD18A4C000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7172BD18A4C000BF24D /* PixelKit */; }; + F198C71A2BD18A5B000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C7192BD18A5B000BF24D /* PixelKit */; }; + F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71B2BD18A61000BF24D /* PixelKit */; }; + F198C71E2BD18D88000BF24D /* SwiftLintTool in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71D2BD18D88000BF24D /* SwiftLintTool */; }; + F198C7202BD18D92000BF24D /* SwiftLintTool in Frameworks */ = {isa = PBXBuildFile; productRef = F198C71F2BD18D92000BF24D /* SwiftLintTool */; }; F1B33DF22BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; @@ -2547,6 +2537,10 @@ F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; F1D43AF32B98E47800BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */; }; F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */; }; + F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 08D4923DC968236E22E373E2 /* Crashes */; }; + F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 537FC71EA5115A983FAF3170 /* Crashes */; }; + F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = DC3F73D49B2D44464AFEFCD8 /* Subscription */; }; + F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = F1DF95E62BD188B60045E591 /* LoginItems */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */; }; F4A6198C283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */; }; @@ -3182,7 +3176,6 @@ 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStoryboardExtension.swift; sourceTree = ""; }; - 4BE15DB12A0B0DD500898243 /* PixelKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PixelKit; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.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 = ""; }; @@ -3279,8 +3272,6 @@ 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; - 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionIPCTunnelControllerTests.swift; sourceTree = ""; }; - 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTestingSupport.swift; sourceTree = ""; }; 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPCServiceManager.swift; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; @@ -3999,8 +3990,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */, 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, - 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, @@ -4009,10 +4000,12 @@ 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */, 37269EFD2B332FAC005E8E46 /* Common in Frameworks */, + F198C7142BD18A30000BF24D /* PixelKit in Frameworks */, F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */, 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */, 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */, + F198C7202BD18D92000BF24D /* SwiftLintTool in Frameworks */, 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */, 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */, 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, @@ -4022,7 +4015,6 @@ B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, - 37CF915B2BB416AC00BADCAE /* Crashes in Frameworks */, 3706FCAB293F65D500E42796 /* TrackerRadarKit in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, @@ -4038,9 +4030,9 @@ buildActionMask = 2147483647; files = ( 3706FE88293F661700E42796 /* OHHTTPStubs in Frameworks */, + F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, 3706FE89293F661700E42796 /* OHHTTPStubsSwift in Frameworks */, - 4B81AD372B29513100706C96 /* PixelKitTestingUtilities in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4063,6 +4055,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CD2B316DFC00A595BB /* SnapshotTesting in Frameworks */, B6AE39F329374AEC00C37AA4 /* OHHTTPStubs in Frameworks */, B6AE39F529374AEC00C37AA4 /* OHHTTPStubsSwift in Frameworks */, @@ -4075,8 +4068,8 @@ files = ( 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, - 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, 7B37C7A52BAA32A50062546A /* Subscription in Frameworks */, + F198C7182BD18A4C000BF24D /* PixelKit in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, @@ -4087,13 +4080,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F198C71A2BD18A5B000BF24D /* PixelKit in Frameworks */, 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */, - 7BFCB74E2ADE7E1A00DA3EA7 /* PixelKit in Frameworks */, EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */, EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */, 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */, @@ -4104,9 +4097,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, + F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, BDADBDCB2BD2BC2800421B9B /* Lottie in Frameworks */, 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, @@ -4129,10 +4122,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */, 37269EFF2B332FBB005E8E46 /* Common in Frameworks */, EE7295E72A545BBB008C0991 /* NetworkProtection in Frameworks */, - 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */, - 1E46E1A02BD029BD0007273A /* Subscription in Frameworks */, + F198C7162BD18A44000BF24D /* PixelKit in Frameworks */, 4B4D60AF2A0C837F00BCD287 /* Networking in Frameworks */, 7B25856E2BA2F2ED00D49F79 /* NetworkProtectionUI in Frameworks */, 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */, @@ -4161,7 +4154,6 @@ files = ( 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, - 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, ); @@ -4172,7 +4164,6 @@ buildActionMask = 2147483647; files = ( 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */, - 9D6983F92AC773C3002C02FC /* PixelKit in Frameworks */, 9D9AE8F92AAA3AD00026E7DC /* DataBrokerProtection in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4181,7 +4172,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D6983FB2AC773C8002C02FC /* PixelKit in Frameworks */, 315A023F2B6421AE00BFA577 /* Networking in Frameworks */, 9D9AE8FB2AAA3AD90026E7DC /* DataBrokerProtection in Frameworks */, ); @@ -4191,16 +4181,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 37CF91592BB416A500BADCAE /* Crashes in Frameworks */, - 373FB4B12B4D6C42004C88D6 /* PreferencesViews in Frameworks */, + F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */, 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, - 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */, EE7295E32A545B9A008C0991 /* NetworkProtection in Frameworks */, 9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */, 987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */, - 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */, 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */, 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */, 378F44E429B4BDE900899924 /* SwiftUIExtensions in Frameworks */, @@ -4210,8 +4197,10 @@ 7B31FD8C2AD125620086AA24 /* NetworkProtectionIPC in Frameworks */, 37269EFB2B332F9E005E8E46 /* Common in Frameworks */, AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */, + F198C71E2BD18D88000BF24D /* SwiftLintTool in Frameworks */, 1EA7B8D52B7E078C000330A4 /* Subscription in Frameworks */, B6B77BE8297973D4001E68A1 /* Navigation in Frameworks */, + F198C7122BD18A28000BF24D /* PixelKit in Frameworks */, 3739326729AE4B42009346AE /* DDGSync in Frameworks */, 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */, 371D00E129D8509400EC8598 /* OpenSSL in Frameworks */, @@ -4220,6 +4209,7 @@ 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */, 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, + F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, @@ -4232,9 +4222,9 @@ buildActionMask = 2147483647; files = ( B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */, + F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, - 4B81AD352B29512B00706C96 /* PixelKitTestingUtilities in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4665,7 +4655,6 @@ 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, 7B25FE322AD12C990012AFAB /* NetworkProtectionMac */, - 4BE15DB12A0B0DD500898243 /* PixelKit */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, 1E862A882A9FC01200F84D4B /* SubscriptionUI */, @@ -5589,13 +5578,11 @@ isa = PBXGroup; children = ( BDA7648F2BC4E56200D0400C /* Mocks */, - 7BBE65122BC67EF6008F4EE9 /* Support */, 4BCF15E62ABB98A20083F6DF /* Resources */, 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, - 7BBE650C2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -5801,14 +5788,6 @@ path = LetsMove1.25; sourceTree = ""; }; - 7BBE65122BC67EF6008F4EE9 /* Support */ = { - isa = PBXGroup; - children = ( - 7BBE650F2BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift */, - ); - path = Support; - sourceTree = ""; - }; 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { isa = PBXGroup; children = ( @@ -6369,7 +6348,6 @@ 565E46DE2B2725DD0013AC2A /* SyncE2EUITests */, AA585D7F248FD31100E9A3E2 /* Products */, 85AE2FF024A33A2D002D507F /* Frameworks */, - EE0629702B90EE3500D868B4 /* Recovered References */, ); sourceTree = ""; }; @@ -7096,10 +7074,10 @@ 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */, 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */, 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */, + 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */, 4B92928726670D1600AD2C21 /* BookmarkOutlineCellView.swift */, 4B92928526670D1600AD2C21 /* BookmarksOutlineView.swift */, 4B92928926670D1700AD2C21 /* BookmarkTableCellView.swift */, - 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */, 4B9292C62667123700AD2C21 /* BrowserTabSelectionDelegate.swift */, 4B92928626670D1600AD2C21 /* OutlineSeparatorViewCell.swift */, 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */, @@ -7994,13 +7972,6 @@ path = fonts; sourceTree = ""; }; - EE0629702B90EE3500D868B4 /* Recovered References */ = { - isa = PBXGroup; - children = ( - ); - name = "Recovered References"; - sourceTree = ""; - }; EEA3EEAF2B24EB5100E8333A /* VPNLocation */ = { isa = PBXGroup; children = ( @@ -8073,16 +8044,15 @@ CBCCF59F2996681700C02DFE /* Assert Xcode version */, 378E2798296F6D1D00FCADA2 /* Validate PRODUCT_NAME */, 3706FA79293F65D500E42796 /* Check Embedded Config URLs */, - B6E6BA192BA2D8BE008AA7E1 /* Run swiftlint */, 3706FA7A293F65D500E42796 /* Sources */, 3706FCA6293F65D500E42796 /* Frameworks */, 3706FCB1293F65D500E42796 /* Resources */, 4BBA2D272B6AC09D00F6A470 /* Embed Login Items */, + 6A8856B31B2BC5078B61ED81 /* Run swiftlint */, ); buildRules = ( ); dependencies = ( - B637D1BD2BC6AE6200C7DCA7 /* PBXTargetDependency */, 4BBA2D2B2B6AD01E00F6A470 /* PBXTargetDependency */, 4BBA2D292B6ACD4D00F6A470 /* PBXTargetDependency */, 4B5F14FE2A1529230060320F /* PBXTargetDependency */, @@ -8103,7 +8073,6 @@ B6EC37FE29B8D915001ACE79 /* Configuration */, 37DF000629F9C061002B7D3E /* SyncDataProviders */, 9DC70B192AA1FA5B005A844B /* LoginItems */, - 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */, 37269EFC2B332FAC005E8E46 /* Common */, 372217812B33380700B8E9C2 /* TestUtils */, 4BF97AD02B43C43F00EB4240 /* NetworkProtectionIPC */, @@ -8117,8 +8086,10 @@ 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */, 85D44B872BA08D30001B4AB5 /* Suggestions */, 4BCBE4592BA7E17800FC75A1 /* Subscription */, - 37CF915A2BB416AC00BADCAE /* Crashes */, 9FF521472BAA909C00B9819B /* Lottie */, + 537FC71EA5115A983FAF3170 /* Crashes */, + F198C7132BD18A30000BF24D /* PixelKit */, + F198C71F2BD18D92000BF24D /* SwiftLintTool */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8142,8 +8113,8 @@ packageProductDependencies = ( 3706FDD6293F661700E42796 /* OHHTTPStubs */, 3706FDD8293F661700E42796 /* OHHTTPStubsSwift */, - 4B81AD362B29513100706C96 /* PixelKitTestingUtilities */, B65CD8CE2B316E0200A595BB /* SnapshotTesting */, + F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */, ); productName = DuckDuckGoTests; productReference = 3706FE99293F661700E42796 /* Unit Tests App Store.xctest */; @@ -8185,8 +8156,6 @@ 376113D62B29CD6800E794BB /* PBXTargetDependency */, ); name = "SyncE2EUITests App Store"; - packageProductDependencies = ( - ); productName = DuckDuckGoSyncUITests; productReference = 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -8210,6 +8179,7 @@ B6AE39F229374AEC00C37AA4 /* OHHTTPStubs */, B6AE39F429374AEC00C37AA4 /* OHHTTPStubsSwift */, B65CD8CC2B316DFC00A595BB /* SnapshotTesting */, + F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */, ); productName = "Integration Tests"; productReference = 4B1AD89D25FC27E200261379 /* Integration Tests.xctest */; @@ -8230,12 +8200,12 @@ ); name = NetworkProtectionSystemExtension; packageProductDependencies = ( - 4B2537762A11BFE100610219 /* PixelKit */, 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, 7B37C7A42BAA32A50062546A /* Subscription */, + F198C7172BD18A4C000BF24D /* PixelKit */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8263,10 +8233,10 @@ 7BEEA5112AD1235B00A9E72B /* NetworkProtectionIPC */, 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */, 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, - 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, EE2F9C5A2B90F2FF00D45FC9 /* Subscription */, + F198C7192BD18A5B000BF24D /* PixelKit */, BDADBDC82BD2BC2200421B9B /* Lottie */, ); productName = DuckDuckGoAgent; @@ -8296,10 +8266,10 @@ EE7295EE2A545C12008C0991 /* NetworkProtection */, 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, - 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, 4BCBE45B2BA7E18500FC75A1 /* Subscription */, + F198C71B2BD18A61000BF24D /* PixelKit */, BDADBDCA2BD2BC2800421B9B /* Lottie */, ); productName = DuckDuckGoAgentAppStore; @@ -8343,12 +8313,12 @@ ); name = NetworkProtectionAppExtension; packageProductDependencies = ( - 4B4D60972A0B2A5C00BCD287 /* PixelKit */, 4B4D60AE2A0C837F00BCD287 /* Networking */, EE7295E62A545BBB008C0991 /* NetworkProtection */, 37269EFE2B332FBB005E8E46 /* Common */, 7B25856D2BA2F2ED00D49F79 /* NetworkProtectionUI */, - 1E46E19F2BD029BD0007273A /* Subscription */, + DC3F73D49B2D44464AFEFCD8 /* Subscription */, + F198C7152BD18A44000BF24D /* PixelKit */, ); productName = NetworkProtectionAppExtension; productReference = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; @@ -8367,8 +8337,6 @@ dependencies = ( ); name = SyncE2EUITests; - packageProductDependencies = ( - ); productName = DuckDuckGoSyncUITests; productReference = 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -8411,7 +8379,6 @@ packageProductDependencies = ( 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, 7B97CD5A2B7E0B85004FEF43 /* Common */, - 7B97CD612B7E0C4B004FEF43 /* PixelKit */, 7B7DFB212B7E7473009EA1A3 /* Networking */, ); productName = VPNProxyExtension; @@ -8434,7 +8401,6 @@ name = DuckDuckGoDBPBackgroundAgent; packageProductDependencies = ( 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */, - 9D6983F82AC773C3002C02FC /* PixelKit */, 9DEF97E02B06C4EE00764F03 /* Networking */, ); productName = DuckDuckGoAgent; @@ -8457,7 +8423,6 @@ name = DuckDuckGoDBPBackgroundAgentAppStore; packageProductDependencies = ( 9D9AE8FA2AAA3AD90026E7DC /* DataBrokerProtection */, - 9D6983FA2AC773C8002C02FC /* PixelKit */, 315A023E2B6421AE00BFA577 /* Networking */, ); productName = DuckDuckGoAgent; @@ -8470,17 +8435,16 @@ buildPhases = ( CBCCF59E299667B700C02DFE /* Assert Xcode version */, 3705272528992C8A000C06A2 /* Check Embedded Config URLs */, - B6409DC52BC7BD1F00D66F9E /* Run swiftlint */, AA585D7A248FD31100E9A3E2 /* Sources */, AA585D7B248FD31100E9A3E2 /* Frameworks */, AA585D7C248FD31100E9A3E2 /* Resources */, B6F2C8722A7A4C7D000498CF /* Make /Applications symlink, remove app on Clean build */, 4B2D065D2A11D2AE00DE1F49 /* Embed Login Items */, + 28003FDBDB96625F1630CFF2 /* Run swiftlint */, ); buildRules = ( ); dependencies = ( - B637D1BB2BC6AE5600C7DCA7 /* PBXTargetDependency */, 7B4627742B9AF2C8004ACE0B /* PBXTargetDependency */, 4B5F14FC2A15291D0060320F /* PBXTargetDependency */, 31C6E9AD2B0C07BA0086DC30 /* PBXTargetDependency */, @@ -8503,22 +8467,22 @@ 37DF000429F9C056002B7D3E /* SyncDataProviders */, 4B4D60B02A0C83B900BCD287 /* NetworkProtectionUI */, EE7295E22A545B9A008C0991 /* NetworkProtection */, - 9DB6E7232AA0DC5800A17F3C /* LoginItems */, 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */, 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */, - 7B5DD6992AE51FFA001DE99C /* PixelKit */, 31A3A4E22B0C115F0021063C /* DataBrokerProtection */, 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, - 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, 85E2BBCD2B8F534000DBEC7A /* History */, 1EA7B8D22B7E078C000330A4 /* SubscriptionUI */, 1EA7B8D42B7E078C000330A4 /* Subscription */, F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */, 85D44B852BA08D29001B4AB5 /* Suggestions */, - 37CF91582BB416A500BADCAE /* Crashes */, 9FF521452BAA908500B9819B /* Lottie */, + 08D4923DC968236E22E373E2 /* Crashes */, + F1DF95E62BD188B60045E591 /* LoginItems */, + F198C7112BD18A28000BF24D /* PixelKit */, + F198C71D2BD18D88000BF24D /* SwiftLintTool */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8542,8 +8506,8 @@ packageProductDependencies = ( B6DA44162616C13800DD1EC2 /* OHHTTPStubs */, B6DA44182616C13800DD1EC2 /* OHHTTPStubsSwift */, - 4B81AD342B29512B00706C96 /* PixelKitTestingUtilities */, B65CD8CA2B316DF100A595BB /* SnapshotTesting */, + F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -8986,6 +8950,25 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 28003FDBDB96625F1630CFF2 /* Run swiftlint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run swiftlint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; + }; 3121F62B2B64266A002F706A /* Copy Swift Package resources */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -9118,7 +9101,7 @@ shellPath = /bin/sh; shellScript = "# Embeds login items for the App Store build.\n\n# Skip login item embedding for release builds until they're ready to go live.\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n VPN_AGENT_NAME=\"${AGENT_RELEASE_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_RELEASE_PRODUCT_NAME}\"\nelse\n VPN_AGENT_NAME=\"${AGENT_PRODUCT_NAME}\"\n PIR_AGENT_NAME=\"${DBP_BACKGROUND_AGENT_PRODUCT_NAME}\"\nfi\n\nVPN_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${VPN_AGENT_NAME}.app\")\nPIR_AGENT_ORIGIN=$(readlink -f \"${CONFIGURATION_BUILD_DIR}/${PIR_AGENT_NAME}.app\")\nAGENT_DESTINATION=\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/Library/LoginItems\"\n \n# Make sure that Library/LoginItems exists before copying\nmkdir -p \"$AGENT_DESTINATION\"\n \necho \"Copying VPN agent from $VPN_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$VPN_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n \necho \"Copying Personal Information Removal agent from $PIR_AGENT_ORIGIN to $AGENT_DESTINATION\"\nrsync -r --links \"$PIR_AGENT_ORIGIN\" \"$AGENT_DESTINATION\"\n"; }; - 7B31FD922AD126C40086AA24 /* Embed System Network Extension */ = { + 6A8856B31B2BC5078B61ED81 /* Run swiftlint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9128,16 +9111,16 @@ ); inputPaths = ( ); - name = "Embed System Network Extension"; + name = "Run swiftlint"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -z \"${SYSEX_BUNDLE_ID}\" ]]; then\n echo \"Required build settings are not defined, please check xcconfig files\"\n exit 1\nfi\n\n\necho \"ditto ${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension $BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\"\n\nditto \"${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension\" \"$BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\" || exit 1\n"; + shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; }; - 7B557F2A2B8CA2A400099746 /* Embed Debug-only Network Extensions */ = { + 7B31FD922AD126C40086AA24 /* Embed System Network Extension */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9147,16 +9130,16 @@ ); inputPaths = ( ); - name = "Embed Debug-only Network Extensions"; + name = "Embed System Network Extension"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Support for embedding debug-only extensions (https://stackoverflow.com/a/76948057/712306)\nfunction embedExtensions() {\n for extension in $1\n do\n rsync -r --copy-links \"${CONFIGURATION_BUILD_DIR}/${extension}.appex\" \"${CONFIGURATION_BUILD_DIR}/${PLUGINS_FOLDER_PATH}\"\n done\n}\n\ndebug_extensions=(\"VPNProxyExtension\")\n\nif [ \"${CONFIGURATION}\" != \"Release\" ]\nthen\n embedExtensions $debug_extensions\nfi\n"; + shellScript = "if [[ -z \"${SYSEX_BUNDLE_ID}\" ]]; then\n echo \"Required build settings are not defined, please check xcconfig files\"\n exit 1\nfi\n\n\necho \"ditto ${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension $BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\"\n\nditto \"${BUILT_PRODUCTS_DIR}/${SYSEX_BUNDLE_ID}.systemextension\" \"$BUILT_PRODUCTS_DIR/${CONTENTS_FOLDER_PATH}/Library/SystemExtensions/${SYSEX_BUNDLE_ID}.systemextension\" || exit 1\n"; }; - 7BB34F502AD98394005691AE /* Copy Swift Package resources */ = { + 7B557F2A2B8CA2A400099746 /* Embed Debug-only Network Extensions */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9166,16 +9149,16 @@ ); inputPaths = ( ); - name = "Copy Swift Package resources"; + name = "Embed Debug-only Network Extensions"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# We had issues where the Swift Package resources were not being added to the Agent Apps,\n# so we're manually coping them here.\n# It seems to be a known issue: https://forums.swift.org/t/swift-packages-resource-bundle-not-present-in-xcarchive-when-framework-using-said-package-is-archived/50084/2\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/ContentScopeScripts_ContentScopeScripts.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/DataBrokerProtection_DataBrokerProtection.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\n"; + shellScript = "# Support for embedding debug-only extensions (https://stackoverflow.com/a/76948057/712306)\nfunction embedExtensions() {\n for extension in $1\n do\n rsync -r --copy-links \"${CONFIGURATION_BUILD_DIR}/${extension}.appex\" \"${CONFIGURATION_BUILD_DIR}/${PLUGINS_FOLDER_PATH}\"\n done\n}\n\ndebug_extensions=(\"VPNProxyExtension\")\n\nif [ \"${CONFIGURATION}\" != \"Release\" ]\nthen\n embedExtensions $debug_extensions\nfi\n"; }; - B6409DC52BC7BD1F00D66F9E /* Run swiftlint */ = { + 7BB34F502AD98394005691AE /* Copy Swift Package resources */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -9185,14 +9168,14 @@ ); inputPaths = ( ); - name = "Run swiftlint"; + name = "Copy Swift Package resources"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; + shellScript = "# We had issues where the Swift Package resources were not being added to the Agent Apps,\n# so we're manually coping them here.\n# It seems to be a known issue: https://forums.swift.org/t/swift-packages-resource-bundle-not-present-in-xcarchive-when-framework-using-said-package-is-archived/50084/2\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/ContentScopeScripts_ContentScopeScripts.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\ncp -RL \"${BUILT_PRODUCTS_DIR}\"/DataBrokerProtection_DataBrokerProtection.bundle \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/\"\n"; }; B6AEB5532BA3029B00781A09 /* Cleanup entitlements */ = { isa = PBXShellScriptBuildPhase; @@ -9232,25 +9215,6 @@ shellPath = /bin/sh; shellScript = "FRAMEWORKS_DIR=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nDYLIB_TARGET_PATH=\"${FRAMEWORKS_DIR}/libswift_Concurrency.dylib\"\n\nif [ ! -e \"${DYLIB_TARGET_PATH}\" ]; then\n\n DYLIB_PATH=$(find \"${TOOLCHAIN_DIR}/usr/lib\" -path \"*/${HOST_PLATFORM}/libswift_Concurrency.dylib\")\n echo \"copy ${DYLIB_PATH} to ${DYLIB_TARGET_PATH}\"\n\n mkdir -p \"${FRAMEWORKS_DIR}\"\n cp \"${DYLIB_PATH}\" \"${DYLIB_TARGET_PATH}\" || exit 1\n\nelse\n echo \"${DYLIB_TARGET_PATH} exists 👌\"\nfi\n"; }; - B6E6BA192BA2D8BE008AA7E1 /* Run swiftlint */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run swiftlint"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$ENABLE_PREVIEWS\" = \"YES\" ]; then exit 0; fi\n${BUILT_PRODUCTS_DIR}/SwiftLintTool\n"; - }; B6F2C8722A7A4C7D000498CF /* Make /Applications symlink, remove app on Clean build */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -10210,10 +10174,8 @@ B630E80129C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 3706FE31293F661700E42796 /* TabCollectionViewModelDelegateMock.swift in Sources */, 3706FE32293F661700E42796 /* BookmarksHTMLReaderTests.swift in Sources */, - 7BBE650E2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */, 3706FE33293F661700E42796 /* FireTests.swift in Sources */, B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, - 7BBE65112BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */, 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 317295D32AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */, 3706FE34293F661700E42796 /* PermissionStoreTests.swift in Sources */, @@ -11578,7 +11540,6 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */, 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */, - 7BBE65102BC67EED008F4EE9 /* NetworkProtectionTestingSupport.swift in Sources */, 1D9FDEC02B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */, B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */, @@ -11658,7 +11619,6 @@ 37CD54B927F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift in Sources */, EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */, 376C4DB928A1A48A00CC0F5B /* FirePopoverViewModelTests.swift in Sources */, - 7BBE650D2BC67BA0008F4EE9 /* NetworkProtectionIPCTunnelControllerTests.swift in Sources */, AAEC74B62642CC6A00C2EFBC /* HistoryStoringMock.swift in Sources */, AA652CB125DD825B009059CC /* LocalBookmarkStoreTests.swift in Sources */, B630794226731F5400DCEE41 /* WKDownloadMock.swift in Sources */, @@ -11774,14 +11734,6 @@ target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; targetProxy = 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */; }; - B637D1BB2BC6AE5600C7DCA7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = B637D1BA2BC6AE5600C7DCA7 /* SwiftLintTool */; - }; - B637D1BD2BC6AE6200C7DCA7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = B637D1BC2BC6AE6200C7DCA7 /* SwiftLintTool */; - }; B6AEB5552BA3042300781A09 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B6E6B9F22BA1FD90008AA7E1 /* sandbox-test-tool */; @@ -12641,7 +12593,7 @@ version = 3.1.4000; }; }; - 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + 3FFD51CF7C19ACBDB9687474 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { @@ -12649,6 +12601,22 @@ version = 138.0.0; }; }; + 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; + requirement = { + kind = exactVersion; + version = 137.0.0; + }; + }; + 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; + requirement = { + kind = exactVersion; + version = 138.1.0; + }; + }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/airbnb/lottie-spm.git"; @@ -12705,13 +12673,21 @@ version = 0.1.0; }; }; + FAE06B199CA1F209B55B34E9 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; + requirement = { + kind = exactVersion; + version = 137.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 1E46E19F2BD029BD0007273A /* Subscription */ = { + 08D4923DC968236E22E373E2 /* Crashes */ = { isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Subscription; + package = FAE06B199CA1F209B55B34E9 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Crashes; }; 1E950E3E2912A10D0051A99B /* ContentBlocking */ = { isa = XCSwiftPackageProductDependency; @@ -12834,10 +12810,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = DDGSync; }; - 373FB4B02B4D6C42004C88D6 /* PreferencesViews */ = { - isa = XCSwiftPackageProductDependency; - productName = PreferencesViews; - }; 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */ = { isa = XCSwiftPackageProductDependency; productName = PreferencesViews; @@ -12863,16 +12835,6 @@ isa = XCSwiftPackageProductDependency; productName = SyncUI; }; - 37CF91582BB416A500BADCAE /* Crashes */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Crashes; - }; - 37CF915A2BB416AC00BADCAE /* Crashes */ = { - isa = XCSwiftPackageProductDependency; - package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; - productName = Crashes; - }; 37DF000429F9C056002B7D3E /* SyncDataProviders */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12888,10 +12850,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Navigation; }; - 4B2537762A11BFE100610219 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 4B2D062B2A11C0E100DE1F49 /* Networking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12913,10 +12871,6 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtection; }; - 4B4D60972A0B2A5C00BCD287 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 4B4D60AE2A0C837F00BCD287 /* Networking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -12934,14 +12888,6 @@ isa = XCSwiftPackageProductDependency; productName = "plugin:InputFilesChecker"; }; - 4B81AD342B29512B00706C96 /* PixelKitTestingUtilities */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKitTestingUtilities; - }; - 4B81AD362B29513100706C96 /* PixelKitTestingUtilities */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKitTestingUtilities; - }; 4BA7C4DC2B3F64E500AFE511 /* LoginItems */ = { isa = XCSwiftPackageProductDependency; productName = LoginItems; @@ -12973,6 +12919,11 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 537FC71EA5115A983FAF3170 /* Crashes */ = { + isa = XCSwiftPackageProductDependency; + package = 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Crashes; + }; 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; @@ -12998,14 +12949,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; - 7B5DD6992AE51FFA001DE99C /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; - 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; @@ -13024,10 +12967,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Common; }; - 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; @@ -13066,14 +13005,6 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; }; - 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; - 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 85D44B852BA08D29001B4AB5 /* Suggestions */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -13114,14 +13045,6 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Persistence; }; - 9D6983F82AC773C3002C02FC /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; - 9D6983FA2AC773C8002C02FC /* PixelKit */ = { - isa = XCSwiftPackageProductDependency; - productName = PixelKit; - }; 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */ = { isa = XCSwiftPackageProductDependency; productName = DataBrokerProtection; @@ -13130,10 +13053,6 @@ isa = XCSwiftPackageProductDependency; productName = DataBrokerProtection; }; - 9DB6E7232AA0DC5800A17F3C /* LoginItems */ = { - isa = XCSwiftPackageProductDependency; - productName = LoginItems; - }; 9DC70B192AA1FA5B005A844B /* LoginItems */ = { isa = XCSwiftPackageProductDependency; productName = LoginItems; @@ -13158,16 +13077,6 @@ package = AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; - B637D1BA2BC6AE5600C7DCA7 /* SwiftLintTool */ = { - isa = XCSwiftPackageProductDependency; - package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; - productName = SwiftLintTool; - }; - B637D1BC2BC6AE6200C7DCA7 /* SwiftLintTool */ = { - isa = XCSwiftPackageProductDependency; - package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; - productName = SwiftLintTool; - }; B65CD8CA2B316DF100A595BB /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = B65CD8C92B316DF100A595BB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; @@ -13253,6 +13162,11 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Configuration; }; + DC3F73D49B2D44464AFEFCD8 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 3FFD51CF7C19ACBDB9687474 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; EE02D41F2BB460C000DBE6B3 /* BrowserServicesKit */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; @@ -13288,6 +13202,61 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKitTestingUtilities; + }; + F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKitTestingUtilities; + }; + F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKitTestingUtilities; + }; + F198C7112BD18A28000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7132BD18A30000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7152BD18A44000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7172BD18A4C000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C7192BD18A5B000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C71B2BD18A61000BF24D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + F198C71D2BD18D88000BF24D /* SwiftLintTool */ = { + isa = XCSwiftPackageProductDependency; + package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; + productName = SwiftLintTool; + }; + F198C71F2BD18D92000BF24D /* SwiftLintTool */ = { + isa = XCSwiftPackageProductDependency; + package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; + productName = SwiftLintTool; + }; F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */ = { isa = XCSwiftPackageProductDependency; package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; @@ -13298,6 +13267,10 @@ package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; productName = BareBonesBrowserKit; }; + F1DF95E62BD188B60045E591 /* LoginItems */ = { + isa = XCSwiftPackageProductDependency; + productName = LoginItems; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 462a0cc553..9799888051 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" : "b8f0e5db431c63943b509d522c157f870ef03ae0", - "version" : "138.0.0" + "revision" : "4ec92e6b94076c53b9b2d34fdcbc2b52cecf9c8e", + "version" : "138.1.0" } }, { @@ -120,7 +120,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "46989693916f56d1186bd59ac15124caef896560", "version" : "1.3.1" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme index 86fd422c11..e343e06df9 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme @@ -157,16 +157,6 @@ ReferencedContainer = "container:LocalPackages/SyncUI"> - - - - - - - - - - - - - - - - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift deleted file mode 100644 index f6ca4e0701..0000000000 --- a/LocalPackages/PixelKit/Package.swift +++ /dev/null @@ -1,50 +0,0 @@ -// swift-tools-version: 5.7 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "PixelKit", - platforms: [ - .macOS("11.4") - ], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "PixelKit", - targets: ["PixelKit"] - ), - .library( - name: "PixelKitTestingUtilities", - targets: ["PixelKitTestingUtilities"] - ) - ], - dependencies: [ - ], - targets: [ - .target( - name: "PixelKit", - dependencies: [ - ], - swiftSettings: [ - .define("DEBUG", .when(configuration: .debug)), - ] - ), - .testTarget( - name: "PixelKitTests", - dependencies: [ - "PixelKit", - "PixelKitTestingUtilities", - ], - swiftSettings: [ - .define("DEBUG", .when(configuration: .debug)) - ] - ), - .target( - name: "PixelKitTestingUtilities", - dependencies: [ - "PixelKit", - ] - ) - ] -) diff --git a/LocalPackages/PixelKit/README.md b/LocalPackages/PixelKit/README.md deleted file mode 100644 index 9d5094a7d8..0000000000 --- a/LocalPackages/PixelKit/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# PixelKit - -This package is meant to provide basic support for firing pixel across different targets. - -This package was designed to not really know specific pixels. Those can be defined -individually by each target importing this package, or through more specialized -shared packages. - -This design decision is meant to make PixelKit lean and to make it possible to use it -for future apps we may decide to make, without it having to carry over all of the business -domain logic for any single app. diff --git a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift b/LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift deleted file mode 100644 index 19f9b8b692..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/String+StaticString.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// String+StaticString.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 - -extension String { - init(_ staticString: StaticString) { - self = staticString.withUTF8Buffer { - String(decoding: $0, as: UTF8.self) - } - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift deleted file mode 100644 index 98bb77c2c4..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/Extensions/URL+PixelKit.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// URL+PixelKit.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 - -extension URL { - - static let pixelBase = ProcessInfo.processInfo.environment["PIXEL_BASE_URL", default: "https://improving.duckduckgo.com"] - - public static func pixelUrl(forPixelNamed pixelName: String) -> URL { - let urlString = "\(Self.pixelBase)/t/\(pixelName)" - return URL(string: urlString)! - } - -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift deleted file mode 100644 index b692271709..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// PixelKit+Parameters.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 - -public extension PixelKit { - - enum Parameters: Hashable { - public static let duration = "duration" - public static let test = "test" - public static let appVersion = "appVersion" - public static let pixelSource = "pixelSource" - public static let osMajorVersion = "osMajorVersion" - - public static let errorCode = "e" - public static let errorDomain = "d" - public static let errorCount = "c" - public static let errorSource = "error_source" - public static let sourceBrowserVersion = "source_browser_version" - public static let underlyingErrorCode = "ue" - public static let underlyingErrorDomain = "ud" - public static let underlyingErrorSQLiteCode = "sqlrc" - public static let underlyingErrorSQLiteExtendedCode = "sqlerc" - - public static let keychainFieldName = "fieldName" - public static let keychainErrorCode = "keychain_error_code" - - public static let emailCohort = "cohort" - public static let emailLastUsed = "duck_address_last_used" - - public static let assertionMessage = "message" - public static let assertionFile = "file" - public static let assertionLine = "line" - - public static let function = "function" - public static let line = "line" - - public static let latency = "latency" - public static let server = "server" - public static let networkType = "net_type" - - // Pixel experiments - public static let experimentCohort = "cohort" - - // Dashboard - public static let dashboardTriggerOrigin = "trigger_origin" - - // VPN - public static let vpnBreakageCategory = "breakageCategory" - public static let vpnBreakageDescription = "breakageDescription" - public static let vpnBreakageMetadata = "breakageMetadata" - - public static let reason = "reason" - - public static let vpnCohort = "cohort" - } - - enum Values { - public static let test = "1" - } - -} - -public protocol ErrorWithPixelParameters { - - var errorParameters: [String: String] { get } - -} - -public extension Error { - - var pixelParameters: [String: String] { - var params = [String: String]() - - if let errorWithUserInfo = self as? ErrorWithPixelParameters { - params = errorWithUserInfo.errorParameters - } - - let nsError = self as NSError - - params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDomain] = nsError.domain - - let underlyingErrorParameters = self.underlyingErrorParameters(for: nsError) - params.merge(underlyingErrorParameters) { first, _ in - return first - } - - if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteCode] = "\(sqlErrorCode.intValue)" - } - - if let sqlExtendedErrorCode = nsError.userInfo["SQLiteExtendedResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteExtendedCode] = "\(sqlExtendedErrorCode.intValue)" - } - - return params - } - - /// Recursive call to add underlying error information - /// - func underlyingErrorParameters(for nsError: NSError, level: Int = 0) -> [String: String] { - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { - let errorCodeParameterName = PixelKit.Parameters.underlyingErrorCode + (level == 0 ? "" : String(level + 1)) - let errorDomainParameterName = PixelKit.Parameters.underlyingErrorDomain + (level == 0 ? "" : String(level + 1)) - - let currentUnderlyingErrorParameters = [ - errorCodeParameterName: "\(underlyingError.code)", - errorDomainParameterName: underlyingError.domain - ] - - // Check if the underlying error has an underlying error of its own - let additionalParameters = underlyingErrorParameters(for: underlyingError, level: level + 1) - - return currentUnderlyingErrorParameters.merging(additionalParameters) { first, _ in - return first // Doesn't really matter as there should be no conflict of parameters - } - } - - return [:] - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift deleted file mode 100644 index d647b28835..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ /dev/null @@ -1,476 +0,0 @@ -// -// PixelKit.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 // swiftlint:disable:this enforce_os_log_wrapper - -public final class PixelKit { - /// `true` if a request is fired, `false` otherwise - public typealias CompletionBlock = (Bool, Error?) -> Void - - /// The frequency with which a pixel is sent to our endpoint. - public enum Frequency { - /// The default frequency for pixels. This fires pixels with the event names as-is. - case standard - - /// [Legacy] Used in Pixel.fire(...) as .unique but without the `_u` requirement in the name - case legacyInitial - - /// Sent only once ever. The timestamp for this pixel is stored. - /// Note: This is the only pixel that MUST end with `_u`, Name for pixels of this type must end with if it doesn't an assertion is fired. - case unique - - /// [Legacy] Used in Pixel.fire(...) as .daily but without the `_d` automatically added to the name - case legacyDaily - - /// Sent once per day. The last timestamp for this pixel is stored and compared to the current date. Pixels of this type will have `_d` appended to their name. - case daily - - /// Sent once per day with a `_d` suffix, in addition to every time it is called with a `_c` suffix. - /// This means a pixel will get sent twice the first time it is called per-day, and subsequent calls that day will only send the `_c` variant. - /// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected. - case dailyAndCount - - fileprivate var description: String { - switch self { - case .standard: - "Standard" - case .legacyInitial: - "Legacy Initial" - case .unique: - "Unique" - case .legacyDaily: - "Legacy Daily" - case .daily: - "Daily" - case .dailyAndCount: - "Daily and Count" - } - } - } - - public enum Header { - public static let acceptEncoding = "Accept-Encoding" - public static let acceptLanguage = "Accept-Language" - public static let userAgent = "User-Agent" - public static let ifNoneMatch = "If-None-Match" - public static let moreInfo = "X-DuckDuckGo-MoreInfo" - public static let client = "X-DuckDuckGo-Client" - } - - public enum Source: String { - case macStore = "browser-appstore" - case macDMG = "browser-dmg" - case iOS = "phone" - case iPadOS = "tablet" - } - - /// A closure typealias to request sending pixels through the network. - public typealias FireRequest = ( - _ pixelName: String, - _ headers: [String: String], - _ parameters: [String: String], - _ allowedQueryReservedCharacters: CharacterSet?, - _ callBackOnMainThread: Bool, - _ onComplete: @escaping CompletionBlock) -> Void - - public typealias Event = PixelKitEvent - public static let duckDuckGoMorePrivacyInfo = URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/atb/")! - private let defaults: UserDefaults - - private let logger = Logger(subsystem: "com.duckduckgo.PixelKit", category: "PixelKit") - - private static let defaultDailyPixelCalendar: Calendar = { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - return calendar - }() - - private static let weeksToCoalesceCohort = 6 - - private let dateGenerator: () -> Date - - public private(set) static var shared: PixelKit? - - private let appVersion: String - private let defaultHeaders: [String: String] - private let fireRequest: FireRequest - - /// Sets up PixelKit for the entire app. - /// - /// - Parameters: - /// - `dryRun`: if `true`, simulate requests and "send" them at an accelerated rate (once every 2 minutes instead of once a day) - /// - `source`: if set, adds a `pixelSource` parameter to the pixel call; this can be used to specify which target is sending the pixel - /// - `fireRequest`: this is not triggered when `dryRun` is `true` - public static func setUp(dryRun: Bool = false, - appVersion: String, - source: String? = nil, - defaultHeaders: [String: String], - dailyPixelCalendar: Calendar? = nil, - dateGenerator: @escaping () -> Date = Date.init, - defaults: UserDefaults, - fireRequest: @escaping FireRequest) { - shared = PixelKit(dryRun: dryRun, - appVersion: appVersion, - source: source, - defaultHeaders: defaultHeaders, - dailyPixelCalendar: dailyPixelCalendar, - dateGenerator: dateGenerator, - defaults: defaults, - fireRequest: fireRequest) - } - - public static func tearDown() { - shared = nil - } - - private var dryRun: Bool - private let source: String? - private let pixelCalendar: Calendar - - public init(dryRun: Bool, - appVersion: String, - source: String? = nil, - defaultHeaders: [String: String], - dailyPixelCalendar: Calendar? = nil, - dateGenerator: @escaping () -> Date = Date.init, - defaults: UserDefaults, - fireRequest: @escaping FireRequest) { - - self.dryRun = dryRun - self.appVersion = appVersion - self.source = source - self.defaultHeaders = defaultHeaders - self.pixelCalendar = dailyPixelCalendar ?? Self.defaultDailyPixelCalendar - self.dateGenerator = dateGenerator - self.defaults = defaults - self.fireRequest = fireRequest - logger.debug("👾 PixelKit initialised: dryRun: \(self.dryRun, privacy: .public) appVersion: \(self.appVersion, privacy: .public) source: \(self.source ?? "-", privacy: .public) defaultHeaders: \(self.defaultHeaders, privacy: .public) pixelCalendar: \(self.pixelCalendar, privacy: .public)") - } - - // swiftlint:disable:next function_body_length cyclomatic_complexity - private func fire(pixelNamed pixelName: String, - frequency: Frequency, - withHeaders headers: [String: String]?, - withAdditionalParameters params: [String: String]?, - withError error: Error?, - allowedQueryReservedCharacters: CharacterSet?, - includeAppVersionParameter: Bool, - onComplete: @escaping CompletionBlock) { - - var newParams = params ?? [:] - if includeAppVersionParameter { newParams[Parameters.appVersion] = appVersion } - if let source { newParams[Parameters.pixelSource] = source } - if let error { newParams.appendErrorPixelParams(error: error) } - - #if DEBUG - newParams[Parameters.test] = Values.test - #endif - - var headers = headers ?? defaultHeaders - headers[Header.moreInfo] = "See " + Self.duckDuckGoMorePrivacyInfo.absoluteString - headers[Header.client] = "macOS" - - switch frequency { - case .standard: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - case .legacyInitial: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") - if !pixelHasBeenFiredEver(pixelName) { - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: newParams, skipped: true) - } - case .unique: - reportErrorIf(pixel: pixelName, endsWith: "_d") - guard pixelName.hasSuffix("_u") else { - assertionFailure("Unique pixel: must end with _u") - return - } - if !pixelHasBeenFiredEver(pixelName) { - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: newParams, skipped: true) - } - case .legacyDaily: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") - if !pixelHasBeenFiredToday(pixelName) { - fireRequestWrapper(pixelName, headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: newParams, skipped: true) - } - case .daily: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") // Because is added automatically - if !pixelHasBeenFiredToday(pixelName) { - fireRequestWrapper(pixelName + "_d", headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName + "_d", frequency: frequency, parameters: newParams, skipped: true) - } - case .dailyAndCount: - reportErrorIf(pixel: pixelName, endsWith: "_u") - reportErrorIf(pixel: pixelName, endsWith: "_d") // Because is added automatically - reportErrorIf(pixel: pixelName, endsWith: "_c") // Because is added automatically - if !pixelHasBeenFiredToday(pixelName) { - fireRequestWrapper(pixelName + "_d", headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - updatePixelLastFireDate(pixelName: pixelName) - } else { - printDebugInfo(pixelName: pixelName + "_d", frequency: frequency, parameters: newParams, skipped: true) - } - - fireRequestWrapper(pixelName + "_c", headers, newParams, allowedQueryReservedCharacters, true, frequency, onComplete) - } - } - - /// If the pixel name ends with the forbiddenString then an error is logged or an assertion failure is fired in debug - func reportErrorIf(pixel: String, endsWith forbiddenString: String) { - if pixel.hasSuffix(forbiddenString) { - logger.error("Pixel \(pixel, privacy: .public) must not end with \(forbiddenString, privacy: .public)") - assertionFailure("Pixel \(pixel) must not end with \(forbiddenString)") - } - } - - private func printDebugInfo(pixelName: String, frequency: Frequency, parameters: [String: String], skipped: Bool = false) { - let params = parameters.filter { key, _ in !["test"].contains(key) } - logger.debug("👾[\(frequency.description, privacy: .public)-\(skipped ? "Skipped" : "Fired", privacy: .public)] \(pixelName, privacy: .public) \(params, privacy: .public)") - } - - private func fireRequestWrapper( - _ pixelName: String, - _ headers: [String: String], - _ parameters: [String: String], - _ allowedQueryReservedCharacters: CharacterSet?, - _ callBackOnMainThread: Bool, - _ frequency: Frequency, - _ onComplete: @escaping CompletionBlock) { - printDebugInfo(pixelName: pixelName, frequency: frequency, parameters: parameters, skipped: false) - guard !dryRun else { - // simulate server response time for Dry Run mode - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - onComplete(true, nil) - } - return - } - fireRequest(pixelName, headers, parameters, allowedQueryReservedCharacters, callBackOnMainThread, onComplete) - } - - private func prefixedName(for event: Event) -> String { - if event.name.hasPrefix("m_mac_") { - return event.name - } - - if let debugEvent = event as? DebugEvent { - return "m_mac_debug_\(debugEvent.name)" - } else { - return "m_mac_\(event.name)" - } - } - - public func fire(_ event: Event, - frequency: Frequency = .standard, - withHeaders headers: [String: String]? = nil, - withAdditionalParameters params: [String: String]? = nil, - withError error: Error? = nil, - allowedQueryReservedCharacters: CharacterSet? = nil, - includeAppVersionParameter: Bool = true, - onComplete: @escaping CompletionBlock = { _, _ in }) { - - let pixelName = prefixedName(for: event) - - if !dryRun { - if frequency == .daily, pixelHasBeenFiredToday(pixelName) { - onComplete(false, nil) - return - } else if frequency == .unique, pixelHasBeenFiredEver(pixelName) { - onComplete(false, nil) - return - } - } - - let newParams: [String: String]? - switch (event.parameters, params) { - case (.some(let parameters), .none): - newParams = parameters - case (.none, .some(let parameters)): - newParams = parameters - case (.some(let params1), .some(let params2)): - newParams = params1.merging(params2) { $1 } - case (.none, .none): - newParams = nil - } - - let newError: Error? - - if let event = event as? PixelKitEventV2, - let error = event.error { - - // For v2 events we only consider the error specified in the event - // and purposedly ignore the parameter in this call. - // This is to encourage moving the error over to the protocol error - // instead of still relying on the parameter of this call. - newError = error - } else { - newError = error - } - - fire(pixelNamed: pixelName, - frequency: frequency, - withHeaders: headers, - withAdditionalParameters: newParams, - withError: newError, - allowedQueryReservedCharacters: allowedQueryReservedCharacters, - includeAppVersionParameter: includeAppVersionParameter, - onComplete: onComplete) - } - - public static func fire(_ event: Event, - frequency: Frequency = .standard, - withHeaders headers: [String: String] = [:], - withAdditionalParameters parameters: [String: String]? = nil, - withError error: Error? = nil, - allowedQueryReservedCharacters: CharacterSet? = nil, - includeAppVersionParameter: Bool = true, - onComplete: @escaping CompletionBlock = { _, _ in }) { - - Self.shared?.fire(event, - frequency: frequency, - withHeaders: headers, - withAdditionalParameters: parameters, - withError: error, - allowedQueryReservedCharacters: allowedQueryReservedCharacters, - includeAppVersionParameter: includeAppVersionParameter, - onComplete: onComplete) - } - - private func cohort(from cohortLocalDate: Date?, dateGenerator: () -> Date = Date.init) -> String? { - guard let cohortLocalDate, - let baseDate = pixelCalendar.date(from: .init(year: 2023, month: 1, day: 1)), - let weeksSinceCohortAssigned = pixelCalendar.dateComponents([.weekOfYear], from: cohortLocalDate, to: dateGenerator()).weekOfYear, - let assignedCohort = pixelCalendar.dateComponents([.weekOfYear], from: baseDate, to: cohortLocalDate).weekOfYear else { - return nil - } - - if weeksSinceCohortAssigned > Self.weeksToCoalesceCohort { - return "" - } else { - return "week-" + String(assignedCohort + 1) - } - } - - public static func cohort(from cohortLocalDate: Date?, dateGenerator: () -> Date = Date.init) -> String { - Self.shared?.cohort(from: cohortLocalDate, dateGenerator: dateGenerator) ?? "" - } - - public static func pixelLastFireDate(event: Event) -> Date? { - Self.shared?.pixelLastFireDate(event: event) - } - - public func pixelLastFireDate(pixelName: String) -> Date? { - var date = defaults.object(forKey: userDefaultsKeyName(forPixelName: pixelName)) as? Date - if date == nil { - date = defaults.object(forKey: legacyUserDefaultsKeyName(forPixelName: pixelName)) as? Date - } - return date - } - - public func pixelLastFireDate(event: Event) -> Date? { - pixelLastFireDate(pixelName: prefixedName(for: event)) - } - - private func updatePixelLastFireDate(pixelName: String) { - defaults.set(dateGenerator(), forKey: userDefaultsKeyName(forPixelName: pixelName)) - } - - private func pixelHasBeenFiredToday(_ name: String) -> Bool { - guard !dryRun else { - if let lastFireDate = pixelLastFireDate(pixelName: name), - let twoMinsAgo = pixelCalendar.date(byAdding: .minute, value: -2, to: dateGenerator()) { - return lastFireDate >= twoMinsAgo - } - - return false - } - - if let lastFireDate = pixelLastFireDate(pixelName: name) { - return pixelCalendar.isDate(dateGenerator(), inSameDayAs: lastFireDate) - } - - return false - } - - private func pixelHasBeenFiredEver(_ name: String) -> Bool { - pixelLastFireDate(pixelName: name) != nil - } - - public func clearFrequencyHistoryFor(pixel: PixelKitEventV2) { - guard let name = Self.shared?.userDefaultsKeyName(forPixelName: pixel.name) else { - return - } - self.defaults.removeObject(forKey: name) - } - - public func clearFrequencyHistoryForAllPixels() { - for (key, _) in self.defaults.dictionaryRepresentation() { - if key.hasPrefix(Self.storageKeyPrefixLegacy) || key.hasPrefix(Self.storageKeyPrefix) { - self.defaults.removeObject(forKey: key) - self.logger.debug("🚮 Removing from storage \(key, privacy: .public)") - } - } - } - - static let storageKeyPrefixLegacy = "com.duckduckgo.network-protection.pixel." - static let storageKeyPrefix = "com.duckduckgo.network-protection.pixel." - - /// Initially PixelKit was configured only for serving netP so these very specific keys were used, now PixelKit serves the entire app so we need to move away from them. - /// NOTE: I would remove this 6 months after release - private func legacyUserDefaultsKeyName(forPixelName pixelName: String) -> String { - dryRun - ? "\(Self.storageKeyPrefixLegacy)\(pixelName).dry-run" - : "\(Self.storageKeyPrefixLegacy)\(pixelName)" - } - - private func userDefaultsKeyName(forPixelName pixelName: String) -> String { - return "\(Self.storageKeyPrefix)\(pixelName)\( dryRun ? ".dry-run" : "" )" - } -} - -extension Dictionary where Key == String, Value == String { - - mutating func appendErrorPixelParams(error: Error) { - self.merge(error.pixelParameters) { _, second in - return second - } - } -} - -internal extension PixelKit { - - /// [USE ONLY FOR TESTS] Sets the shared PixelKit.shared singleton - /// - Parameter pixelkit: A custom instance of PixelKit - static func setSharedForTesting(pixelKit: PixelKit) { - Self.shared = pixelKit - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift deleted file mode 100644 index ca352f3347..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// PixelKitEvent.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 - -/// An event that can be fired using PixelKit. -/// -public protocol PixelKitEvent { - var name: String { get } - var parameters: [String: String]? { get } -} - -/// Implementation of ``PixelKitEvent`` with specific logic for debug events. -/// -public final class DebugEvent: PixelKitEvent { - public enum EventType { - case assertionFailure(message: String, file: StaticString, line: UInt) - case custom(_ event: PixelKitEvent) - } - - public let eventType: EventType - public let error: Error? - - public init(eventType: EventType, error: Error? = nil) { - self.eventType = eventType - self.error = error - } - - public init(_ event: PixelKitEvent, error: Error? = nil) { - self.eventType = .custom(event) - self.error = error - } - - public var name: String { - switch eventType { - case .assertionFailure: - return "assertion_failure" - case .custom(let event): - return event.name - } - } - - public var parameters: [String: String]? { - var params: [String: String] - - if case let .custom(event) = eventType, - let eventParams = event.parameters { - params = eventParams - } else { - params = [String: String]() - } - - if let errorWithUserInfo = error as? ErrorWithPixelParameters { - params = errorWithUserInfo.errorParameters - } - - if case let .assertionFailure(message, file, line) = eventType { - params[PixelKit.Parameters.assertionMessage] = message - params[PixelKit.Parameters.assertionFile] = String(file) - params[PixelKit.Parameters.assertionLine] = String(line) - } - - if let error = error { - let nsError = error as NSError - - params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDomain] = nsError.domain - - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { - params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain - } - - if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteCode] = "\(sqlErrorCode.intValue)" - } - - if let sqlExtendedErrorCode = nsError.userInfo["SQLiteExtendedResultCode"] as? NSNumber { - params[PixelKit.Parameters.underlyingErrorSQLiteExtendedCode] = "\(sqlExtendedErrorCode.intValue)" - } - } - - return params - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift deleted file mode 100644 index dc641454c9..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// PixelKitEventV2.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 - -/// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent -/// -/// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. -/// The starting example of how this can be useful is error parameter handling - this protocol allows -/// the implementer to specify an error without having to know about its parameterisation. -/// -/// The reason this wasn't done directly in `PixelKitEvent` is to reduce the risk of breaking existing -/// pixels, and to allow us to migrate towards this incrementally. -/// -public protocol PixelKitEventV2: PixelKitEvent { - var error: Error? { get } -} - -/// Protocol to support mocking pixel firing. -/// -/// We're adding support for `PixelKitEventV2` events strategically because adding support for earlier pixels -/// would be more complicated and time consuming. The idea of V2 events is that fire calls should not include a lot -/// of parameters. Parameters should be provided by the `PixelKitEventV2` protocol (extending it if necessary) -/// and the call to `fire` should process those properties to serialize in the requests. -/// -public protocol PixelFiring { - func fire(_ event: PixelKitEventV2) - - func fire(_ event: PixelKitEventV2, - frequency: PixelKit.Frequency) -} - -extension PixelKit: PixelFiring { - public func fire(_ event: PixelKitEventV2) { - fire(event, frequency: .standard) - } - - public func fire(_ event: PixelKitEventV2, - frequency: PixelKit.Frequency) { - - fire(event, frequency: frequency, onComplete: { _, _ in }) - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift deleted file mode 100644 index 1a1d8c2f64..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// PixelFireExpectations.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 - -/// Structure containing information about a pixel fire event. -/// -/// This is useful for test validation for libraries that rely on PixelKit, to make sure the pixels contain -/// all of the fields they are supposed to contain.. -/// -public struct PixelFireExpectations { - let pixelName: String - var error: Error? - var underlyingErrors: [Error] - var customFields: [String: String]? - - /// Convenience initializer for cleaner semantics - /// - public static func expect(pixelName: String, error: Error? = nil, underlyingErrors: [Error] = [], customFields: [String: String]? = nil) -> PixelFireExpectations { - - .init(pixelName: pixelName, error: error, underlyingErrors: underlyingErrors, customFields: customFields) - } - - public init(pixelName: String, error: Error? = nil, underlyingErrors: [Error] = [], customFields: [String: String]? = nil) { - self.pixelName = pixelName - self.error = error - self.underlyingErrors = underlyingErrors - self.customFields = customFields - } - - public var parameters: [String: String] { - var parameters = customFields ?? [String: String]() - - if let nsError = error as? NSError { - parameters[PixelKit.Parameters.errorCode] = String(nsError.code) - parameters[PixelKit.Parameters.errorDomain] = nsError.domain - } - - for (index, error) in underlyingErrors.enumerated() { - let errorCodeParameterName = PixelKit.Parameters.underlyingErrorCode + (index == 0 ? "" : String(index + 1)) - let errorDomainParameterName = PixelKit.Parameters.underlyingErrorDomain + (index == 0 ? "" : String(index + 1)) - let nsError = error as NSError - - parameters[errorCodeParameterName] = String(nsError.code) - parameters[errorDomainParameterName] = nsError.domain - } - - return parameters - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift deleted file mode 100644 index 00547deb5b..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/ValidatePixel.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ValidatePixel.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 -import XCTest - -public final class PixelRequestValidator { - public init() {} - - public func validateBasicPixelParams( - expectedAppVersion: String, - expectedUserAgent: String, - requestParameters parameters: [String: String], - requestHeaders headers: [String: String]) { - - XCTAssertEqual(parameters[PixelKit.Parameters.test], "1") - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], expectedAppVersion) - - XCTAssertEqual(headers[PixelKit.Header.userAgent], expectedUserAgent) - XCTAssertEqual(headers[PixelKit.Header.acceptEncoding], "gzip;q=1.0, compress;q=0.5") - XCTAssertNotNil(headers[PixelKit.Header.acceptLanguage]) - XCTAssertNotNil(headers[PixelKit.Header.moreInfo], PixelKit.duckDuckGoMorePrivacyInfo.absoluteString) - } - - public func validateDebugPixelParams( - expectedError: Error?, - requestParameters parameters: [String: String]) { - - if let error = expectedError as? NSError { - XCTAssertEqual(parameters[PixelKit.Parameters.errorCode], "\(error.code)") - XCTAssertEqual(parameters[PixelKit.Parameters.errorDomain], error.domain) - } - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift deleted file mode 100644 index 81232cd4bb..0000000000 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// XCTestCase+PixelKit.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 -@testable import PixelKit -import XCTest - -public extension XCTestCase { - - // MARK: - Parameters - - /// List of standard pixel parameters. - /// This is useful to support filtering these parameters out if needed. - private static var standardPixelParameters = [ - PixelKit.Parameters.appVersion, - PixelKit.Parameters.pixelSource, - PixelKit.Parameters.test - ] - - /// List of errror pixel parameters - private static var errorPixelParameters = [ - PixelKit.Parameters.errorCode, - PixelKit.Parameters.errorDomain - ] - - /// List of underlying error pixel parameters - private static var underlyingErrorPixelParameters = [ - PixelKit.Parameters.underlyingErrorCode, - PixelKit.Parameters.underlyingErrorDomain - ] - - /// Filter out the standard parameters. - private static func filterStandardPixelParameters(from parameters: [String: String]) -> [String: String] { - parameters.filter { element in - !standardPixelParameters.contains(element.key) - } - } - - static var pixelPlatformPrefix: String { -#if os(macOS) - return "m_mac_" -#elseif os(iOS) - return "m_" -#endif - } - - /// These parameters are known to be expected just based on the event definition. - /// - /// They're not a complete list of parameters for the event, as the fire call may contain extra information - /// that results in additional parameters. Ideally we want most (if not all) that information to eventually - /// make part of the pixel definition. - func knownExpectedParameters(for event: PixelKitEventV2) -> [String: String] { - var expectedParameters = [String: String]() - - if let error = event.error { - let nsError = error as NSError - expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" - expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain - - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { - expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain - } - } - - return expectedParameters - } - - // MARK: - Misc Convenience - - private var userDefaults: UserDefaults { - UserDefaults(suiteName: "testing_\(UUID().uuidString)")! - } - - // MARK: - Pixel Firing Expectations - - func fire(_ event: PixelKitEventV2, frequency: PixelKit.Frequency, and expectations: PixelFireExpectations, file: StaticString, line: UInt) { - verifyThat(event, frequency: frequency, meets: expectations, file: file, line: line) - } - - /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. - /// - /// This method also checks that there is internal consistency in the expected fields. - func verifyThat(_ event: PixelKitEventV2, - frequency: PixelKit.Frequency, - meets expectations: PixelFireExpectations, - file: StaticString, - line: UInt) { - let expectedPixelNames: [String] = expectedPixelNames(originalName: event.name, frequency: frequency) - let knownExpectedParameters = knownExpectedParameters(for: event) - let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") - - if frequency == .dailyAndCount { - callbackExecutedExpectation.expectedFulfillmentCount = 2 - } - - // Ensure PixelKit is torn down before setting it back up, avoiding unit test race conditions: - PixelKit.tearDown() - - PixelKit.setUp(dryRun: false, - appVersion: "1.0.5", - source: "test-app", - defaultHeaders: [:], - defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in - callbackExecutedExpectation.fulfill() - - let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) - - // Internal validations - XCTAssertTrue(expectedPixelNames.contains(firedPixelName), file: file, line: line) - XCTAssertTrue(knownExpectedParameters.allSatisfy { (key, value) in - firedParameters[key] == value - }) - - if frequency == .dailyAndCount { - XCTAssertTrue(firedPixelName.hasPrefix(expectations.pixelName)) - XCTAssertTrue(firedPixelName.hasSuffix("_c") || firedPixelName.hasSuffix("_d")) - XCTAssertEqual(firedPixelName.count, expectations.pixelName.count + 2) - let exp = self.expectedPixelNames(originalName: expectations.pixelName, frequency: frequency) - XCTAssertTrue(exp.contains(firedPixelName)) - } else { - XCTAssertEqual(expectations.pixelName, firedPixelName) - } - XCTAssertEqual(firedParameters, expectations.parameters) - - completion(true, nil) - } - - PixelKit.fire(event, frequency: frequency) - waitForExpectations(timeout: 0.1) - } - - func expectedPixelNames(originalName: String, frequency: PixelKit.Frequency) -> [String] { - let expectedPixelNameWithoutSuffix = originalName.hasPrefix(Self.pixelPlatformPrefix) ? originalName : Self.pixelPlatformPrefix + originalName - var expectedPixelNames: [String] = [] - - switch frequency { - case .standard: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .legacyInitial: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .unique: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .legacyDaily: - expectedPixelNames.append(expectedPixelNameWithoutSuffix) - case .daily: - expectedPixelNames.append(expectedPixelNameWithoutSuffix.appending("_d")) - case .dailyAndCount: - expectedPixelNames.append(expectedPixelNameWithoutSuffix.appending("_d")) - expectedPixelNames.append(expectedPixelNameWithoutSuffix.appending("_c")) - } - return expectedPixelNames - } -} diff --git a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift deleted file mode 100644 index 4d661d3336..0000000000 --- a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// PixelKitParametersTests.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 -@testable import PixelKit -import PixelKitTestingUtilities - -final class PixelKitParametersTests: XCTestCase { - - /// Test events for convenience - /// - private enum TestEvent: PixelKitEventV2 { - case errorEvent(error: Error) - - var name: String { - switch self { - case .errorEvent: - return "error_event" - } - } - - var parameters: [String: String]? { - nil - } - - var error: Error? { - switch self { - case .errorEvent(let error): - error - } - } - } - - /// Test that when firing pixels that include multiple levels of underlying error information, all levels - /// are properly included in the pixel. - /// - func testUnderlyingErrorInformationParameters() { - let underlyingError3 = NSError(domain: "test", code: 3) - let underlyingError2 = NSError( - domain: "test", - code: 2, - userInfo: [ - NSUnderlyingErrorKey: underlyingError3 as NSError - ]) - let topLevelError = NSError( - domain: "test", - code: 1, - userInfo: [ - NSUnderlyingErrorKey: underlyingError2 as NSError - ]) - - fire(TestEvent.errorEvent(error: topLevelError), - frequency: .standard, - and: .expect(pixelName: "m_mac_error_event", - error: topLevelError, - underlyingErrors: [underlyingError2, underlyingError3]), - file: #filePath, - line: #line) - } -} diff --git a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift deleted file mode 100644 index fed5b7789d..0000000000 --- a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift +++ /dev/null @@ -1,385 +0,0 @@ -// -// PixelKitTests.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 PixelKit -import os.log // swiftlint:disable:this enforce_os_log_wrapper - -final class PixelKitTests: XCTestCase { - - private func userDefaults() -> UserDefaults { - UserDefaults(suiteName: "testing_\(UUID().uuidString)")! - } - - /// Test events for convenience - /// - private enum TestEvent: String, PixelKitEvent { - case testEvent - case testEventWithoutParameters - case dailyEvent - case dailyEventWithoutParameters - case dailyAndContinuousEvent - case dailyAndContinuousEventWithoutParameters - case uniqueEvent - - var name: String { - switch self { - case .uniqueEvent: - return "\(rawValue)_u" - default: - return rawValue - } - } - - var parameters: [String: String]? { - switch self { - case .testEvent, .dailyEvent, .dailyAndContinuousEvent, .uniqueEvent: - return [ - "eventParam1": "eventParamValue1", - "eventParam2": "eventParamValue2" - ] - case .testEventWithoutParameters, .dailyEventWithoutParameters, .dailyAndContinuousEventWithoutParameters: - return nil - } - } - - var frequency: PixelKit.Frequency { - switch self { - case .testEvent, .testEventWithoutParameters: - return .standard - case .uniqueEvent: - return .unique - case .dailyEvent, .dailyEventWithoutParameters: - return .daily - case .dailyAndContinuousEvent, .dailyAndContinuousEventWithoutParameters: - return .dailyAndCount - } - } - } - - /// Test that a dry run won't execute the fire request callback. - /// - func testDryRunWontExecuteCallback() async { - let appVersion = "1.0.5" - let headers: [String: String] = [:] - - let pixelKit = PixelKit(dryRun: true, appVersion: appVersion, defaultHeaders: headers, dailyPixelCalendar: nil, defaults: userDefaults()) { _, _, _, _, _, _ in - - XCTFail("This callback should not be executed when doing a dry run") - } - - pixelKit.fire(TestEvent.testEvent) - } - - /// Tests firing a sample pixel and ensuring that all fields are properly set in the fire request callback. - /// - func testFiringASamplePixel() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.testEvent - let userDefaults = userDefaults() - - // Set expectations - let expectedPixelName = "m_mac_\(event.name)" - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - defaults: userDefaults) { firedPixelName, firedHeaders, parameters, _, _, _ in - - fireCallbackCalled.fulfill() - - XCTAssertEqual(expectedPixelName, firedPixelName) - XCTAssertTrue(headers.allSatisfy({ key, value in - firedHeaders[key] == value - })) - - XCTAssertEqual(firedHeaders[PixelKit.Header.moreInfo], "See \(PixelKit.duckDuckGoMorePrivacyInfo)") - - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], appVersion) -#if DEBUG - XCTAssertEqual(parameters[PixelKit.Parameters.test], PixelKit.Values.test) -#else - XCTAssertNil(parameters[PixelKit.Parameters.test]) -#endif - } - - // Run test - pixelKit.fire(event) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// We test firing a daily pixel for the first time executes the fire request callback with the right parameters - /// - func testFiringDailyPixelForTheFirstTime() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.dailyEvent - let userDefaults = userDefaults() - - // Set expectations - let expectedPixelName = "m_mac_\(event.name)_d" - let expectedMoreInfoString = "See \(PixelKit.duckDuckGoMorePrivacyInfo)" - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - defaults: userDefaults) { firedPixelName, firedHeaders, parameters, _, _, _ in - - fireCallbackCalled.fulfill() - - XCTAssertEqual(expectedPixelName, firedPixelName) - XCTAssertTrue(headers.allSatisfy({ key, value in - firedHeaders[key] == value - })) - - XCTAssertEqual(firedHeaders[PixelKit.Header.moreInfo], expectedMoreInfoString) - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], appVersion) -#if DEBUG - XCTAssertEqual(parameters[PixelKit.Parameters.test], PixelKit.Values.test) -#else - XCTAssertNil(parameters[PixelKit.Parameters.test]) -#endif - } - - // Run test - pixelKit.fire(event, frequency: .daily) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// We test firing a daily pixel a second time does not execute the fire request callback. - /// - func testDailyPixelDoubleFiringFrequency() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.dailyEvent - let userDefaults = userDefaults() - - // Set expectations - let expectedPixelName = "m_mac_\(event.name)_d" - let expectedMoreInfoString = "See \(PixelKit.duckDuckGoMorePrivacyInfo)" - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - fireCallbackCalled.expectedFulfillmentCount = 1 - fireCallbackCalled.assertForOverFulfill = true - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - defaults: userDefaults) { firedPixelName, firedHeaders, parameters, _, _, _ in - - fireCallbackCalled.fulfill() - - XCTAssertEqual(expectedPixelName, firedPixelName) - XCTAssertTrue(headers.allSatisfy({ key, value in - firedHeaders[key] == value - })) - - XCTAssertEqual(firedHeaders[PixelKit.Header.moreInfo], expectedMoreInfoString) - XCTAssertEqual(parameters[PixelKit.Parameters.appVersion], appVersion) -#if DEBUG - XCTAssertEqual(parameters[PixelKit.Parameters.test], PixelKit.Values.test) -#else - XCTAssertNil(parameters[PixelKit.Parameters.test]) -#endif - } - - // Run test - pixelKit.fire(event, frequency: .daily) - pixelKit.fire(event, frequency: .daily) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// Test firing a daily pixel a few times - func testDailyPixelFrequency() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.dailyEvent - let userDefaults = userDefaults() - - let timeMachine = TimeMachine() - - // Set expectations - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - fireCallbackCalled.expectedFulfillmentCount = 3 - fireCallbackCalled.assertForOverFulfill = true - - // Prepare mock to validate expectations - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - dateGenerator: timeMachine.now, - defaults: userDefaults) { _, _, _, _, _, _ in - fireCallbackCalled.fulfill() - } - - // Run test - pixelKit.fire(event, frequency: .daily) // Fired - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .legacyDaily) // Skipped - - timeMachine.travel(by: .day, value: 1) - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .legacyDaily) // Fired - - timeMachine.travel(by: .hour, value: 10) - pixelKit.fire(event, frequency: .legacyDaily) // Skipped - - timeMachine.travel(by: .day, value: 1) - pixelKit.fire(event, frequency: .legacyDaily) // Fired - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - /// Test firing a unique pixel - func testUniquePixel() { - // Prepare test parameters - let appVersion = "1.0.5" - let headers = ["a": "2", "b": "3", "c": "2000"] - let event = TestEvent.uniqueEvent - let userDefaults = userDefaults() - - let timeMachine = TimeMachine() - - // Set expectations - let fireCallbackCalled = expectation(description: "Expect the pixel firing callback to be called") - fireCallbackCalled.expectedFulfillmentCount = 1 - fireCallbackCalled.assertForOverFulfill = true - - let pixelKit = PixelKit(dryRun: false, - appVersion: appVersion, - defaultHeaders: headers, - dailyPixelCalendar: nil, - dateGenerator: timeMachine.now, - defaults: userDefaults) { _, _, _, _, _, _ in - fireCallbackCalled.fulfill() - } - - // Run test - pixelKit.fire(event, frequency: .unique) // Fired - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - timeMachine.travel(by: .day, value: 1) - timeMachine.travel(by: .hour, value: 2) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - timeMachine.travel(by: .hour, value: 10) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - timeMachine.travel(by: .day, value: 1) - pixelKit.fire(event, frequency: .unique) // Skipped (already fired) - - // Wait for expectations to be fulfilled - wait(for: [fireCallbackCalled], timeout: 0.5) - } - - func testVPNCohort() { - XCTAssertEqual(PixelKit.cohort(from: nil), "") - assertCohortEqual(.init(year: 2023, month: 1, day: 1), reportAs: "week-1") - assertCohortEqual(.init(year: 2024, month: 2, day: 24), reportAs: "week-60") - } - - private func assertCohortEqual(_ cohort: DateComponents, reportAs reportedCohort: String) { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - calendar.locale = Locale(identifier: "en_US_POSIX") - - let cohort = calendar.date(from: cohort) - let timeMachine = TimeMachine(calendar: calendar, date: cohort) - - PixelKit.setUp(appVersion: "test", - defaultHeaders: [:], - dailyPixelCalendar: calendar, - dateGenerator: timeMachine.now, - defaults: userDefaults()) { _, _, _, _, _, _ in } - - // 1st week - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 2nd week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 3rd week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 4th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 5th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 6th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 7th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) - - // 8th week - timeMachine.travel(by: .weekOfYear, value: 1) - XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), "") - } -} - -private class TimeMachine { - private var date: Date - private let calendar: Calendar - - init(calendar: Calendar? = nil, date: Date? = nil) { - self.calendar = calendar ?? { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - calendar.locale = Locale(identifier: "en_US_POSIX") - return calendar - }() - self.date = date ?? .init(timeIntervalSince1970: 0) - } - - func travel(by component: Calendar.Component, value: Int) { - date = calendar.date(byAdding: component, value: value, to: now())! - } - - func now() -> Date { - date - } -} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 9092bb8c50..31a96e104f 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: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ @@ -24,7 +24,11 @@ let package = Package( ], resources: [ .process("Resources") - ]), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), .testTarget( name: "SubscriptionUITests", dependencies: ["SubscriptionUI"]), From 5e230ad1b0c20f935c148e112903dbd0ae719f84 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 23 Apr 2024 05:13:14 +0000 Subject: [PATCH 006/134] Bump version to 1.85.0 (171) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 04fb8e2cc7..050c96774c 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 170 +CURRENT_PROJECT_VERSION = 171 From 3561c79d0ce0d10d62dc1d75959e5417d39775e3 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Tue, 23 Apr 2024 13:22:37 +0100 Subject: [PATCH 007/134] macOS: Bundle-Specfic Autofill Secure Vault Keychain Items (#2652) This work implements bundle-specific Keychain storage for our L1 keys, L2 keys and L2 encryption passwords. --- DuckDuckGo.xcodeproj/project.pbxproj | 18 ++--- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../Autofill/AutofillActionBuilder.swift | 2 +- .../ContentOverlayViewController.swift | 4 +- .../SecureVaultLoginImporter.swift | 2 +- DuckDuckGo/Fire/Model/Fire.swift | 2 +- .../Model/DataImportStatusProviding.swift | 2 +- DuckDuckGo/Menus/MainMenuActions.swift | 6 +- .../AutofillNeverPromptWebsitesManager.swift | 2 +- .../SecureVault/SecureVaultReporter.swift | 65 +++++++++++++++++++ .../PasswordManagementViewController.swift | 4 +- .../View/SaveCredentialsViewController.swift | 2 +- .../View/SaveIdentityViewController.swift | 2 +- .../SavePaymentMethodViewController.swift | 2 +- DuckDuckGo/Statistics/GeneralPixel.swift | 8 +++ DuckDuckGo/Sync/SyncCredentialsAdapter.swift | 4 +- .../TabExtensions/AutofillTabExtension.swift | 4 +- .../DataBrokerProtection/Package.swift | 2 +- .../DataBrokerProtectionDatabase.swift | 46 ++++++------- ...erProtectionSecureVaultErrorReporter.swift | 4 +- .../DataBrokerProtectionBrokerUpdater.swift | 2 +- .../DataBrokerProtectionProcessor.swift | 2 +- .../DataBrokerProtectionSecureVault.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Mocks/MockAutofillActionExecutor.swift | 2 +- ...ofillDeleteAllPasswordsExecutorTests.swift | 2 +- .../DataExport/CSVLoginExporterTests.swift | 2 +- UnitTests/DataExport/MockSecureVault.swift | 2 +- .../HomePage/DataImportProviderTests.swift | 2 +- .../AutofillPreferencesModelTests.swift | 2 +- 31 files changed, 141 insertions(+), 68 deletions(-) create mode 100644 DuckDuckGo/SecureVault/SecureVaultReporter.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0f516624ee..98daa3292d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -610,7 +610,7 @@ 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */; }; 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853014D525E671A000FB8205 /* PageObserverUserScript.swift */; }; - 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */; }; + 3706FC78293F65D500E42796 /* SecureVaultReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */; }; 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */; }; 3706FC7B293F65D500E42796 /* PasswordManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85625995269C953C00EE44BC /* PasswordManagementViewController.swift */; }; 3706FC7C293F65D500E42796 /* ImportedBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CFA26FE191E001E4761 /* ImportedBookmarks.swift */; }; @@ -2038,7 +2038,7 @@ B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0DF26AFE32F00A9DAD1 /* GeolocationProviderMock.swift */; }; B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0E226B3E7FA00A9DAD1 /* CLLocationManagerMock.swift */; }; B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */; }; - B642738227B65BAC0005DFD1 /* SecureVaultErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */; }; + B642738227B65BAC0005DFD1 /* SecureVaultReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */; }; B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */; }; B644B43D29D56829003FA9AB /* SearchNonexistentDomainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */; }; B644B43E29D5682B003FA9AB /* SearchNonexistentDomainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */; }; @@ -3679,7 +3679,7 @@ B63ED0DF26AFE32F00A9DAD1 /* GeolocationProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeolocationProviderMock.swift; sourceTree = ""; }; B63ED0E226B3E7FA00A9DAD1 /* CLLocationManagerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CLLocationManagerMock.swift; sourceTree = ""; }; B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingMenu.swift; sourceTree = ""; }; - B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureVaultErrorReporter.swift; sourceTree = ""; }; + B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureVaultReporter.swift; sourceTree = ""; }; B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWorkspaceExtension.swift; sourceTree = ""; }; B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNonexistentDomainTests.swift; sourceTree = ""; }; B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKProcessPoolExtension.swift; sourceTree = ""; }; @@ -5954,7 +5954,7 @@ 85CC1D7826A05E790062F04E /* Model */, 85CC1D7F26A05F6C0062F04E /* Services */, 85CC1D7926A05E820062F04E /* View */, - B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */, + B642738127B65BAC0005DFD1 /* SecureVaultReporter.swift */, ); path = SecureVault; sourceTree = ""; @@ -9987,7 +9987,7 @@ C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, - 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, + 3706FC78293F65D500E42796 /* SecureVaultReporter.swift in Sources */, 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */, 3706FEBD293F6EFF00E42796 /* BWCommand.swift in Sources */, 3706FC7B293F65D500E42796 /* PasswordManagementViewController.swift in Sources */, @@ -11305,7 +11305,7 @@ 1D2DC0072901679C008083A1 /* BWError.swift in Sources */, 853014D625E671A000FB8205 /* PageObserverUserScript.swift in Sources */, B677FC4F2B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, - B642738227B65BAC0005DFD1 /* SecureVaultErrorReporter.swift in Sources */, + B642738227B65BAC0005DFD1 /* SecureVaultReporter.swift in Sources */, 4B139AFD26B60BD800894F82 /* NSImageExtensions.swift in Sources */, B62B48392ADE46FC000DECE5 /* Application.swift in Sources */, 4B9DB02C2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */, @@ -12607,8 +12607,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 137.0.0; + kind = revision; + revision = c06709ba8a586f6a40190bacaaaaa96b2d55e540; }; }; 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { @@ -12616,7 +12616,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 138.1.0; + version = 139.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 9799888051..3696fa870b 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" : "4ec92e6b94076c53b9b2d34fdcbc2b52cecf9c8e", - "version" : "138.1.0" + "revision" : "1c2e84e6cd4543e9104aff753e48b146eeb36007", + "version" : "139.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo/Autofill/AutofillActionBuilder.swift b/DuckDuckGo/Autofill/AutofillActionBuilder.swift index de48f8d6b3..cbf46a19e2 100644 --- a/DuckDuckGo/Autofill/AutofillActionBuilder.swift +++ b/DuckDuckGo/Autofill/AutofillActionBuilder.swift @@ -35,7 +35,7 @@ extension AutofillActionBuilder { struct AutofillDeleteAllPasswordsBuilder: AutofillActionBuilder { @MainActor func buildExecutor() -> AutofillActionExecutor? { - guard let secureVault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + guard let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), let syncService = NSApp.delegateTyped.syncService else { return nil } return AutofillDeleteAllPasswordsExecutor(userAuthenticator: DeviceAuthenticator.shared, diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 9e325ef8b0..5bfdf2ca85 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -309,8 +309,8 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { } } - public func secureVaultInitFailed(_ error: SecureStorageError) { - SecureVaultErrorReporter.shared.secureVaultInitFailed(error) + public func secureVaultError(_ error: SecureStorageError) { + SecureVaultReporter.shared.secureVaultError(error) } public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { diff --git a/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift b/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift index 17acb874e0..193806cef0 100644 --- a/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift +++ b/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift @@ -23,7 +23,7 @@ import SecureStorage final class SecureVaultLoginImporter: LoginImporter { func importLogins(_ logins: [ImportedLoginCredential], progressCallback: @escaping (Int) throws -> Void) throws -> DataImport.DataTypeSummary { - let vault = try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) var successful: [String] = [] var duplicates: [String] = [] diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 29c2ca70f0..4766d8b6e6 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -385,7 +385,7 @@ final class Fire { // MARK: - Favicons private func autofillDomains() -> Set { - guard let vault = try? secureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + guard let vault = try? secureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), let accounts = try? vault.accounts() else { return [] } diff --git a/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift b/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift index 4d65b77c05..17efd1a64d 100644 --- a/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift +++ b/DuckDuckGo/HomePage/Model/DataImportStatusProviding.swift @@ -31,7 +31,7 @@ final class BookmarksAndPasswordsImportStatusProvider: DataImportStatusProviding let secureVault: (any AutofillSecureVault)? let bookmarkManager: BookmarkManager - init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { self.secureVault = secureVault self.bookmarkManager = bookmarkManager diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 5be8c2344c..59729584ac 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -215,7 +215,7 @@ extension AppDelegate { savePanel.beginSheetModal(for: window) { response in guard response == .OK, let selectedURL = savePanel.url else { return } - let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) let exporter = CSVLoginExporter(secureVault: vault!) do { try exporter.exportVaultLogins(to: selectedURL) @@ -670,7 +670,7 @@ extension MainViewController { } @objc func resetSecureVaultData(_ sender: Any?) { - let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) let accounts = (try? vault?.accounts()) ?? [] for accountID in accounts.compactMap(\.id) { @@ -1042,7 +1042,7 @@ extension AppDelegate: NSMenuItemValidation { } private var areTherePasswords: Bool { - let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) guard let vault else { return false } diff --git a/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift b/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift index 015f0606f3..0e69ffd6dc 100644 --- a/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift +++ b/DuckDuckGo/SecureVault/Model/AutofillNeverPromptWebsitesManager.swift @@ -28,7 +28,7 @@ final class AutofillNeverPromptWebsitesManager { private let secureVault: (any AutofillSecureVault)? - public init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared)) { + public init(secureVault: (any AutofillSecureVault)? = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared)) { self.secureVault = secureVault fetchNeverPromptWebsites() diff --git a/DuckDuckGo/SecureVault/SecureVaultReporter.swift b/DuckDuckGo/SecureVault/SecureVaultReporter.swift new file mode 100644 index 0000000000..af50ce3e84 --- /dev/null +++ b/DuckDuckGo/SecureVault/SecureVaultReporter.swift @@ -0,0 +1,65 @@ +// +// SecureVaultReporter.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import Foundation +import BrowserServicesKit +import PixelKit +import SecureStorage + +final class SecureVaultKeyStoreEventMapper: EventMapping { + public init() { + super.init { event, _, _, _ in + switch event { + case .l1KeyMigration: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultKeystoreEventL1KeyMigration)) + case .l2KeyMigration: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultKeystoreEventL2KeyMigration)) + case .l2KeyPasswordMigration: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultKeystoreEventL2KeyPasswordMigration)) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} + +final class SecureVaultReporter: SecureVaultReporting { + static let shared = SecureVaultReporter() + private var keyStoreMapper: SecureVaultKeyStoreEventMapper + private init(keyStoreMapper: SecureVaultKeyStoreEventMapper = SecureVaultKeyStoreEventMapper()) { + self.keyStoreMapper = keyStoreMapper + } + + func secureVaultError(_ error: SecureStorageError) { + guard NSApp.runType.requiresEnvironment else { return } + + switch error { + case .initFailed, .failedToOpenDatabase: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultInitError(error: error))) + default: + PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) + } + } + + func secureVaultKeyStoreEvent(_ event: SecureStorageKeyStoreEvent) { + keyStoreMapper.fire(event) + } +} diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index b5fd2cf4fc..f18a2478de 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -153,7 +153,7 @@ final class PasswordManagementViewController: NSViewController { } var secureVault: (any AutofillSecureVault)? { - try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) + try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) } private let passwordManagerCoordinator: PasswordManagerCoordinating = PasswordManagerCoordinator.shared @@ -1074,7 +1074,7 @@ extension PasswordManagementViewController: NSMenuItemValidation { } private var haveDuckDuckGoPasswords: Bool { - guard let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) else { return false } + guard let vault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) else { return false } let accounts = (try? vault.accounts()) ?? [] return !accounts.isEmpty } diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index fa4da02ff7..02a5595240 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -222,7 +222,7 @@ final class SaveCredentialsViewController: NSViewController { } } } else { - _ = try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeWebsiteCredentials(credentials) + _ = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeWebsiteCredentials(credentials) NSApp.delegateTyped.syncService?.scheduler.notifyDataChanged() os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") } diff --git a/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift b/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift index 88006e5096..61ce88b628 100644 --- a/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift @@ -71,7 +71,7 @@ final class SaveIdentityViewController: NSViewController { identity.title = UserText.pmDefaultIdentityAutofillTitle do { - try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeIdentity(identity) + 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) diff --git a/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift b/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift index 68275eeb5e..31c9d6a3b3 100644 --- a/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift +++ b/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift @@ -92,7 +92,7 @@ final class SavePaymentMethodViewController: NSViewController { paymentMethod.title = CreditCardValidation.type(for: paymentMethod.cardNumber).displayName do { - try AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeCreditCard(paymentMethod) + 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) PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 285a3bbdee..708d25fbe4 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -311,6 +311,10 @@ enum GeneralPixel: PixelKitEventV2 { // Tracks installation without tracking retention. case installationAttribution + case secureVaultKeystoreEventL1KeyMigration + case secureVaultKeystoreEventL2KeyMigration + case secureVaultKeystoreEventL2KeyPasswordMigration + var name: String { switch self { @@ -764,6 +768,10 @@ enum GeneralPixel: PixelKitEventV2 { // Installation Attribution case .installationAttribution: return "m_mac_install" + + case .secureVaultKeystoreEventL1KeyMigration: return "m_mac_secure_vault_keystore_event_l1-key-migration" + case .secureVaultKeystoreEventL2KeyMigration: return "m_mac_secure_vault_keystore_event_l2-key-migration" + case .secureVaultKeystoreEventL2KeyPasswordMigration: return "m_mac_secure_vault_keystore_event_l2-key-password-migration" } } diff --git a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift index 571fa21afb..c9f495901e 100644 --- a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift +++ b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift @@ -44,7 +44,7 @@ final class SyncCredentialsAdapter { syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() databaseCleaner = CredentialsDatabaseCleaner( secureVaultFactory: secureVaultFactory, - secureVaultErrorReporter: SecureVaultErrorReporter.shared, + secureVaultErrorReporter: SecureVaultReporter.shared, errorEvents: CredentialsCleanupErrorHandling(), log: .passwordManager ) @@ -71,7 +71,7 @@ final class SyncCredentialsAdapter { do { let provider = try CredentialsProvider( secureVaultFactory: secureVaultFactory, - secureVaultErrorReporter: SecureVaultErrorReporter.shared, + secureVaultErrorReporter: SecureVaultReporter.shared, metadataStore: metadataStore, metricsEvents: metricsEventsHandler, log: OSLog.sync, diff --git a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift index 867702bd7e..328b447ea3 100644 --- a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift @@ -163,8 +163,8 @@ extension AutofillTabExtension: SecureVaultManagerDelegate { } } - func secureVaultInitFailed(_ error: SecureStorageError) { - SecureVaultErrorReporter.shared.secureVaultInitFailed(error) + func secureVaultError(_ error: SecureStorageError) { + SecureVaultReporter.shared.secureVaultError(error) } public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index a397dcb586..f82dfc3ce0 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: "138.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "139.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index b32a2e42a1..bab6bce44f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -55,12 +55,12 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { private let fakeBrokerFlag: DataBrokerDebugFlag private let pixelHandler: EventMapping private let vault: (any DataBrokerProtectionSecureVault)? - private let secureVaultErrorReporter: SecureVaultErrorReporting? + private let secureVaultErrorReporter: SecureVaultReporting? init(fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker(), pixelHandler: EventMapping, vault: (any DataBrokerProtectionSecureVault)? = nil, - secureVaultErrorReporter: SecureVaultErrorReporting? = DataBrokerProtectionSecureVaultErrorReporter.shared) { + secureVaultErrorReporter: SecureVaultReporting? = DataBrokerProtectionSecureVaultErrorReporter.shared) { self.fakeBrokerFlag = fakeBrokerFlag self.pixelHandler = pixelHandler self.vault = vault @@ -69,7 +69,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func save(_ profile: DataBrokerProtectionProfile) async throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) if try vault.fetchProfile(with: Self.profileId) != nil { try await updateProfile(profile, vault: vault) @@ -85,7 +85,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { public func fetchProfile() throws -> DataBrokerProtectionProfile? { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -96,7 +96,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { public func deleteProfileData() throws { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.deleteProfileData() } catch { os_log("Database error: deleteProfileData, error: %{public}@", log: .error, error.localizedDescription) @@ -107,7 +107,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchChildBrokers(for parentBroker: String) throws -> [DataBroker] { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -118,7 +118,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func save(_ extractedProfile: ExtractedProfile, brokerId: Int64, profileQueryId: Int64) throws -> Int64 { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.save(extractedProfile: extractedProfile, brokerId: brokerId, profileQueryId: profileQueryId) } catch { @@ -130,7 +130,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) throws -> BrokerProfileQueryData? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let broker = try vault.fetchBroker(with: brokerId), let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { @@ -157,7 +157,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchExtractedProfiles(for brokerId: Int64) throws -> [ExtractedProfile] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -168,7 +168,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -179,7 +179,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updatePreferredRunDate( date, @@ -195,7 +195,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -206,7 +206,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updateLastRunDate( date, @@ -223,7 +223,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func updateRemovedDate(_ date: Date?, on extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -234,7 +234,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func add(_ historyEvent: HistoryEvent) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) if let extractedProfileId = historyEvent.extractedProfileId { try vault.save(historyEvent: historyEvent, brokerId: historyEvent.brokerId, profileQueryId: historyEvent.profileQueryId, extractedProfileId: extractedProfileId) @@ -250,7 +250,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchAllBrokerProfileQueryData() throws -> [BrokerProfileQueryData] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) let brokers = try vault.fetchAllBrokers() let profileQueries = try vault.fetchAllProfileQueries(for: Self.profileId) var brokerProfileQueryDataList = [BrokerProfileQueryData]() @@ -283,7 +283,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.save(brokerId: optOut.brokerId, profileQueryId: optOut.profileQueryId, @@ -299,7 +299,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchLastEvent(brokerId: Int64, profileQueryId: Int64) throws -> HistoryEvent? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) let events = try vault.fetchEvents(brokerId: brokerId, profileQueryId: profileQueryId) return events.max(by: { $0.date < $1.date }) @@ -312,7 +312,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func hasMatches() throws -> Bool { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -323,7 +323,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) guard let scan = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { return [HistoryEvent]() } @@ -338,7 +338,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) guard let optOut = try vault.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) else { return [HistoryEvent]() } @@ -353,7 +353,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchAttemptInformation(for extractedProfileId: Int64) throws -> AttemptInformation? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + 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) @@ -364,7 +364,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func addAttempt(extractedProfileId: Int64, attemptUUID: UUID, dataBroker: String, lastStageDate: Date, startTime: Date) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.save(extractedProfileId: extractedProfileId, attemptUUID: attemptUUID, dataBroker: dataBroker, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift index 21cb003862..86421908fd 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift @@ -22,7 +22,7 @@ import SecureStorage import PixelKit import Common -final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultErrorReporting { +final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultReporting { static let shared = DataBrokerProtectionSecureVaultErrorReporter(pixelHandler: DataBrokerProtectionPixelsHandler()) let pixelHandler: EventMapping @@ -30,7 +30,7 @@ final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultErrorReport self.pixelHandler = pixelHandler } - func secureVaultInitFailed(_ error: SecureStorageError) { + func secureVaultError(_ error: SecureStorageError) { switch error { case .initFailed, .failedToOpenDatabase: pixelHandler.fire(.secureVaultInitError(error: error)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 79020cde0b..9128f87178 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -118,7 +118,7 @@ public struct DataBrokerProtectionBrokerUpdater { } public static func provide() -> DataBrokerProtectionBrokerUpdater? { - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { return DataBrokerProtectionBrokerUpdater(vault: vault) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 0ba21a21f7..8b06083278 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -129,7 +129,7 @@ final class DataBrokerProtectionProcessor { completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { // Before running new operations we check if there is any updates to the broker files. - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) brokerUpdater.checkForUpdatesInBrokerJSONFiles() } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift index aa0e13a95e..c854055aab 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift @@ -26,7 +26,7 @@ typealias DataBrokerProtectionVaultFactory = SecureVaultFactory( makeCryptoProvider: { return DataBrokerProtectionCryptoProvider() - }, makeKeyStoreProvider: { + }, makeKeyStoreProvider: { _ in return DataBrokerProtectionKeyStoreProvider() }, makeDatabaseProvider: { key in return try DefaultDataBrokerProtectionDatabaseProvider(key: key) diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 3e81a2140b..baaeee8336 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "139.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 31a96e104f..41c971172f 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: "138.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "139.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift b/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift index bb7a4ec8bb..d94b13ccb0 100644 --- a/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift +++ b/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift @@ -27,7 +27,7 @@ final class MockAutofillActionBuilder: AutofillActionBuilder { var mockPresenter: MockAutofillActionPresenter? func buildExecutor() -> AutofillActionExecutor? { - guard let secureVault = try? MockSecureVaultFactory.makeVault(errorReporter: nil) else { return nil } + guard let secureVault = try? MockSecureVaultFactory.makeVault(reporter: nil) else { return nil } let syncService = MockDDGSyncing(authState: .inactive, scheduler: CapturingScheduler(), isSyncInProgress: false) let executor = MockAutofillActionExecutor(userAuthenticator: UserAuthenticatorMock(), secureVault: secureVault, syncService: syncService) self.mockExecutor = executor diff --git a/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift b/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift index f34960a14f..3b262f145d 100644 --- a/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift +++ b/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift @@ -30,7 +30,7 @@ final class AutofillDeleteAllPasswordsExecutorTests: XCTestCase { private var syncService: DDGSyncing! override func setUpWithError() throws { - secureVault = try MockSecureVaultFactory.makeVault(errorReporter: nil) + secureVault = try MockSecureVaultFactory.makeVault(reporter: nil) syncService = MockDDGSyncing(authState: .inactive, scheduler: scheduler, isSyncInProgress: false) sut = .init(userAuthenticator: mockAuthenticator, secureVault: secureVault, syncService: syncService) } diff --git a/UnitTests/DataExport/CSVLoginExporterTests.swift b/UnitTests/DataExport/CSVLoginExporterTests.swift index 20d244968e..bd3c079383 100644 --- a/UnitTests/DataExport/CSVLoginExporterTests.swift +++ b/UnitTests/DataExport/CSVLoginExporterTests.swift @@ -25,7 +25,7 @@ class CSVLoginExporterTests: XCTestCase { func testWhenExportingLogins_ThenLoginsArePersistedToDisk() throws { let mockFileStore = FileStoreMock() - let vault = try MockSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try MockSecureVaultFactory.makeVault(reporter: nil) vault.addWebsiteCredentials(identifiers: [1]) diff --git a/UnitTests/DataExport/MockSecureVault.swift b/UnitTests/DataExport/MockSecureVault.swift index c92a6f6189..d9d55a92fa 100644 --- a/UnitTests/DataExport/MockSecureVault.swift +++ b/UnitTests/DataExport/MockSecureVault.swift @@ -26,7 +26,7 @@ typealias MockVaultFactory = SecureVaultFactory( makeCryptoProvider: { return MockCryptoProvider() - }, makeKeyStoreProvider: { + }, makeKeyStoreProvider: { _ in let provider = MockKeyStoreProvider() provider._l1Key = "key".data(using: .utf8) return provider diff --git a/UnitTests/HomePage/DataImportProviderTests.swift b/UnitTests/HomePage/DataImportProviderTests.swift index 325f97e461..a67468dd72 100644 --- a/UnitTests/HomePage/DataImportProviderTests.swift +++ b/UnitTests/HomePage/DataImportProviderTests.swift @@ -55,7 +55,7 @@ final class DataImportProviderTests: XCTestCase { override func setUp() { UserDefaultsWrapper.clearAll() - vault = try! MockSecureVaultFactory.makeVault(errorReporter: nil) + vault = try! MockSecureVaultFactory.makeVault(reporter: nil) vault.storedAccounts = notImportedAccounts vault.storedIdentities = [] vault.storedCards = [] diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index 79341abe79..9851f65f72 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -49,7 +49,7 @@ final class UserAuthenticatorMock: UserAuthenticating { final class AutofillPreferencesModelTests: XCTestCase { func neverPromptWebsitesManager() throws -> AutofillNeverPromptWebsitesManager { - try AutofillNeverPromptWebsitesManager(secureVault: MockSecureVaultFactory.makeVault(errorReporter: nil)) + try AutofillNeverPromptWebsitesManager(secureVault: MockSecureVaultFactory.makeVault(reporter: nil)) } @MainActor From 6a76084f82593bf031946af4d7276c2944c74507 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 23 Apr 2024 09:29:03 -0300 Subject: [PATCH 008/134] DBP: Rename scanAllBrokers to startManualScan (#2679) --- .../DBP/DataBrokerProtectionDebugMenu.swift | 2 +- ...taBrokerProtectionLoginItemScheduler.swift | 6 +++--- .../IPCServiceManager.swift | 6 +++--- .../IPC/DataBrokerProtectionIPCClient.swift | 6 +++--- .../DataBrokerProtectionIPCScheduler.swift | 6 +++--- .../IPC/DataBrokerProtectionIPCServer.swift | 14 ++++++------- .../Model/DBPUIViewModel.swift | 2 +- .../DataBrokerProtectionNoOpScheduler.swift | 2 +- .../DataBrokerProtectionProcessor.swift | 10 +++++----- .../DataBrokerProtectionScheduler.swift | 20 +++++++++---------- 10 files changed, 37 insertions(+), 37 deletions(-) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 77d10f6eea..48a458f3cf 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -227,7 +227,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.scanAllBrokers(showWebView: showWebView) { errors in + DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView) { errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift index 8cd8416bc4..cc0d841ee8 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift @@ -55,10 +55,10 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler ipcScheduler.statusPublisher } - func scanAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + func startManualScan(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { enableLoginItem() - ipcScheduler.scanAllBrokers(showWebView: showWebView, completion: completion) + ipcScheduler.startManualScan(showWebView: showWebView, completion: completion) } func startScheduler(showWebView: Bool) { diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 01acff18bc..33a1fc1d77 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -84,10 +84,10 @@ extension IPCServiceManager: IPCServerInterface { } } - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { pixelHandler.fire(.ipcServerScanAllBrokersReceivedByAgent) - scheduler.scanAllBrokers(showWebView: showWebView) { errors in + scheduler.startManualScan(showWebView: showWebView) { errors in if let error = errors?.oneTimeError { switch error { case DataBrokerProtectionSchedulerError.operationsInterrupted: diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 7525572fd5..f7b809ac9f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -136,8 +136,8 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + public func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { self.pixelHandler.fire(.ipcServerScanAllBrokersCalledByApp) guard loginItemStatusChecker.doesHaveNecessaryPermissions() else { @@ -155,7 +155,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { } xpc.execute(call: { server in - server.scanAllBrokers(showWebView: showWebView) { errors in + server.startManualScan(showWebView: showWebView) { errors in if let error = errors?.oneTimeError { let nsError = error as NSError let interruptedError = DataBrokerProtectionSchedulerError.operationsInterrupted as NSError diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift index cfb11f5187..0763404514 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift @@ -51,10 +51,10 @@ public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionSchedul ipcClient.optOutAllBrokers(showWebView: showWebView, completion: completion) } - public func scanAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + public func startManualScan(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } - ipcClient.scanAllBrokers(showWebView: showWebView, completion: completion) + ipcClient.startManualScan(showWebView: showWebView, completion: completion) } public func runQueuedOperations(showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 9027e1d275..dce168b8fe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -94,8 +94,8 @@ public protocol IPCServerInterface: AnyObject { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runAllOperations(showWebView: Bool) @@ -135,8 +135,8 @@ protocol XPCServerInterface { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runAllOperations(showWebView: Bool) @@ -213,9 +213,9 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { serverDelegate?.optOutAllBrokers(showWebView: showWebView, completion: completion) } - func scanAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.scanAllBrokers(showWebView: showWebView, completion: completion) + func startManualScan(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + serverDelegate?.startManualScan(showWebView: showWebView, completion: completion) } func runQueuedOperations(showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index 3ca4eeb399..df3cf6fd82 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -75,7 +75,7 @@ final class DBPUIViewModel { extension DBPUIViewModel: DBPUIScanOps { func startScan() -> Bool { - scheduler.scanAllBrokers() + scheduler.startManualScan() return true } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift index 222377923b..cb3057b937 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift @@ -36,7 +36,7 @@ final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { func stopScheduler() { } func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runAllOperations(showWebView: Bool) { } func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 8b06083278..5bb320d3d1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -25,7 +25,7 @@ protocol OperationRunnerProvider { } private enum DataBrokerProtectionProcessorFunction { - case runAllScanOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + case startManualScans(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) case runAllOptOutOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) case runQueuedOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) case runAllOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) @@ -64,10 +64,10 @@ final class DataBrokerProtectionProcessor { } // MARK: - Public functions - func runAllScanOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + func startManualScans(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runAllScanOperations(pendingCompletion: completion) + currentlyRunningOperationsForFunction = .startManualScans(pendingCompletion: completion) runOperations(operationType: .scan, priorityDate: nil, showWebView: showWebView) { errors in @@ -202,7 +202,7 @@ final class DataBrokerProtectionProcessor { operationQueue.cancelAllOperations() switch currentlyRunningOperationsForFunction { - case .runAllScanOperations(let pendingCompletion), + case .startManualScans(let pendingCompletion), .runAllOptOutOperations(let pendingCompletion), .runQueuedOperations(let pendingCompletion), .runAllOperations(let pendingCompletion): diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index d5f9083d07..090f2d94f7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -80,7 +80,7 @@ public protocol DataBrokerProtectionScheduler { func stopScheduler() func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runAllOperations(showWebView: Bool) @@ -98,8 +98,8 @@ extension DataBrokerProtectionScheduler { runAllOperations(showWebView: false) } - public func scanAllBrokers() { - scanAllBrokers(showWebView: false, completion: nil) + public func startManualScan() { + startManualScan(showWebView: false, completion: nil) } } @@ -249,14 +249,14 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } - public func scanAllBrokers(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + public func startManualScan(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { stopScheduler() userNotificationService.requestNotificationPermission() os_log("Scanning all brokers...", log: .dataBrokerProtection) - dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] errors in + dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in guard let self = self else { return } self.startScheduler(showWebView: showWebView) @@ -272,15 +272,15 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch if let oneTimeError = errors.oneTimeError { switch oneTimeError { case DataBrokerProtectionSchedulerError.operationsInterrupted: - os_log("Interrupted during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) default: - os_log("Error during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.scanAllBrokers")) + os_log("Error during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startManualScan")) } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } From 442aa19b54fb0e20cbe7781c9449f56927a22a04 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Tue, 23 Apr 2024 15:03:25 +0100 Subject: [PATCH 009/134] Stop first scan completed notification being sent if there's an error (#2685) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207148631921399/f Tech Design URL: CC: **Description**: Stops the "first scan notification' being sent if there is an error. Since we've added a new interrupted error, it will get a lot more common if we don't fix it before releasing. We seem to have other issues relating to scan completion that this doesn't fix, **Steps to test this PR**: 1. Test that you can replicate the original issue on main by forcing a "oneTimeError" in scanAllBrokers and checking you see the notification 2. Test the same thing on this PR, you shouldn't see a notification 3. Test that when scans complete you do see a notification --- ###### 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) --- .../Scheduler/DataBrokerProtectionScheduler.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 090f2d94f7..e7a15bd1bb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -261,7 +261,9 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch self.startScheduler(showWebView: showWebView) - self.userNotificationService.sendFirstScanCompletedNotification() + if errors?.oneTimeError == nil { + self.userNotificationService.sendFirstScanCompletedNotification() + } if let hasMatches = try? self.dataManager.hasMatches(), hasMatches { From ac42cb8517391a97963caf049f3897b41a9e1623 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 23 Apr 2024 19:48:36 +0500 Subject: [PATCH 010/134] Add Untitled tab title (#2650) Task/Issue URL: https://app.asana.com/0/1204912272578138/1206900493125406/f --- DuckDuckGo/Common/Localizables/UserText.swift | 1 + DuckDuckGo/Localizable.xcstrings | 67 ++++++++++++++++++- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 5 +- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 9cb9493ebf..d7de7c47bf 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -212,6 +212,7 @@ struct UserText { static let addFolder = NSLocalizedString("menu.add.folder", value: "Add Folder…", comment: "Menu item to add a folder") static let tabHomeTitle = NSLocalizedString("tab.home.title", value: "New Tab", comment: "Tab home title") + static let tabUntitledTitle = NSLocalizedString("tab.empty.title", value: "Untitled", comment: "Title for an empty tab without a title") static let tabPreferencesTitle = NSLocalizedString("tab.preferences.title", value: "Settings", comment: "Tab preferences title") static let tabBookmarksTitle = NSLocalizedString("tab.bookmarks.title", value: "Bookmarks", comment: "Tab bookmarks title") static let tabOnboardingTitle = NSLocalizedString("tab.onboarding.title", value: "Welcome", comment: "Tab onboarding title") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index ef1f610c2e..993f125ffd 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -20060,6 +20060,7 @@ }, "Hide" : { "comment" : "Main Menu > View > Home Button > None item\n Preferences > Home Button > None item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48009,6 +48010,7 @@ }, "Show left of the back button" : { "comment" : "Preferences > Home Button > left position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48062,6 +48064,7 @@ }, "Show Left of the Back Button" : { "comment" : "Main Menu > View > Home Button > left position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48327,6 +48330,7 @@ }, "Show right of the reload button" : { "comment" : "Preferences > Home Button > right position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -48380,6 +48384,7 @@ }, "Show Right of the Reload Button" : { "comment" : "Main Menu > View > Home Button > right position item", + "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { @@ -49868,6 +49873,66 @@ } } }, + "tab.empty.title" : { + "comment" : "Title for an empty tab without a title", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohne Titel" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Untitled" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin título" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sans titre" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohne Titel" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naamloos" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bez tytułu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem título" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Без названия" + } + } + } + }, "tab.error.title" : { "comment" : "Tab error title", "extractionState" : "extracted_with_value", @@ -53180,4 +53245,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 1db6697d20..42ac6fcd45 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -290,7 +290,7 @@ final class TabViewModel { } private func updateTitle() { // swiftlint:disable:this cyclomatic_complexity - let title: String + var title: String switch tab.content { // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors case _ where isShowingErrorPage && (tab.error?.code != .webContentProcessTerminated || tab.title == nil): @@ -324,6 +324,9 @@ final class TabViewModel { title = addressBarString } } + if title.isEmpty { + title = UserText.tabUntitledTitle + } if self.title != title { self.title = title } From 2cd361fca433611919e78fa6bca0c22407fd6baa Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Tue, 23 Apr 2024 19:17:16 +0200 Subject: [PATCH 011/134] Bump BSK (#2688) Task/Issue URL: https://app.asana.com/0/1204099484721401/1207112724044306/f **Description**: Bumps BSK with iOS-related changes **Steps to test this PR**: 1. See iOS for test Steps https://github.com/duckduckgo/iOS/pull/2764/files --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../xcshareddata/xcschemes/sandbox-test-tool.xcscheme | 2 +- .../NetworkProtectionSubscriptionEventHandler.swift | 1 + LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 98daa3292d..dd280d3553 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12616,7 +12616,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 139.0.0; + version = 140.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 3696fa870b..feaf63045b 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" : "1c2e84e6cd4543e9104aff753e48b146eeb36007", - "version" : "139.0.0" + "revision" : "89442d067d2fcd77d487202b8d38be7e47ac0b5b", + "version" : "140.0.0" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index eb7e5e26bb..41730d7069 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> Date: Wed, 24 Apr 2024 00:31:56 +0100 Subject: [PATCH 012/134] Validate DBP permissions (#2673) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207136634464353/f **Description**: Display error messages if DBP agent prerequisites aren't met --- DuckDuckGo.xcodeproj/project.pbxproj | 32 +++ .../Contents.json | 0 .../DBP-Icon.imageset/Contents.json | 0 .../DBP-Icon.imageset/DBP-Icon.pdf | Bin .../Contents.json | 0 .../DBP-Information-Remover.svg | 0 .../dbp-error-info.imageset/Contents.json | 15 ++ .../dbp-error-info.pdf | Bin 0 -> 1128 bytes DuckDuckGo/Common/Localizables/UserText.swift | 10 + DuckDuckGo/DBP/DBPHomeViewController.swift | 208 ++++++++++-------- ...ataBrokerPrerequisitesStatusVerifier.swift | 50 +++++ .../DataBrokerProtectionPixelsHandler.swift | 118 ++++++++++ ...aBrokerProtectionErrorViewController.swift | 92 ++++++++ .../Pixels/DataBrokerProtectionPixels.swift | 27 +++ ...okerPrerequisitesStatusVerifierTests.swift | 73 ++++++ 15 files changed, 530 insertions(+), 95 deletions(-) rename DuckDuckGo/Assets.xcassets/Images/{DataBrokerProtectionWaitlist => DataBrokerProtection}/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Images/{DataBrokerProtectionWaitlist => DataBrokerProtection}/DBP-Icon.imageset/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Images/{DataBrokerProtectionWaitlist => DataBrokerProtection}/DBP-Icon.imageset/DBP-Icon.pdf (100%) rename DuckDuckGo/Assets.xcassets/Images/{DataBrokerProtectionWaitlist => DataBrokerProtection}/DBP-Information-Remover.imageset/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Images/{DataBrokerProtectionWaitlist => DataBrokerProtection}/DBP-Information-Remover.imageset/DBP-Information-Remover.svg (100%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf create mode 100644 DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift create mode 100644 DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift create mode 100644 DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift create mode 100644 UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 82841761e4..0adc19521d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -166,6 +166,12 @@ 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */; }; 3168506D2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; 3168506E2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; + 316913232BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */; }; + 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */; }; + 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; }; + 316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; }; + 316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; }; + 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; }; 3171D6B82889849F0068632A /* CookieManagedNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */; }; 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B9288984D00068632A /* BadgeAnimationView.swift */; }; 3171D6DB2889B64D0068632A /* CookieManagedNotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */; }; @@ -186,6 +192,8 @@ 31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */; }; 31CF3432288B0B1B0087244B /* NavigationBarBadgeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */; }; 31D5375C291D944100407A95 /* PasswordManagementBitwardenItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D5375B291D944100407A95 /* PasswordManagementBitwardenItemView.swift */; }; + 31DC2F222BD6DE6C001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */; }; + 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */; }; 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */; }; 31E163BD293A579E00963C10 /* PrivacyReferenceTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */; }; 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; @@ -2805,6 +2813,9 @@ 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubePlayerNavigationHandler.swift; sourceTree = ""; }; 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewControllerPresenter.swift; sourceTree = ""; }; 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionDebugMenu.swift; sourceTree = ""; }; + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionPixelsHandler.swift; sourceTree = ""; }; + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifier.swift; sourceTree = ""; }; + 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionErrorViewController.swift; sourceTree = ""; }; 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationView.swift; sourceTree = ""; }; 3171D6B9288984D00068632A /* BadgeAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeAnimationView.swift; sourceTree = ""; }; 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationContainerView.swift; sourceTree = ""; }; @@ -2824,6 +2835,7 @@ 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistFeatureSetupHandler.swift; sourceTree = ""; }; 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarBadgeAnimator.swift; sourceTree = ""; }; 31D5375B291D944100407A95 /* PasswordManagementBitwardenItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordManagementBitwardenItemView.swift; sourceTree = ""; }; + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifierTests.swift; sourceTree = ""; }; 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSiteReportingReferenceTests.swift; sourceTree = ""; }; 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = ""; }; 31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; @@ -4439,6 +4451,14 @@ path = Subscription; sourceTree = ""; }; + 3169132B2BD2C7960051B46D /* ErrorView */ = { + isa = PBXGroup; + children = ( + 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */, + ); + path = ErrorView; + sourceTree = ""; + }; 3171D6DC2889B6700068632A /* CookieManaged */ = { isa = PBXGroup; children = ( @@ -4461,6 +4481,8 @@ 3192EC862A4DCF0E001E97A5 /* DBP */ = { isa = PBXGroup; children = ( + 3169132B2BD2C7960051B46D /* ErrorView */, + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */, 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, @@ -4473,6 +4495,7 @@ BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */, 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */, + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */, ); path = DBP; sourceTree = ""; @@ -4481,6 +4504,7 @@ isa = PBXGroup; children = ( 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */, + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, ); path = DBP; sourceTree = ""; @@ -9684,6 +9708,7 @@ B66260E129AC6EBD00E9E3EE /* HistoryTabExtension.swift in Sources */, 3706FB8E293F65D500E42796 /* FirefoxEncryptionKeyReader.swift in Sources */, 3706FB8F293F65D500E42796 /* BookmarkManagementSplitViewController.swift in Sources */, + 316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 3706FB90293F65D500E42796 /* CookieManagedNotificationContainerView.swift in Sources */, 3706FB91293F65D500E42796 /* FileManagerExtension.swift in Sources */, 3706FB92293F65D500E42796 /* PermissionModel.swift in Sources */, @@ -9718,6 +9743,7 @@ 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, + 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, @@ -10020,6 +10046,7 @@ 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, + 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, @@ -10305,6 +10332,7 @@ 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, + 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, 9F3344632BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, @@ -10659,6 +10687,7 @@ AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, 1D2DC00629016798008083A1 /* BWCredential.swift in Sources */, EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, + 316913232BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, 37AFCE8727DA334800471A10 /* PreferencesRootView.swift in Sources */, B684590825C9027900DC17B6 /* AppStateChangedPublisher.swift in Sources */, 4B92928F26670D1700AD2C21 /* BookmarkTableCellView.swift in Sources */, @@ -10797,6 +10826,7 @@ 4B379C2227BDBA29008A968E /* LocalAuthenticationService.swift in Sources */, 37CEFCA92A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */, 4BB99D0326FE191E001E4761 /* SafariBookmarksReader.swift in Sources */, + 316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DA6D0FD2A1FF9A100540406 /* HTTPCookie.swift in Sources */, AACF6FD626BC366D00CF09F9 /* SafariVersionReader.swift in Sources */, 4BE65485271FCD7B008D1D63 /* LoginFaviconView.swift in Sources */, @@ -11077,6 +11107,7 @@ 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */, AA6AD95B2704B6DB00159F8A /* FirePopoverViewController.swift in Sources */, 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */, @@ -11537,6 +11568,7 @@ 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */, 566B195D29CDB692007E38F4 /* MoreOptionsMenuTests.swift in Sources */, AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */, + 31DC2F222BD6DE6C001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, B69B50452726C5C200758A2B /* AtbParserTests.swift in Sources */, 1D8C2FED2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/DBP-Icon.pdf b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/DBP-Icon.pdf similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/DBP-Icon.pdf rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/DBP-Icon.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/DBP-Information-Remover.svg b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/DBP-Information-Remover.svg similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/DBP-Information-Remover.svg rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/DBP-Information-Remover.svg diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json new file mode 100644 index 0000000000..9869fe984d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "dbp-error-info.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c9486753e58a3af9f18ee9f0049ae4025ab7e564 GIT binary patch literal 1128 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f^liEcb=%Bz@6VB#f{H*?0b}!f+2m~Vf|(iTK5B2UpAz;|A|T6cGKkNT7?^HChE?)KULdovdI=EU00>>l2*5)g^d!=G`8927sSlH)AXQo z*EX|5!RK5;#B&riJrY~aYMt2`+t9Q4Vnq9Q<%o9X0+*W`th2ghr7#w()jU2!d23}v01{~J!?JbGHX{J_fH$2a7P)ZS^BUbsH_AOBXDH+`UF z0gYE!0s+N1B;lBt8N!o|30xGIob(;@^72cH6{2IIi6x}6AXPy>ATb@5dVEt;G83Kh zD-@y?4D<}Z0KqUafe9g4Fq48(i}Op1l2eNnKnWO>@<55vIX|x?H4kVbDD}bw0}_jX zVhW~EA*3`85`w3FeecW^pi>n{Qe=H?h0Ele!ns!ECyGjmdlxInSz=>iN6jpF>=R80kq%oI&Xpy&tX=a(pe gJPZzC{ou^1RG{m@89%Y81neD419L7_RabvE04{HW=>Px# literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 9cb9493ebf..85eb68cad2 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1138,4 +1138,14 @@ struct UserText { // Key: "subscription.progress.view.completing.purchase" // Comment: "Progress view title when completing the purchase" static let completingPurchaseTitle = "Completing purchase..." + + // MARK: - DBP Error pages + + static let dbpErrorPageBadPathTitle = "Move DuckDuckGo App to Applications" + static let dbpErrorPageBadPathMessage = "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you." + static let dbpErrorPageBadPathCTA = "Move App for Me..." + + static let dbpErrorPageNoPermissionTitle = "Change System Setting" + static let dbpErrorPageNoPermissionMessage = "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background." + static let dbpErrorPageNoPermissionCTA = "Open System Settings..." } diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 3e5c8ce916..55d08d7d62 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -34,8 +34,15 @@ final class DBPHomeViewController: NSViewController { private var presentedWindowController: NSWindowController? private let dataBrokerProtectionManager: DataBrokerProtectionManager private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + private var currentChildViewController: NSViewController? + private var observer: NSObjectProtocol? - lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { + private let prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier + private lazy var errorViewController: DataBrokerProtectionErrorViewController = { + DataBrokerProtectionErrorViewController() + }() + + private lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { let privacyConfigurationManager = PrivacyFeatures.contentBlocking.privacyConfigurationManager let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, @@ -64,8 +71,9 @@ final class DBPHomeViewController: NSViewController { }) }() - init(dataBrokerProtectionManager: DataBrokerProtectionManager) { + init(dataBrokerProtectionManager: DataBrokerProtectionManager, prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier = DefaultDataBrokerPrerequisitesStatusVerifier()) { self.dataBrokerProtectionManager = dataBrokerProtectionManager + self.prerequisiteVerifier = prerequisiteVerifier super.init(nibName: nil, bundle: nil) } @@ -80,9 +88,8 @@ final class DBPHomeViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() - if !dataBrokerProtectionManager.shouldAskForInviteCode() { - attachDataBrokerContainerView() - } + setupUI() + setupObserver() do { if try dataBrokerProtectionManager.dataManager.fetchProfile() != nil { @@ -95,15 +102,10 @@ final class DBPHomeViewController: NSViewController { } } - private func attachDataBrokerContainerView() { - addChild(dataBrokerProtectionViewController) - view.addSubview(dataBrokerProtectionViewController.view) - } - override func viewDidAppear() { super.viewDidAppear() - if dataBrokerProtectionManager.shouldAskForInviteCode() { + if shouldAskForInviteCode() { presentInviteCodeFlow() } } @@ -111,6 +113,19 @@ final class DBPHomeViewController: NSViewController { override func viewDidLayout() { super.viewDidLayout() dataBrokerProtectionViewController.view.frame = view.bounds + errorViewController.view.frame = view.bounds + } + + private func setupUI() { + if !shouldAskForInviteCode() { + setupUIWithCurrentStatus() + } + } + + private func setupObserver() { + observer = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in + self?.setupUI() + } } private func presentInviteCodeFlow() { @@ -128,13 +143,54 @@ final class DBPHomeViewController: NSViewController { } parentWindowController.window?.beginSheet(newWindow) } + + private func setupUIWithCurrentStatus() { + setupUIWithStatus(prerequisiteVerifier.checkStatus()) + } + + private func setupUIWithStatus(_ status: DataBrokerPrerequisitesStatus) { + switch status { + case .invalidDirectory: + displayWrongDirectoryErrorUI() + pixelHandler.fire(.homeViewShowBadPathError) + case .invalidSystemPermission: + displayWrongPermissionsErrorUI() + pixelHandler.fire(.homeViewShowNoPermissionError) + case .valid: + displayDBPUI() + pixelHandler.fire(.homeViewShowWebUI) + } + } + + private func shouldAskForInviteCode() -> Bool { + prerequisiteVerifier.checkStatus() == .valid && dataBrokerProtectionManager.shouldAskForInviteCode() + } + + private func displayDBPUI() { + replaceChildController(dataBrokerProtectionViewController) + } + + private func replaceChildController(_ childViewController: NSViewController) { + if let child = currentChildViewController { + child.removeCompletely() + } + + addAndLayoutChild(childViewController) + self.currentChildViewController = childViewController + } + + deinit { + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } + } } extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDelegate { func dataBrokerProtectionInviteDialogsViewModelDidReedemSuccessfully(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { presentedWindowController?.window?.close() presentedWindowController = nil - attachDataBrokerContainerView() + setupUIWithCurrentStatus() } func dataBrokerProtectionInviteDialogsViewModelDidCancel(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { @@ -144,92 +200,54 @@ extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDeleg } } -public class DataBrokerProtectionPixelsHandler: EventMapping { - - // swiftlint:disable:next function_body_length - public init() { - super.init { event, _, _, _ in - switch event { - case .error(let error, _): - PixelKit.fire(DebugEvent(event, error: error)) - case .generalError(let error, _), - .secureVaultInitError(let error), - .secureVaultError(let error): - PixelKit.fire(DebugEvent(event, error: error)) - case .ipcServerStartSchedulerXPCError(error: let error), - .ipcServerStopSchedulerXPCError(error: let error), - .ipcServerScanAllBrokersXPCError(error: let error), - .ipcServerScanAllBrokersCompletedOnAgentWithError(error: let error), - .ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: let error), - .ipcServerOptOutAllBrokersCompletion(error: let error), - .ipcServerRunQueuedOperationsCompletion(error: let error): - PixelKit.fire(DebugEvent(event, error: error), frequency: .dailyAndCount, includeAppVersionParameter: true) - case .ipcServerStartSchedulerCalledByApp, - .ipcServerStartSchedulerReceivedByAgent, - .ipcServerStopSchedulerCalledByApp, - .ipcServerStopSchedulerReceivedByAgent, - .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, - .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, - .ipcServerScanAllBrokersCalledByApp, - .ipcServerScanAllBrokersReceivedByAgent, - .ipcServerScanAllBrokersCompletedOnAgentWithoutError, - .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, - .ipcServerScanAllBrokersInterruptedOnAgent, - .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: - PixelKit.fire(event, frequency: .dailyAndCount, includeAppVersionParameter: true) - case .parentChildMatches, - .optOutStart, - .optOutEmailGenerate, - .optOutCaptchaParse, - .optOutCaptchaSend, - .optOutCaptchaSolve, - .optOutSubmit, - .optOutEmailReceive, - .optOutEmailConfirm, - .optOutValidate, - .optOutFinish, - .optOutSubmitSuccess, - .optOutFillForm, - .optOutSuccess, - .optOutFailure, - .backgroundAgentStarted, - .backgroundAgentRunOperationsAndStartSchedulerIfPossible, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, - .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, - .ipcServerOptOutAllBrokers, - .ipcServerRunQueuedOperations, - .ipcServerRunAllOperations, - .scanSuccess, - .scanFailed, - .scanError, - .dataBrokerProtectionNotificationSentFirstScanComplete, - .dataBrokerProtectionNotificationOpenedFirstScanComplete, - .dataBrokerProtectionNotificationSentFirstRemoval, - .dataBrokerProtectionNotificationOpenedFirstRemoval, - .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, - .dataBrokerProtectionNotificationOpened2WeeksCheckIn, - .dataBrokerProtectionNotificationSentAllRecordsRemoved, - .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, - .dailyActiveUser, - .weeklyActiveUser, - .monthlyActiveUser, - .weeklyReportScanning, - .weeklyReportRemovals, - .scanningEventNewMatch, - .scanningEventReAppearance, - .webUILoadingFailed, - .webUILoadingStarted, - .webUILoadingSuccess, - .emptyAccessTokenDaily, - .generateEmailHTTPErrorDaily: - PixelKit.fire(event) - } +// MARK: - Error UI + +extension DBPHomeViewController { + private func displayWrongDirectoryErrorUI() { + let errorViewModel = DataBrokerProtectionErrorViewModel(title: UserText.dbpErrorPageBadPathTitle, + message: UserText.dbpErrorPageBadPathMessage, + ctaText: UserText.dbpErrorPageBadPathCTA, + ctaAction: { [weak self] in + self?.moveToApplicationFolder() + }) + + errorViewController.errorViewModel = errorViewModel + replaceChildController(errorViewController) + } + + private func displayWrongPermissionsErrorUI() { + let errorViewModel = DataBrokerProtectionErrorViewModel(title: UserText.dbpErrorPageNoPermissionTitle, + message: UserText.dbpErrorPageNoPermissionMessage, + ctaText: UserText.dbpErrorPageNoPermissionCTA, + ctaAction: { [weak self] in + self?.openLoginItemSettings() + }) + + errorViewController.errorViewModel = errorViewModel + replaceChildController(errorViewController) + } +} + +// MARK: - System configuration + +import ServiceManagement + +extension DBPHomeViewController { + func openLoginItemSettings() { + pixelHandler.fire(.homeViewCTAGrantPermissionClicked) + if #available(macOS 13.0, *) { + SMAppService.openSystemSettingsLoginItems() + } else { + let loginItemsURL = URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension")! + NSWorkspace.shared.open(loginItemsURL) } } - override init(mapping: @escaping EventMapping.Mapping) { - fatalError("Use init()") + func moveToApplicationFolder() { + pixelHandler.fire(.homeViewCTAMoveApplicationClicked) + Task { @MainActor in + await AppLauncher(appBundleURL: Bundle.main.bundleURL).launchApp(withCommand: .moveAppToApplications) + } } } diff --git a/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift new file mode 100644 index 0000000000..f5f5c4d091 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift @@ -0,0 +1,50 @@ +// +// DataBrokerPrerequisitesStatusVerifier.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 Combine +import DataBrokerProtection +import LoginItems + +enum DataBrokerPrerequisitesStatus { + case invalidDirectory + case invalidSystemPermission + case valid +} + +protocol DataBrokerPrerequisitesStatusVerifier: AnyObject { + func checkStatus() -> DataBrokerPrerequisitesStatus +} + +final class DefaultDataBrokerPrerequisitesStatusVerifier: DataBrokerPrerequisitesStatusVerifier { + private let statusChecker: DBPLoginItemStatusChecker + + init(statusChecker: DBPLoginItemStatusChecker = LoginItem.dbpBackgroundAgent) { + self.statusChecker = statusChecker + } + + func checkStatus() -> DataBrokerPrerequisitesStatus { + if !statusChecker.doesHaveNecessaryPermissions() { + return .invalidSystemPermission + } else if !statusChecker.isInCorrectDirectory() { + return .invalidDirectory + } else { + return .valid + } + } +} diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift new file mode 100644 index 0000000000..08c5b74f18 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -0,0 +1,118 @@ +// +// DataBrokerProtectionPixelsHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import DataBrokerProtection +import PixelKit +import Common + +public class DataBrokerProtectionPixelsHandler: EventMapping { + + // swiftlint:disable:next function_body_length + public init() { + super.init { event, _, _, _ in + switch event { + case .error(let error, _): + PixelKit.fire(DebugEvent(event, error: error)) + case .generalError(let error, _), + .secureVaultInitError(let error), + .secureVaultError(let error): + PixelKit.fire(DebugEvent(event, error: error)) + case .ipcServerStartSchedulerXPCError(error: let error), + .ipcServerStopSchedulerXPCError(error: let error), + .ipcServerScanAllBrokersXPCError(error: let error), + .ipcServerScanAllBrokersCompletedOnAgentWithError(error: let error), + .ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: let error), + .ipcServerOptOutAllBrokersCompletion(error: let error), + .ipcServerRunQueuedOperationsCompletion(error: let error): + PixelKit.fire(DebugEvent(event, error: error), frequency: .dailyAndCount, includeAppVersionParameter: true) + case .ipcServerStartSchedulerCalledByApp, + .ipcServerStartSchedulerReceivedByAgent, + .ipcServerStopSchedulerCalledByApp, + .ipcServerStopSchedulerReceivedByAgent, + .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, + .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, + .ipcServerScanAllBrokersCalledByApp, + .ipcServerScanAllBrokersReceivedByAgent, + .ipcServerScanAllBrokersCompletedOnAgentWithoutError, + .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, + .ipcServerScanAllBrokersInterruptedOnAgent, + .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: + PixelKit.fire(event, frequency: .dailyAndCount, includeAppVersionParameter: true) + case .parentChildMatches, + .optOutStart, + .optOutEmailGenerate, + .optOutCaptchaParse, + .optOutCaptchaSend, + .optOutCaptchaSolve, + .optOutSubmit, + .optOutEmailReceive, + .optOutEmailConfirm, + .optOutValidate, + .optOutFinish, + .optOutSubmitSuccess, + .optOutFillForm, + .optOutSuccess, + .optOutFailure, + .backgroundAgentStarted, + .backgroundAgentRunOperationsAndStartSchedulerIfPossible, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, + .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, + .ipcServerOptOutAllBrokers, + .ipcServerRunQueuedOperations, + .ipcServerRunAllOperations, + .scanSuccess, + .scanFailed, + .scanError, + .dataBrokerProtectionNotificationSentFirstScanComplete, + .dataBrokerProtectionNotificationOpenedFirstScanComplete, + .dataBrokerProtectionNotificationSentFirstRemoval, + .dataBrokerProtectionNotificationOpenedFirstRemoval, + .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, + .dataBrokerProtectionNotificationOpened2WeeksCheckIn, + .dataBrokerProtectionNotificationSentAllRecordsRemoved, + .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, + .dailyActiveUser, + .weeklyActiveUser, + .monthlyActiveUser, + .weeklyReportScanning, + .weeklyReportRemovals, + .scanningEventNewMatch, + .scanningEventReAppearance, + .webUILoadingFailed, + .webUILoadingStarted, + .webUILoadingSuccess, + .emptyAccessTokenDaily, + .generateEmailHTTPErrorDaily: + PixelKit.fire(event) + + case .homeViewShowNoPermissionError, + .homeViewShowWebUI, + .homeViewShowBadPathError, + .homeViewCTAMoveApplicationClicked, + .homeViewCTAGrantPermissionClicked: + PixelKit.fire(event, frequency: .dailyAndCount) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} diff --git a/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift b/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift new file mode 100644 index 0000000000..fa628b686e --- /dev/null +++ b/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift @@ -0,0 +1,92 @@ +// +// DataBrokerProtectionErrorViewController.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +final class DataBrokerProtectionErrorViewController: NSViewController { + private var errorSubview: NSView? + + var errorViewModel: DataBrokerProtectionErrorViewModel? { + didSet { + guard let errorViewModel = errorViewModel else { return } + + errorSubview?.removeFromSuperview() + + let errorView = DataBrokerProtectionErrorView(viewModel: errorViewModel) + errorSubview = NSHostingView(rootView: errorView) + + if let errorSubview = errorSubview { + view.addAndLayout(errorSubview) + } + } + } +} + +struct DataBrokerProtectionErrorView: View { + var viewModel: DataBrokerProtectionErrorViewModel + + var body: some View { + VStack(alignment: .center, spacing: 16) { + + HStack { + Image("DaxLockScreenLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) + + Text("Privacy Pro") + .font(.title) + .fontWeight(.light) + } + .padding(.bottom, 25) + + HStack { + Image("dbp-error-info") + .resizable() + .frame(width: 24, height: 24) + + Text(viewModel.title) + .font(.title) + .fontWeight(.light) + } + + Text(viewModel.message) + .font(.body) + .fontWeight(.light) + .multilineTextAlignment(.center) + .padding(.bottom, 10) + + Button(action: { + viewModel.ctaAction() + }) { + Text(viewModel.ctaText) + } + + Spacer() + }.padding() + .frame(maxWidth: 500) + } +} + +struct DataBrokerProtectionErrorViewModel { + let title: String + let message: String + let ctaText: String + let ctaAction: () -> Void +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 075f405dc5..d9de1179a7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -161,6 +161,13 @@ public enum DataBrokerProtectionPixels { // Backend service errors case generateEmailHTTPErrorDaily(statusCode: Int, environment: String, wasOnWaitlist: Bool) case emptyAccessTokenDaily(environment: String, wasOnWaitlist: Bool, callSite: BackendServiceCallSite) + + // Home View + case homeViewShowNoPermissionError + case homeViewShowWebUI + case homeViewShowBadPathError + case homeViewCTAMoveApplicationClicked + case homeViewCTAGrantPermissionClicked } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -265,6 +272,13 @@ extension DataBrokerProtectionPixels: PixelKitEvent { // Backend service errors case .generateEmailHTTPErrorDaily: return "m_mac_dbp_service_email-generate-http-error" case .emptyAccessTokenDaily: return "m_mac_dbp_service_empty-auth-token" + + // Home View + case .homeViewShowNoPermissionError: return "m_mac_dbp_home_view_show-no-permission-error" + case .homeViewShowWebUI: return "m_mac_dbp_home_view_show-web-ui" + case .homeViewShowBadPathError: return "m_mac_dbp_home_view_show-bad-path-error" + case .homeViewCTAMoveApplicationClicked: return "m_mac_dbp_home_view-cta-move-application-clicked" + case .homeViewCTAGrantPermissionClicked: return "m_mac_dbp_home_view-cta-grant-permission-clicked" } } @@ -357,6 +371,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .scanningEventNewMatch, .scanningEventReAppearance, + .homeViewShowNoPermissionError, + .homeViewShowWebUI, + .homeViewShowBadPathError, + .homeViewCTAMoveApplicationClicked, + .homeViewCTAGrantPermissionClicked, .secureVaultInitError, .secureVaultError: @@ -486,6 +505,14 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Bool { + return doesHavePermissionValue + } + func isInCorrectDirectory() -> Bool { + return isInCorrectDirectoryValue + } + + func reset() { + doesHavePermissionValue = true + isInCorrectDirectoryValue = true + } +} From bc1880ee2a8927dd99d2815c0ddc6fb0f30c23c5 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 24 Apr 2024 05:13:39 +0000 Subject: [PATCH 013/134] Bump version to 1.85.0 (172) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 050c96774c..59f3d0e6e2 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 171 +CURRENT_PROJECT_VERSION = 172 From 99408c1603e1eb984551fe2e05016be4f6599d9a Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 24 Apr 2024 11:43:27 +0100 Subject: [PATCH 014/134] Fix DBP interrupt function not being cleared on succesful completion (#2690) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207152863082294/f Tech Design URL: CC: **Description**: Straightforward, as title **Steps to test this PR**: 1. test DBP still works 2. Test the various pixels still work as expected. Test the interrupt pixel is called only when a function is actually interrupted 3. Test that an interrupt pixel and a completed successfully pixel are never fired at the same time --- ###### 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) --- .../Scheduler/DataBrokerProtectionProcessor.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 5bb320d3d1..4e0cf5ac7d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -70,10 +70,11 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .startManualScans(pendingCompletion: completion) runOperations(operationType: .scan, priorityDate: nil, - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Scans done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) - self.calculateMisMatches() + self?.calculateMisMatches() } } @@ -88,8 +89,9 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runAllOptOutOperations(pendingCompletion: completion) runOperations(operationType: .optOut, priorityDate: nil, - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Optouts done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } @@ -100,8 +102,9 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runQueuedOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: Date(), - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Queued operations done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } @@ -112,8 +115,9 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runAllOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: nil, - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Queued operations done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } From 4441fbab466c3f9edf78ee71ebae410f783e0752 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 24 Apr 2024 22:30:29 +1000 Subject: [PATCH 015/134] Bookmark All Tabs (#2683) Task/Issue URL: https://app.asana.com/0/72649045549333/1206339769350174/f Description: This PR adds the capability to bookmark all tabs in one step. --- DuckDuckGo.xcodeproj/project.pbxproj | 100 +++++ .../Bookmarks/Model/BookmarkManager.swift | 13 +- DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift | 8 +- .../Bookmarks/Services/BookmarkStore.swift | 3 +- .../Services/BookmarkStoreMock.swift | 19 + .../Services/LocalBookmarkStore.swift | 87 +++- .../UserDefaultsBookmarkFoldersStore.swift | 48 +++ .../View/AddBookmarkFolderPopoverView.swift | 1 - .../View/AddBookmarkPopoverView.swift | 1 - .../Dialog/AddEditBookmarkDialogView.swift | 2 +- .../Dialog/BookmarkAllTabsDialogView.swift | 141 +++++++ .../Dialog/BookmarkDialogContainerView.swift | 6 +- .../Dialog/BookmarksDialogViewFactory.swift | 12 + ...AddEditBookmarkFolderDialogViewModel.swift | 23 +- ...arkAllTabsDialogCoordinatorViewModel.swift | 74 ++++ .../BookmarkAllTabsDialogViewModel.swift | 127 ++++++ DuckDuckGo/Common/Localizables/UserText.swift | 12 +- DuckDuckGo/Localizable.xcstrings | 378 ++++++++++++++++- DuckDuckGo/Menus/MainMenu.swift | 9 + DuckDuckGo/Menus/MainMenuActions.swift | 15 + .../NavigationBar/View/MoreOptionsMenu.swift | 11 + .../View/NavigationBarViewController.swift | 5 + DuckDuckGo/Statistics/GeneralPixel.swift | 1 - .../TabBar/View/TabBarViewController.swift | 9 + DuckDuckGo/TabBar/View/TabBarViewItem.swift | 27 +- .../ViewModel/TabCollectionViewModel.swift | 11 + .../PromptActionView/PromptActionView.swift | 1 + .../MultilineTextHeightFixer.swift | 2 +- .../Helpers/WebsiteInfoHelpers.swift | 35 ++ .../Model/LocalBookmarkManagerTests.swift | 92 +++++ .../Bookmarks/Model/WebsiteInfoTests.swift | 77 ++++ .../Services/BookmarkFolderStoreMock.swift | 25 ++ .../Services/LocalBookmarkStoreTests.swift | 185 +++++++++ ...serDefaultsBookmarkFoldersStoreTests.swift | 62 +++ ...kmarkDialogCoordinatorViewModelTests.swift | 68 ++-- .../AddEditBookmarkDialogViewModelTests.swift | 44 +- ...itBookmarkFolderDialogViewModelTests.swift | 4 +- ...lTabsDialogCoordinatorViewModelTests.swift | 171 ++++++++ .../BookmarkAllTabsDialogViewModelTests.swift | 382 ++++++++++++++++++ .../AddEditBookmarkDialogViewModelMock.swift | 43 ++ ...ditBookmarkFolderDialogViewModelMock.swift | 40 ++ .../BookmarkAllTabsDialogViewModelMock.swift | 43 ++ .../HomePage/Mocks/MockBookmarkManager.swift | 6 + UnitTests/Menus/MainMenuTests.swift | 23 ++ .../CapturingOptionsButtonMenuDelegate.swift | 5 + .../MoreOptionsMenu+BookmarksTests.swift | 61 +++ UnitTests/Menus/MoreOptionsMenuTests.swift | 17 + .../TabBar/View/MockTabViewItemDelegate.swift | 11 + .../TabBar/View/TabBarViewItemTests.swift | 64 ++- .../TabCollectionViewModelTests.swift | 73 ++++ 50 files changed, 2580 insertions(+), 97 deletions(-) create mode 100644 DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift rename LocalPackages/{NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI => SwiftUIExtensions/Sources/SwiftUIExtensions}/MultilineTextHeightFixer.swift (98%) create mode 100644 UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift create mode 100644 UnitTests/Bookmarks/Model/WebsiteInfoTests.swift create mode 100644 UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift create mode 100644 UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift create mode 100644 UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift create mode 100644 UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift create mode 100644 UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d5977b32bd..65b524fef3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1694,6 +1694,14 @@ 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; + 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; + 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; + 9F0FFFB82BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */; }; + 9F0FFFB92BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */; }; + 9F0FFFBB2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */; }; + 9F0FFFBC2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */; }; + 9F0FFFBE2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */; }; + 9F0FFFBF2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */; }; 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; @@ -1726,10 +1734,20 @@ 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F8D57322BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */; }; + 9F8D57332BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */; }; 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */; }; + 9F9C49F72BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */; }; + 9F9C49F92BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */; }; + 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */; }; + 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */; }; + 9F9C49FE2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */; }; + 9F9C4A012BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */; }; + 9F9C4A022BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */; }; 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; @@ -1740,8 +1758,18 @@ 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA5A0A52BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */; }; + 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */; }; + 9FA5A0A92BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */; }; + 9FA5A0AA2BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */; }; + 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */; }; + 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */; }; 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FAD623A2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */; }; + 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */; }; + 9FAD623D2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */; }; + 9FAD623E2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */; }; 9FBD84522BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; 9FBD84532BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; 9FBD84562BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */; }; @@ -3415,6 +3443,10 @@ 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; + 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; + 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelMock.swift; sourceTree = ""; }; + 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelMock.swift; sourceTree = ""; }; + 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogViewModelMock.swift; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; @@ -3431,14 +3463,24 @@ 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderInfo.swift; sourceTree = ""; }; + 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsBookmarkFoldersStoreTests.swift; sourceTree = ""; }; 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoreOptionsMenu+BookmarksTests.swift"; sourceTree = ""; }; + 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogView.swift; sourceTree = ""; }; + 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogViewModel.swift; sourceTree = ""; }; + 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModel.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; + 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsBookmarkFoldersStore.swift; sourceTree = ""; }; + 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogViewModelTests.swift; sourceTree = ""; }; + 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderStoreMock.swift; sourceTree = ""; }; 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactoryTests.swift; sourceTree = ""; }; + 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfoHelpers.swift; sourceTree = ""; }; + 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfoTests.swift; sourceTree = ""; }; 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionOriginFileProvider.swift; sourceTree = ""; }; 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionOriginFileProviderTests.swift; sourceTree = ""; }; 9FBD845C2BB3B80300220859 /* Origin.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = Origin.txt; sourceTree = ""; }; @@ -4615,6 +4657,7 @@ 566B195F29CDB7A9007E38F4 /* Mocks */, 378205FA283C277800D1D4AA /* MainMenuTests.swift */, 566B195C29CDB692007E38F4 /* MoreOptionsMenuTests.swift */, + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */, ); path = Menus; sourceTree = ""; @@ -6211,6 +6254,16 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F0FFFB62BCCAE80007C87DD /* Mocks */ = { + isa = PBXGroup; + children = ( + 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */, + 9F0FFFBA2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift */, + 9F0FFFBD2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 9F872D9B2B9058B000138637 /* Extensions */ = { isa = PBXGroup; children = ( @@ -6222,9 +6275,12 @@ 9F982F102B82264400231028 /* ViewModels */ = { isa = PBXGroup; children = ( + 9F0FFFB62BCCAE80007C87DD /* Mocks */, 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */, 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */, + 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */, + 9FA5A0A82BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift */, ); path = ViewModels; sourceTree = ""; @@ -6242,6 +6298,7 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */, + 9F9C49F82BC7BC970099738D /* BookmarkAllTabsDialogView.swift */, ); path = Dialog; sourceTree = ""; @@ -6254,6 +6311,14 @@ path = Factory; sourceTree = ""; }; + 9FAD62382BCFDB1D007F3A65 /* Helpers */ = { + isa = PBXGroup; + children = ( + 9FAD62392BCFDB32007F3A65 /* WebsiteInfoHelpers.swift */, + ); + path = Helpers; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -6603,6 +6668,7 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9FAD62382BCFDB1D007F3A65 /* Helpers */, 9F872D9B2B9058B000138637 /* Extensions */, 9FA75A3C2BA00DF500DA5FA6 /* Factory */, 9F982F102B82264400231028 /* ViewModels */, @@ -6629,6 +6695,7 @@ 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */, + 9FAD623C2BD09DE5007F3A65 /* WebsiteInfoTests.swift */, ); path = Model; sourceTree = ""; @@ -6638,6 +6705,8 @@ children = ( AA652CB025DD825B009059CC /* LocalBookmarkStoreTests.swift */, 986189E52A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift */, + 9FA5A0AC2BC9037A00153786 /* BookmarkFolderStoreMock.swift */, + 9F8D57312BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift */, ); path = Services; sourceTree = ""; @@ -7022,6 +7091,8 @@ 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, + 9F9C4A002BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift */, + 9F9C49FC2BC7E9820099738D /* BookmarkAllTabsDialogViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -7144,6 +7215,7 @@ B6DA06E52913F39400225DE2 /* MenuItemSelectors.swift */, AAC5E4D625D6A710007F5990 /* BookmarkStore.swift */, 987799F829999973005D8EB6 /* LocalBookmarkStore.swift */, + 9FA5A0A42BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift */, ); path = Services; sourceTree = ""; @@ -9702,6 +9774,7 @@ 98779A0129999B64005D8EB6 /* Bookmark.xcdatamodeld in Sources */, 3706FB9E293F65D500E42796 /* AboutModel.swift in Sources */, 3706FB9F293F65D500E42796 /* PasswordManagementCreditCardItemView.swift in Sources */, + 9F9C4A022BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */, 3706FBA0293F65D500E42796 /* NSTextFieldExtension.swift in Sources */, 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, @@ -9764,6 +9837,7 @@ 3706FBCC293F65D500E42796 /* CustomRoundedCornersShape.swift in Sources */, 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, + 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, @@ -9959,6 +10033,7 @@ 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, B6E3E5552BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, + 9F9C49FE2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, B60293E72BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, @@ -10069,6 +10144,7 @@ 3706FCA0293F65D500E42796 /* ContiguousBytesExtension.swift in Sources */, B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, + 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, @@ -10083,6 +10159,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F0FFFB92BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */, 3706FDDA293F661700E42796 /* EmbeddedTrackerDataTests.swift in Sources */, 3706FDDB293F661700E42796 /* AutofillPreferencesTests.swift in Sources */, 3706FDDC293F661700E42796 /* FileManagerExtensionTests.swift in Sources */, @@ -10115,6 +10192,7 @@ 3706FDF5293F661700E42796 /* StartupPreferencesTests.swift in Sources */, 3706FDF6293F661700E42796 /* DuckPlayerTests.swift in Sources */, 3706FDF7293F661700E42796 /* WebViewExtensionTests.swift in Sources */, + 9FA5A0AA2BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */, 56534DEE29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, @@ -10172,7 +10250,9 @@ 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, + 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */, + 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */, 3706FE20293F661700E42796 /* CLLocationManagerMock.swift in Sources */, B6656E0E2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, B6C843DB2BA1CAB6006FDEC3 /* FilePresenterTests.swift in Sources */, @@ -10203,10 +10283,12 @@ B630E80129C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 3706FE31293F661700E42796 /* TabCollectionViewModelDelegateMock.swift in Sources */, 3706FE32293F661700E42796 /* BookmarksHTMLReaderTests.swift in Sources */, + 9F0FFFBC2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 3706FE33293F661700E42796 /* FireTests.swift in Sources */, B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 317295D32AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */, + 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 3706FE34293F661700E42796 /* PermissionStoreTests.swift in Sources */, 3706FE35293F661700E42796 /* ThirdPartyBrowserTests.swift in Sources */, 1DFAB5232A8983E100A0F7F6 /* SetExtensionTests.swift in Sources */, @@ -10214,6 +10296,7 @@ 3706FE37293F661700E42796 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */, 3706FE38293F661700E42796 /* SuggestionContainerTests.swift in Sources */, 3706FE39293F661700E42796 /* TabTests.swift in Sources */, + 9FAD623E2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, 3706FE3A293F661700E42796 /* MockVariantManager.swift in Sources */, 3706FE3C293F661700E42796 /* FireproofDomainsStoreMock.swift in Sources */, 3706FE3D293F661700E42796 /* DataEncryptionTests.swift in Sources */, @@ -10236,6 +10319,7 @@ 3706FE46293F661700E42796 /* EncryptedValueTransformerTests.swift in Sources */, 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, 3706FE47293F661700E42796 /* URLExtensionTests.swift in Sources */, + 9F8D57332BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */, 1DB9617B29F1D06D00CF5568 /* InternalUserDeciderMock.swift in Sources */, 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, 3706FE49293F661700E42796 /* BookmarkNodePathTests.swift in Sources */, @@ -10261,6 +10345,7 @@ 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */, 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 3706FE56293F661700E42796 /* FaviconManagerMock.swift in Sources */, + 9F9C49F72BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 3706FE57293F661700E42796 /* LocalPinningManagerTests.swift in Sources */, 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, 3706FE59293F661700E42796 /* EncryptionKeyGeneratorTests.swift in Sources */, @@ -10277,6 +10362,7 @@ 3706FE5F293F661700E42796 /* CrashReportTests.swift in Sources */, B60C6F7F29B1B41D007BFAA8 /* TestRunHelperInitializer.m in Sources */, 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, + 9F0FFFBF2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, 3706FE61293F661700E42796 /* PinnedTabsViewModelTests.swift in Sources */, 3706FE62293F661700E42796 /* PasswordManagementListSectionTests.swift in Sources */, 3706FE63293F661700E42796 /* RecentlyClosedCoordinatorMock.swift in Sources */, @@ -10645,6 +10731,7 @@ 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */, B6BF5D852946FFDA006742B1 /* PrivacyDashboardTabExtension.swift in Sources */, B6E3E55B2BC0041900A41922 /* DownloadListStoreMock.swift in Sources */, + 9FA5A0A52BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, B6C0B23026E61D630031CB7F /* DownloadListStore.swift in Sources */, 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */, AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, @@ -11152,6 +11239,7 @@ AAD86E52267A0DFF005C11BE /* UpdateController.swift in Sources */, 85A0118225AF60E700FA6A0C /* FindInPageModel.swift in Sources */, 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */, + 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, @@ -11241,6 +11329,7 @@ B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */, 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, + 9F9C49F92BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */, AAA0CC472533833C0079BC96 /* MoreOptionsMenu.swift in Sources */, B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */, @@ -11337,6 +11426,7 @@ 853014D625E671A000FB8205 /* PageObserverUserScript.swift in Sources */, B677FC4F2B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, B642738227B65BAC0005DFD1 /* SecureVaultReporter.swift in Sources */, + 9F9C4A012BC7F36D0099738D /* BookmarkAllTabsDialogCoordinatorViewModel.swift in Sources */, 4B139AFD26B60BD800894F82 /* NSImageExtensions.swift in Sources */, B62B48392ADE46FC000DECE5 /* Application.swift in Sources */, 4B9DB02C2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */, @@ -11423,6 +11513,7 @@ 37D2377C287EBDA300BCE03B /* TabIndexTests.swift in Sources */, 37534CA52811987D002621E7 /* AdjacentItemEnumeratorTests.swift in Sources */, 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */, + 9FAD623A2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, C13909F42B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */, B6619EF62B10DFF700CD9186 /* InstructionsFormatParserTests.swift in Sources */, @@ -11432,6 +11523,7 @@ 4B9292BD2667103100AD2C21 /* BookmarkOutlineViewDataSourceTests.swift in Sources */, C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */, 4BF6961D28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift in Sources */, + 9FA5A0A92BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */, B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, @@ -11480,6 +11572,7 @@ 858A798826A99DBE00A75A42 /* PasswordManagementItemListModelTests.swift in Sources */, 566B196529CDB828007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, + 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, @@ -11496,9 +11589,11 @@ 85AC3B4925DAC9BD00C7D2AA /* ConfigurationStorageTests.swift in Sources */, 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */, AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, + 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 4B723E0726B0003E00E14D75 /* CSVImporterTests.swift in Sources */, 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */, B62EB47C25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift in Sources */, + 9F0FFFBB2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 4BBC16A527C488C900E00A38 /* DeviceAuthenticatorTests.swift in Sources */, 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */, B6106BA026A7BE0B0013B453 /* PermissionManagerTests.swift in Sources */, @@ -11552,6 +11647,7 @@ B6106BB126A7D8720013B453 /* PermissionStoreTests.swift in Sources */, 4BF4951826C08395000547B8 /* ThirdPartyBrowserTests.swift in Sources */, 4B98D27C28D960DD003C2B6F /* FirefoxFaviconsReaderTests.swift in Sources */, + 9F0FFFBE2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, B60C6F7E29B1B41D007BFAA8 /* TestRunHelperInitializer.m in Sources */, 37479F152891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */, AA63745424C9BF9A00AB2AC4 /* SuggestionContainerTests.swift in Sources */, @@ -11603,7 +11699,9 @@ AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */, 4BF6962028BEEE8B00D402D4 /* LocalPinningManagerTests.swift in Sources */, + 9F0FFFB82BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */, AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */, + 9FAD623D2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, 4BA1A6E6258C270800F6F690 /* EncryptionKeyGeneratorTests.swift in Sources */, B6106BB326A7F4AA0013B453 /* GeolocationServiceMock.swift in Sources */, @@ -11612,6 +11710,7 @@ 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */, 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */, 1D1C36E329FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, + 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 4B723E0526B0003E00E14D75 /* DataImportMocks.swift in Sources */, 4B70C00227B0793D000386ED /* CrashReportTests.swift in Sources */, B6656E0D2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, @@ -11646,6 +11745,7 @@ 9FBD84562BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */, 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */, 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, + 9F8D57322BCCCB9A00AEA660 /* UserDefaultsBookmarkFoldersStoreTests.swift in Sources */, 4B723E0826B0003E00E14D75 /* MockSecureVault.swift in Sources */, 37CD54B527F1AC1300F1F7B9 /* PreferencesSidebarModelTests.swift in Sources */, B6CA4824298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index b3172da78e..1220f5eabb 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -28,8 +28,10 @@ protocol BookmarkManager: AnyObject { func allHosts() -> Set func getBookmark(for url: URL) -> Bookmark? func getBookmark(forUrl url: String) -> Bookmark? + func getBookmarkFolder(withId id: String) -> BookmarkFolder? @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool, index: Int?, parent: BookmarkFolder?) -> Bookmark? + func makeBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) func makeFolder(for title: String, parent: BookmarkFolder?, completion: @escaping (BookmarkFolder) -> Void) func remove(bookmark: Bookmark) func remove(folder: BookmarkFolder) @@ -46,7 +48,6 @@ protocol BookmarkManager: AnyObject { func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) func moveFavorites(with objectUUIDs: [String], toIndex: Int?, completion: @escaping (Error?) -> Void) func importBookmarks(_ bookmarks: ImportedBookmarks, source: BookmarkImportSource) -> BookmarksImportSummary - func handleFavoritesAfterDisablingSync() // Wrapper definition in a protocol is not supported yet @@ -138,6 +139,10 @@ final class LocalBookmarkManager: BookmarkManager { return list?[url] } + func getBookmarkFolder(withId id: String) -> BookmarkFolder? { + bookmarkStore.bookmarkFolder(withId: id) + } + @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? { makeBookmark(for: url, title: title, isFavorite: isFavorite, index: nil, parent: nil) } @@ -167,6 +172,12 @@ final class LocalBookmarkManager: BookmarkManager { return bookmark } + func makeBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) { + bookmarkStore.saveBookmarks(for: websitesInfo, inNewFolderNamed: folderName, withinParentFolder: parent) + loadBookmarks() + requestSync() + } + func remove(bookmark: Bookmark) { guard list != nil else { return } guard let latestBookmark = getBookmark(forUrl: bookmark.url) else { diff --git a/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift b/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift index fc393b256d..a4d146abd7 100644 --- a/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift +++ b/DuckDuckGo/Bookmarks/Model/WebsiteInfo.swift @@ -18,15 +18,17 @@ import Foundation -struct WebsiteInfo { +struct WebsiteInfo: Equatable { let url: URL - let title: String? + /// Returns the title of the website if available, otherwise returns the domain of the URL. + /// If both title and and domain are nil, it returns the absolute string representation of the URL. + let title: String init?(_ tab: Tab) { guard case let .url(url, _, _) = tab.content else { return nil } self.url = url - self.title = tab.title + self.title = tab.title ?? url.host ?? url.absoluteString } } diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 3466d1a7fa..8c8f4a6d06 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -48,9 +48,11 @@ protocol BookmarkStore { func loadAll(type: BookmarkStoreFetchPredicateType, completion: @escaping ([BaseBookmarkEntity]?, Error?) -> Void) func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void) + func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) func save(folder: BookmarkFolder, parent: BookmarkFolder?, completion: @escaping (Bool, Error?) -> Void) func remove(objectsWithUUIDs: [String], completion: @escaping (Bool, Error?) -> Void) func update(bookmark: Bookmark) + func bookmarkFolder(withId id: String) -> BookmarkFolder? func update(folder: BookmarkFolder) func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) func add(objectsWithUUIDs: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) @@ -59,6 +61,5 @@ protocol BookmarkStore { func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) func moveFavorites(with objectUUIDs: [String], toIndex: Int?, completion: @escaping (Error?) -> Void) func importBookmarks(_ bookmarks: ImportedBookmarks, source: BookmarkImportSource) -> BookmarksImportSummary - func handleFavoritesAfterDisablingSync() } diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index 2ab57158a9..003ba7bb59 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -104,6 +104,15 @@ public final class BookmarkStoreMock: BookmarkStore { capturedBookmark = bookmark } + var bookmarkFolderWithIdCalled = false + var capturedFolderId: String? + var bookmarkFolder: BookmarkFolder? + func bookmarkFolder(withId id: String) -> BookmarkFolder? { + bookmarkFolderWithIdCalled = true + capturedFolderId = id + return bookmarkFolder + } + var updateFolderCalled = false func update(folder: BookmarkFolder) { updateFolderCalled = true @@ -133,6 +142,16 @@ public final class BookmarkStoreMock: BookmarkStore { return BookmarksImportSummary(successful: 0, duplicates: 0, failed: 0) } + var saveBookmarksInNewFolderNamedCalled = false + var capturedWebsitesInfo: [WebsiteInfo]? + var capturedNewFolderName: String? + func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) { + saveBookmarksInNewFolderNamedCalled = true + capturedWebsitesInfo = websitesInfo + capturedNewFolderName = folderName + capturedParentFolderType = parent + } + var canMoveObjectWithUUIDCalled = false func canMoveObjectWithUUID(objectUUID uuid: String, to parent: BookmarkFolder) -> Bool { canMoveObjectWithUUIDCalled = true diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index fb21f3222f..2a480e8333 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -65,6 +65,7 @@ final class LocalBookmarkStore: BookmarkStore { case missingRoot case missingFavoritesRoot case saveLoopError(Error?) + case badModelMapping } private(set) var favoritesDisplayMode: FavoritesDisplayMode @@ -339,6 +340,23 @@ final class LocalBookmarkStore: BookmarkStore { }) } + func saveBookmarks(for websitesInfo: [WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: ParentFolderType) { + do { + try applyChangesAndSave { context in + // Fetch Parent folder + let parentFolder = try bookmarkEntity(for: parent, in: context) + // Create new Folder for all bookmarks + let newFolderMO = BookmarkEntity.makeFolder(title: folderName, parent: parentFolder, context: context) + // Save the bookmarks + websitesInfo.forEach { info in + _ = BookmarkEntity.makeBookmark(title: info.title, url: info.url.absoluteString, parent: newFolderMO, context: context) + } + } + } catch { + commonOnSaveErrorHandler(error) + } + } + func remove(objectsWithUUIDs identifiers: [String], completion: @escaping (Bool, Error?) -> Void) { applyChangesAndSave(changes: { [weak self] context in @@ -390,6 +408,38 @@ final class LocalBookmarkStore: BookmarkStore { } } + func bookmarkFolder(withId id: String) -> BookmarkFolder? { + let context = makeContext() + + var bookmarkFolderToReturn: BookmarkFolder? + let favoritesDisplayMode = self.favoritesDisplayMode + + context.performAndWait { + let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: id) + do { + let folderFetchRequestResult = try context.fetch(folderFetchRequest) + guard let bookmarkFolderManagedObject = folderFetchRequestResult.first else { return } + + guard let bookmarkFolder = BaseBookmarkEntity.from( + managedObject: bookmarkFolderManagedObject, + parentFolderUUID: bookmarkFolderManagedObject.parent?.uuid, + favoritesDisplayMode: favoritesDisplayMode + ) as? BookmarkFolder + else { + throw BookmarkStoreError.badModelMapping + } + bookmarkFolderToReturn = bookmarkFolder + + } catch BookmarkStoreError.badModelMapping { + os_log("Failed to map BookmarkEntity to BookmarkFolder, with error: %s", log: .bookmarks, type: .error) + } catch { + os_log("Failed to fetch last saved folder for bookmarks all tabs, with error: %s", log: .bookmarks, type: .error, error.localizedDescription) + } + } + + return bookmarkFolderToReturn + } + func update(folder: BookmarkFolder) { do { _ = try applyChangesAndSave(changes: { [weak self] context in @@ -998,32 +1048,38 @@ private extension LocalBookmarkStore { } func move(entities: [BookmarkEntity], toIndex index: Int?, withinParentFolderType type: ParentFolderType, in context: NSManagedObjectContext) throws { + let newParentFolder = try bookmarkEntity(for: type, in: context) + + if let index = index, index < newParentFolder.childrenArray.count { + self.move(entities: entities, to: index, within: newParentFolder) + } else { + for bookmarkManagedObject in entities { + bookmarkManagedObject.parent = nil + newParentFolder.addToChildren(bookmarkManagedObject) + } + } + } + + func bookmarkEntity(for parentFolderType: ParentFolderType, in context: NSManagedObjectContext) throws -> BookmarkEntity { guard let rootFolder = bookmarksRoot(in: context) else { throw BookmarkStoreError.missingRoot } - let newParentFolder: BookmarkEntity + let parentFolder: BookmarkEntity - switch type { - case .root: newParentFolder = rootFolder - case .parent(let newParentUUID): - let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + switch parentFolderType { + case .root: + parentFolder = rootFolder + case let .parent(parentUUID): + let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: parentUUID) if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { - newParentFolder = fetchedParent + parentFolder = fetchedParent } else { throw BookmarkStoreError.missingEntity } } - - if let index = index, index < newParentFolder.childrenArray.count { - self.move(entities: entities, to: index, within: newParentFolder) - } else { - for bookmarkManagedObject in entities { - bookmarkManagedObject.parent = nil - newParentFolder.addToChildren(bookmarkManagedObject) - } - } + return parentFolder } } @@ -1041,6 +1097,7 @@ extension LocalBookmarkStore.BookmarkStoreError: CustomNSError { case .missingRoot: return 7 case .missingFavoritesRoot: return 8 case .saveLoopError: return 9 + case .badModelMapping: return 10 } } diff --git a/DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift b/DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift new file mode 100644 index 0000000000..7fb3187329 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Services/UserDefaultsBookmarkFoldersStore.swift @@ -0,0 +1,48 @@ +// +// UserDefaultsBookmarkFoldersStore.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 + +/// A type used to provide the ID of the folder where all tabs were last saved. +protocol BookmarkFoldersStore: AnyObject { + /// The ID of the folder where all bookmarks from the last session were saved. + var lastBookmarkAllTabsFolderIdUsed: String? { get set } +} + +final class UserDefaultsBookmarkFoldersStore: BookmarkFoldersStore { + + enum Keys { + static let bookmarkAllTabsFolderUsedKey = "bookmarks.all-tabs.last-used-folder" + } + + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + var lastBookmarkAllTabsFolderIdUsed: String? { + get { + userDefaults.string(forKey: Keys.bookmarkAllTabsFolderUsedKey) + } + set { + userDefaults.set(newValue, forKey: Keys.bookmarkAllTabsFolderUsedKey) + } + } + +} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index f15465df36..8ce41c17f2 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -36,7 +36,6 @@ struct AddBookmarkFolderPopoverView: ModalView { isDefaultActionDisabled: model.isDefaultActionButtonDisabled, defaultAction: { _ in model.addFolder() } ) - .padding(.vertical, 16.0) .font(.system(size: 13)) .frame(width: 320) } diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index fba84b44bc..1faa240398 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -55,7 +55,6 @@ struct AddBookmarkPopoverView: View { isDefaultActionDisabled: model.isDefaultActionButtonDisabled, defaultAction: model.doneButtonAction ) - .padding(.vertical, 16.0) .font(.system(size: 13)) .frame(width: 320) } diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 2c0256bba4..78cdc6efcd 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -55,7 +55,7 @@ struct AddEditBookmarkDialogView: ModalView { isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, defaultAction: viewModel.bookmarkModel.addOrSave ) - .frame(width: 448, height: 288) + .frame(width: 448) } private var addFolderView: some View { diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift new file mode 100644 index 0000000000..ad920b10c6 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkAllTabsDialogView.swift @@ -0,0 +1,141 @@ +// +// BookmarkAllTabsDialogView.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 SwiftUIExtensions + +struct BookmarkAllTabsDialogView: ModalView { + @ObservedObject private var viewModel: BookmarkAllTabsDialogCoordinatorViewModel + + init(viewModel: BookmarkAllTabsDialogCoordinatorViewModel) { + self.viewModel = viewModel + } + + var body: some View { + Group { + switch viewModel.viewState { + case .bookmarkAllTabs: + bookmarkAllTabsView + case .addFolder: + addFolderView + } + } + .font(.system(size: 13)) + } + + private var bookmarkAllTabsView: some View { + BookmarkDialogContainerView( + title: viewModel.bookmarkModel.title, + middleSection: { + Text(viewModel.bookmarkModel.educationalMessage) + .multilineText() + .multilineTextAlignment(.leading) + .foregroundColor(.secondary) + .font(.system(size: 11)) + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.folderName, + content: TextField("", text: $viewModel.bookmarkModel.folderName) + .focusedOnAppear() + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: viewModel.bookmarkModel.folders, + selectedFolder: $viewModel.bookmarkModel.selectedFolder, + onActionButton: viewModel.addFolderAction + ) + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(.compressed), + otherButtonAction: .init( + title: viewModel.bookmarkModel.cancelActionTitle, + isDisabled: viewModel.bookmarkModel.isOtherActionDisabled, + action: viewModel.bookmarkModel.cancel + ), + defaultButtonAction: .init( + title: viewModel.bookmarkModel.defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, + action: viewModel.bookmarkModel.addOrSave + ) + ) + } + + ) + .frame(width: 448) + } + + private var addFolderView: some View { + AddEditBookmarkFolderView( + title: viewModel.folderModel.title, + buttonsState: .compressed, + folders: viewModel.folderModel.folders, + folderName: $viewModel.folderModel.folderName, + selectedFolder: $viewModel.folderModel.selectedFolder, + cancelActionTitle: viewModel.folderModel.cancelActionTitle, + isCancelActionDisabled: viewModel.folderModel.isOtherActionDisabled, + cancelAction: { _ in + viewModel.dismissAction() + }, + defaultActionTitle: viewModel.folderModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.folderModel.isDefaultActionDisabled, + defaultAction: { _ in + viewModel.folderModel.addOrSave { + viewModel.dismissAction() + } + } + ) + .frame(width: 448, height: 210) + } +} + +#if DEBUG +#Preview("Bookmark All Tabs - Light") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let websitesInfo: [WebsiteInfo] = [ + .init(.init(content: .url(URL.duckDuckGo, credential: nil, source: .ui)))!, + .init(.init(content: .url(URL.duckDuckGoEmail, credential: nil, source: .ui)))!, + ] + + return BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo, bookmarkManager: bookmarkManager) + .preferredColorScheme(.light) +} + +#Preview("Bookmark All Tabs - Dark") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let websitesInfo: [WebsiteInfo] = [ + .init(.init(content: .url(URL.duckDuckGo, credential: nil, source: .ui)))!, + .init(.init(content: .url(URL.duckDuckGoEmail, credential: nil, source: .ui)))!, + ] + + return BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo, bookmarkManager: bookmarkManager) + .preferredColorScheme(.dark) +} +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift index ea49712abb..120869ea4f 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift @@ -42,9 +42,13 @@ struct BookmarkDialogContainerView: View { Text(title) .foregroundColor(.primary) .fontWeight(.semibold) + .padding(.top, 20) }, center: middleSection, - bottom: bottomSection + bottom: { + bottomSection() + .padding(.bottom, 16.0) + } ) } } diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift index b29b50bbbb..3bff7ff4af 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -80,6 +80,18 @@ enum BookmarksDialogViewFactory { return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) } + /// Creates an instance of AddEditBookmarkDialogView for adding Bookmarks for all the open Tabs. + /// - Parameters: + /// - websitesInfo: A list of websites to add as bookmarks. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of BookmarkAllTabsDialogView + static func makeBookmarkAllOpenTabsView(websitesInfo: [WebsiteInfo], bookmarkManager: LocalBookmarkManager = .shared) -> BookmarkAllTabsDialogView { + let addFolderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let bookmarkAllTabsViewModel = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: UserDefaultsBookmarkFoldersStore(), bookmarkManager: bookmarkManager) + let viewModel = BookmarkAllTabsDialogCoordinatorViewModel(bookmarkModel: bookmarkAllTabsViewModel, folderModel: addFolderViewModel) + return BookmarkAllTabsDialogView(viewModel: viewModel) + } + } private extension BookmarksDialogViewFactory { diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift index 62c1e0356c..48815ebc8d 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -42,8 +42,9 @@ final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { @Published var folderName: String @Published var selectedFolder: BookmarkFolder? + @Published private(set) var folders: [FolderViewModel] - let folders: [FolderViewModel] + private var folderCancellable: AnyCancellable? var title: String { mode.title @@ -77,14 +78,20 @@ final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { folderName = mode.folderName folders = .init(bookmarkManager.list) selectedFolder = mode.parentFolder + + bind() } func cancel(dismiss: () -> Void) { + reset() dismiss() } func addOrSave(dismiss: () -> Void) { - defer { dismiss() } + defer { + reset() + dismiss() + } guard !folderName.isEmpty else { assertionFailure("folderName is empty, button should be disabled") @@ -110,6 +117,14 @@ final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { private extension AddEditBookmarkFolderDialogViewModel { + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) + }) + } + func update(folder: BookmarkFolder, originalParent: BookmarkFolder?, newParent: BookmarkFolder?) { // If the original location of the folder changed move it to the new folder. if selectedFolder?.id != originalParent?.id { @@ -129,6 +144,10 @@ private extension AddEditBookmarkFolderDialogViewModel { } } + func reset() { + self.folderName = "" + } + } // MARK: - AddEditBookmarkFolderDialogViewModel.Mode diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift new file mode 100644 index 0000000000..975c8811b6 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogCoordinatorViewModel.swift @@ -0,0 +1,74 @@ +// +// BookmarkAllTabsDialogCoordinatorViewModel.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 Combine + +final class BookmarkAllTabsDialogCoordinatorViewModel: ObservableObject { + @ObservedObject var bookmarkModel: BookmarkViewModel + @ObservedObject var folderModel: AddFolderViewModel + @Published var viewState: ViewState + + private var cancellables: Set = [] + + init(bookmarkModel: BookmarkViewModel, folderModel: AddFolderViewModel) { + self.bookmarkModel = bookmarkModel + self.folderModel = folderModel + viewState = .bookmarkAllTabs + bind() + } + + func dismissAction() { + viewState = .bookmarkAllTabs + } + + func addFolderAction() { + folderModel.selectedFolder = bookmarkModel.selectedFolder + viewState = .addFolder + } + + private func bind() { + bookmarkModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.addFolderPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] bookmarkFolder in + self?.bookmarkModel.selectedFolder = bookmarkFolder + } + .store(in: &cancellables) + } +} + +extension BookmarkAllTabsDialogCoordinatorViewModel { + enum ViewState { + case bookmarkAllTabs + case addFolder + } +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift new file mode 100644 index 0000000000..daf6018403 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkAllTabsDialogViewModel.swift @@ -0,0 +1,127 @@ +// +// BookmarkAllTabsDialogViewModel.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 Combine + +@MainActor +protocol BookmarkAllTabsDialogEditing: BookmarksDialogViewModel { + var folderName: String { get set } + var educationalMessage: String { get } + var folderNameFieldTitle: String { get } + var locationFieldTitle: String { get } +} + +final class BookmarkAllTabsDialogViewModel: BookmarkAllTabsDialogEditing { + private static let dateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withFullDate, .withDashSeparatorInDate] + return formatter + }() + + private let websites: [WebsiteInfo] + private let foldersStore: BookmarkFoldersStore + private let bookmarkManager: BookmarkManager + + private var folderCancellable: AnyCancellable? + + @Published private(set) var folders: [FolderViewModel] + @Published var selectedFolder: BookmarkFolder? + @Published var folderName: String + + var title: String { + String(format: UserText.Bookmarks.Dialog.Title.bookmarkOpenTabs, websites.count) + } + let cancelActionTitle = UserText.cancel + let defaultActionTitle = UserText.Bookmarks.Dialog.Action.addAllBookmarks + let educationalMessage = UserText.Bookmarks.Dialog.Message.bookmarkOpenTabsEducational + let folderNameFieldTitle = UserText.Bookmarks.Dialog.Field.folderName + let locationFieldTitle = UserText.Bookmarks.Dialog.Field.location + let isOtherActionDisabled = false + + var isDefaultActionDisabled: Bool { + folderName.trimmingWhitespace().isEmpty + } + + init( + websites: [WebsiteInfo], + foldersStore: BookmarkFoldersStore, + bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, + dateFormatterConfigurationProvider: () -> DateFormatterConfiguration = DateFormatterConfiguration.defaultConfiguration + ) { + self.websites = websites + self.foldersStore = foldersStore + self.bookmarkManager = bookmarkManager + + folders = .init(bookmarkManager.list) + selectedFolder = foldersStore.lastBookmarkAllTabsFolderIdUsed.flatMap(bookmarkManager.getBookmarkFolder(withId:)) + folderName = Self.folderName(configuration: dateFormatterConfigurationProvider(), websitesNumber: websites.count) + bind() + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + // Save last used folder + foldersStore.lastBookmarkAllTabsFolderIdUsed = selectedFolder?.id + + // Save all bookmarks + let parentFolder: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.makeBookmarks(for: websites, inNewFolderNamed: folderName, withinParentFolder: parentFolder) + + // Dismiss the view + dismiss() + } +} + +// MARK: - Private + +private extension BookmarkAllTabsDialogViewModel { + + static func folderName(configuration: DateFormatterConfiguration, websitesNumber: Int) -> String { + Self.dateFormatter.timeZone = configuration.timeZone + let dateString = Self.dateFormatter.string(from: configuration.date) + return String(format: UserText.Bookmarks.Dialog.Value.folderName, dateString, websitesNumber) + } + + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) + }) + } + +} + +// MARK: - DateConfiguration + +extension BookmarkAllTabsDialogViewModel { + + struct DateFormatterConfiguration { + let date: Date + let timeZone: TimeZone + + static func defaultConfiguration() -> DateFormatterConfiguration { + .init(date: Date(), timeZone: .current) + } + } + +} diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 4f57400e27..a201afff72 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -465,7 +465,8 @@ struct UserText { static let addFavorite = NSLocalizedString("add.favorite", value: "Add Favorite", comment: "Button for adding a favorite bookmark") static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") - static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page", comment: "Menu item for bookmarking current page") + static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page…", comment: "Menu item for bookmarking current page") + static let bookmarkAllTabs = NSLocalizedString("bookmark.all.tabs", value: "Bookmark All Tabs…", comment: "Menu item for bookmarking all the open tabs") static let bookmarksShowToolbarPanel = NSLocalizedString("bookmarks.show-toolbar-panel", value: "Open Bookmarks Panel", comment: "Menu item for opening the bookmarks panel") static let bookmarksManageBookmarks = NSLocalizedString("bookmarks.manage-bookmarks", value: "Manage Bookmarks", comment: "Menu item for opening the bookmarks management interface") static let bookmarkImportedFromFolder = NSLocalizedString("bookmarks.imported.from.folder", value: "Imported from", comment: "Name of the folder the imported bookmarks are saved into") @@ -1105,15 +1106,24 @@ struct UserText { static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit Bookmark", comment: "Bookmark edit dialog title") static let addFolder = NSLocalizedString("bookmarks.dialog.folder.title.add", value: "Add Folder", comment: "Bookmark folder creation dialog title") static let editFolder = NSLocalizedString("bookmarks.dialog.folder.title.edit", value: "Edit Folder", comment: "Bookmark folder edit dialog title") + static let bookmarkOpenTabs = NSLocalizedString("bookmarks.dialog.allTabs.title.add", value: "Bookmark Open Tabs (%d)", comment: "Title of dialog to bookmark all open tabs. E.g. 'Bookmark Open Tabs (42)'") + } + enum Message { + static let bookmarkOpenTabsEducational = NSLocalizedString("bookmarks.dialog.allTabs.message.add", value: "These bookmarks will be saved in a new folder:", comment: "Bookmark creation for all open tabs dialog title") } enum Field { static let name = NSLocalizedString("bookmarks.dialog.field.name", value: "Name", comment: "Name field label for Bookmark or Folder") static let url = NSLocalizedString("bookmarks.dialog.field.url", value: "URL", comment: "URL field label for Bookmar") static let location = NSLocalizedString("bookmarks.dialog.field.location", value: "Location", comment: "Location field label for Bookmark folder") + static let folderName = NSLocalizedString("bookmarks.dialog.field.folderName", value: "Folder Name", comment: "Folder name field label for Bookmarks folder") + } + enum Value { + static let folderName = NSLocalizedString("bookmarks.dialog.field.folderName.value", value: "%@ - Tabs (%d)", comment: "The suggested name of the folder that will contain the bookmark tabs. Eg. 2024-02-12 - Tabs (42)") } enum Action { static let addBookmark = NSLocalizedString("bookmarks.dialog.action.addBookmark", value: "Add Bookmark", comment: "CTA title for adding a Bookmark") static let addFolder = NSLocalizedString("bookmarks.dialog.action.addFolder", value: "Add Folder", comment: "CTA title for adding a Folder") + static let addAllBookmarks = NSLocalizedString("bookmarks.dialog.action.addAllBookmarks", value: "Save Bookmarks", comment: "CTA title for saving multiple Bookmarks at once") } } } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 993f125ffd..03514781c1 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -7474,6 +7474,66 @@ } } }, + "bookmark.all.tabs" : { + "comment" : "Menu item for bookmarking all the open tabs", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Tabs mit Lesezeichen versehen…" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark All Tabs…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir todas las pestañas a marcadores…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter tous les onglets aux signets…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crea un segnalibro con tutte le schede…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle tabbladen toevoegen als bladwijzer…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj wszystkie karty do zakładek…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marcar todos os separadores…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить все вкладки в закладки…" + } + } + } + }, "bookmark.dialog.add" : { "comment" : "Button to confim a bookmark creation", "extractionState" : "extracted_with_value", @@ -7601,55 +7661,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Diese Seite als Lesezeichen markieren" + "value" : "Diese Seite mit einem Lesezeichen versehen…" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Bookmark This Page" + "value" : "Bookmark This Page…" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Marcar esta página" + "value" : "Marcar esta página…" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ajouter cette page aux signets" + "value" : "Ajouter cette page aux signets…" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Crea un segnalibro per questa pagina" + "value" : "Crea un segnalibro per questa pagina…" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bladwijzer toevoegen aan deze pagina" + "value" : "Bladwijzer toevoegen voor deze pagina…" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dodaj tę stronę do zakładek" + "value" : "Dodaj tę stronę do zakładek…" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Marcar esta página" + "value" : "Marcar esta página…" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранить в закладках" + "value" : "Сохранить страницу в закладках…" } } } @@ -8660,6 +8720,66 @@ } } }, + "bookmarks.dialog.action.addAllBookmarks" : { + "comment" : "CTA title for saving multiple Bookmarks at once", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lesezeichen speichern" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Save Bookmarks" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar marcadores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistrer les signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salva segnalibri" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bladwijzers opslaan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zapisz zakładki" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar marcadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохранение закладок" + } + } + } + }, "bookmarks.dialog.action.addBookmark" : { "comment" : "CTA title for adding a Bookmark", "extractionState" : "extracted_with_value", @@ -8780,6 +8900,246 @@ } } }, + "bookmarks.dialog.allTabs.message.add" : { + "comment" : "Bookmark creation for all open tabs dialog title", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Lesezeichen werden in einem neuen Ordner gespeichert:" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "These bookmarks will be saved in a new folder:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estos marcadores se guardarán en una nueva carpeta:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ces signets seront enregistrés dans un nouveau dossier:" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questi segnalibri verranno salvati in una nuova cartella:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze bladwijzers worden opgeslagen in een nieuwe map:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te zakładki zostaną zapisane w nowym folderze:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estes marcadores serão guardados numa nova pasta:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эти закладки будут сохранены в новой папке:" + } + } + } + }, + "bookmarks.dialog.allTabs.title.add" : { + "comment" : "Title of dialog to bookmark all open tabs. E.g. 'Bookmark Open Tabs (42)'", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Offene Tabs mit Lesezeichen versehen (%d)" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark Open Tabs (%d)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir pestañas abiertas (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter les onglets ouverts (%d) aux signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungere ai segnalibri le schede aperte (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d geopende tabbladen toevoegen als bladwijzer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj otwarte karty (%d) do zakładek" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marcar separadores abertos (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить открытые вкладки (%d) в закладки" + } + } + } + }, + "bookmarks.dialog.field.folderName" : { + "comment" : "Folder name field label for Bookmarks folder", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ordnername" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Folder Name" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nombre de la carpeta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nom du dossier" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nome della cartella" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naam van de map" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nazwa folderu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nome da pasta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название папки" + } + } + } + }, + "bookmarks.dialog.field.folderName.value" : { + "comment" : "The suggested name of the folder that will contain the bookmark tabs. Eg. 2024-02-12 - Tabs (42)", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ – Tabs (%2$d)" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - Tabs (%2$d)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Pestañas (%2$d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Onglets (%2$d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Schede (%2$d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Tabbladen (%2$d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Karty (%2$d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Separadores (%2$d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - Вкладки (%2$d)" + } + } + } + }, "bookmarks.dialog.field.location" : { "comment" : "Location field label for Bookmark folder", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 132e25075d..66e026dc1d 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -291,6 +291,7 @@ import SubscriptionUI func buildBookmarksMenu() -> NSMenuItem { NSMenuItem(title: UserText.bookmarks).submenu(bookmarksMenu.buildItems { NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d") + NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(MainViewController.bookmarkAllOpenTabs), keyEquivalent: [.command, .shift, "d"]) manageBookmarksMenuItem bookmarksMenuToggleBookmarksBarMenuItem NSMenuItem.separator() @@ -557,6 +558,14 @@ import SubscriptionUI let debugMenu = NSMenu(title: "Debug") { NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() + NSMenuItem(title: "Tab") { + NSMenuItem(title: "Append Tabs") { + NSMenuItem(title: "10 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 10) + NSMenuItem(title: "50 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 50) + NSMenuItem(title: "100 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 100) + NSMenuItem(title: "150 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 150) + } + } NSMenuItem(title: "Reset Data") { NSMenuItem(title: "Reset Default Browser Prompt", action: #selector(MainViewController.resetDefaultBrowserPrompt)) NSMenuItem(title: "Reset Default Grammar Checks", action: #selector(MainViewController.resetDefaultGrammarChecks)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 59729584ac..ec129e39df 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -507,6 +507,11 @@ extension MainViewController { .openBookmarkPopover(setFavorite: false, accessPoint: .init(sender: sender, default: .moreMenu)) } + @objc func bookmarkAllOpenTabs(_ sender: Any) { + let websitesInfo = tabCollectionViewModel.tabs.compactMap(WebsiteInfo.init) + BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo).show() + } + @objc func favoriteThisPage(_ sender: Any) { guard let tabIndex = getActiveTabAndIndex()?.index else { return } if tabCollectionViewModel.selectedTabIndex != tabIndex { @@ -656,6 +661,14 @@ extension MainViewController { // MARK: - Debug + @objc func addDebugTabs(_ sender: AnyObject) { + let numberOfTabs = sender.representedObject as? Int ?? 1 + (1...numberOfTabs).forEach { _ in + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .ui)) + tabCollectionViewModel.append(tab: tab) + } + } + @objc func resetDefaultBrowserPrompt(_ sender: Any?) { UserDefaultsWrapper.clear(.defaultBrowserDismissed) } @@ -944,6 +957,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(MainViewController.bookmarkThisPage(_:)), #selector(MainViewController.favoriteThisPage(_:)): return activeTabViewModel?.canBeBookmarked == true + case #selector(MainViewController.bookmarkAllOpenTabs(_:)): + return tabCollectionViewModel.canBookmarkAllOpenTabs() case #selector(MainViewController.openBookmark(_:)), #selector(MainViewController.showManageBookmarks(_:)): return true diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 2638bc96bb..ed7bb58e86 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -28,6 +28,7 @@ import Subscription protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedBookmarkThisPage(_ sender: NSMenuItem) + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkManagementInterface(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkImportInterface(_ menu: NSMenu) @@ -171,6 +172,10 @@ final class MoreOptionsMenu: NSMenu { actionDelegate?.optionsButtonMenuRequestedBookmarkThisPage(sender) } + @objc func bookmarkAllOpenTabs(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedBookmarkAllOpenTabs(sender) + } + @objc func openBookmarks(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkPopover(self) } @@ -627,6 +632,12 @@ final class BookmarksSubMenu: NSMenu { bookmarkPageItem.isEnabled = tabCollectionViewModel.selectedTabViewModel?.canBeBookmarked == true + let bookmarkAllTabsItem = addItem(withTitle: UserText.bookmarkAllTabs, action: #selector(MoreOptionsMenu.bookmarkAllOpenTabs(_:)), keyEquivalent: "d") + .withModifierMask([.command, .shift]) + .targetting(target) + + bookmarkAllTabsItem.isEnabled = tabCollectionViewModel.canBookmarkAllOpenTabs() + addItem(NSMenuItem.separator()) addItem(withTitle: UserText.bookmarksShowToolbarPanel, action: #selector(MoreOptionsMenu.openBookmarks(_:)), keyEquivalent: "") diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index d27b62fd5d..dc181f12b1 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -1011,6 +1011,11 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { .openBookmarkPopover(setFavorite: false, accessPoint: .init(sender: sender, default: .moreMenu)) } + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) { + let websitesInfo = tabCollectionViewModel.tabs.compactMap(WebsiteInfo.init) + BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo).show() + } + func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { popovers.showBookmarkListPopover(usingView: bookmarkListButton, withDelegate: self, diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 708d25fbe4..e2eeb8d192 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -727,7 +727,6 @@ enum GeneralPixel: PixelKitEventV2 { case .bookmarksMigrationCouldNotRemoveOldStore: return "bookmarks_migration_could_not_remove_old_store" case .bookmarksMigrationCouldNotPrepareMultipleFavoriteFolders: return "bookmarks_migration_could_not_prepare_multiple_favorite_folders" - case .syncSentUnauthenticatedRequest: return "sync_sent_unauthenticated_request" case .syncMetadataCouldNotLoadDatabase: return "sync_metadata_could_not_load_database" case .syncBookmarksProviderInitializationFailed: return "sync_bookmarks_provider_initialization_failed" diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 8f0253224b..1fe33c029c 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1049,6 +1049,15 @@ extension TabBarViewController: TabBarViewItemDelegate { bookmarkTab(with: url, title: tabViewModel.title) } + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { + tabCollectionViewModel.canBookmarkAllOpenTabs() + } + + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) { + let websitesInfo = tabCollectionViewModel.tabs.compactMap(WebsiteInfo.init) + BookmarksDialogViewFactory.makeBookmarkAllOpenTabsView(websitesInfo: websitesInfo).show() + } + func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index a92752aa56..02b3ab4af5 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -33,6 +33,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemTogglePermissionAction(_ tabBarViewItem: TabBarViewItem) @@ -41,6 +42,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemDuplicateAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemPinAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewBurnerWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemFireproofSite(_ tabBarViewItem: TabBarViewItem) @@ -196,6 +198,10 @@ final class TabBarViewItem: NSCollectionViewItem { delegate?.tabBarViewItemBookmarkThisPageAction(self) } + @objc func bookmarkAllOpenTabsAction(_ sender: Any) { + delegate?.tabBarViewItemBookmarkAllOpenTabsAction(self) + } + private var lastKnownIndexPath: IndexPath? @IBAction func closeButtonAction(_ sender: Any) { @@ -486,16 +492,19 @@ extension TabBarViewItem: NSMenuDelegate { // Section 1 addDuplicateMenuItem(to: menu) addPinMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) + addMuteUnmuteMenuItem(to: menu) + menu.addItem(.separator()) // Section 2 - addBookmarkMenuItem(to: menu) addFireproofMenuItem(to: menu) - - addMuteUnmuteMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) + addBookmarkMenuItem(to: menu) + menu.addItem(.separator()) // Section 3 + addBookmarkAllTabsMenuItem(to: menu) + menu.addItem(.separator()) + + // Section 4 addCloseMenuItem(to: menu) addCloseOtherMenuItem(to: menu, areThereOtherTabs: areThereOtherTabs) addCloseTabsToTheRightMenuItem(to: menu, areThereTabsToTheRight: otherItemsState.hasItemsToTheRight) @@ -525,6 +534,13 @@ extension TabBarViewItem: NSMenuDelegate { menu.addItem(bookmarkMenuItem) } + private func addBookmarkAllTabsMenuItem(to menu: NSMenu) { + let bookmarkMenuItem = NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(bookmarkAllOpenTabsAction(_:)), keyEquivalent: "") + bookmarkMenuItem.target = self + bookmarkMenuItem.isEnabled = delegate?.tabBarViewAllItemsCanBeBookmarked(self) ?? false + menu.addItem(bookmarkMenuItem) + } + private func addFireproofMenuItem(to menu: NSMenu) { var menuItem = NSMenuItem(title: UserText.fireproofSite, action: #selector(fireproofSiteAction(_:)), keyEquivalent: "") menuItem.isEnabled = false @@ -542,7 +558,6 @@ extension TabBarViewItem: NSMenuDelegate { private func addMuteUnmuteMenuItem(to menu: NSMenu) { guard let audioState = delegate?.tabBarViewItemAudioState(self) else { return } - menu.addItem(NSMenuItem.separator()) let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab let muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") muteUnmuteMenuItem.target = self diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index 0317aed9b9..2467c1d900 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -763,3 +763,14 @@ extension TabCollectionViewModel { } } + +// MARK: - Bookmark All Open Tabs + +extension TabCollectionViewModel { + + func canBookmarkAllOpenTabs() -> Bool { + // At least two non pinned, non empty (URL only), and not showing an error tabs. + tabViewModels.values.filter(\.canBeBookmarked).count >= 2 + } + +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift index ab497e050b..372f25aa52 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift @@ -18,6 +18,7 @@ import Foundation import SwiftUI +import SwiftUIExtensions fileprivate extension View { func applyStepTitleAttributes() -> some View { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MultilineTextHeightFixer.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/MultilineTextHeightFixer.swift similarity index 98% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MultilineTextHeightFixer.swift rename to LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/MultilineTextHeightFixer.swift index 43b81b54c9..9a6e693e43 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/SwiftUI/MultilineTextHeightFixer.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/MultilineTextHeightFixer.swift @@ -39,7 +39,7 @@ private struct MultilineTextHeightFixer: ViewModifier { } } -extension View { +public extension View { /// Meant to be used for multiline-text. This is currently only applying a modifier /// diff --git a/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift b/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift new file mode 100644 index 0000000000..9a87f91090 --- /dev/null +++ b/UnitTests/Bookmarks/Helpers/WebsiteInfoHelpers.swift @@ -0,0 +1,35 @@ +// +// WebsiteInfoHelpers.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 +@testable import DuckDuckGo_Privacy_Browser + +extension WebsiteInfo { + + @MainActor + static func makeWebsitesInfo(url: URL, title: String? = nil, occurrences: Int = 1) -> [WebsiteInfo] { + (1...occurrences) + .map { _ in + let tab = Tab(content: .url(url, credential: nil, source: .ui)) + tab.title = title + return tab + } + .compactMap(WebsiteInfo.init) + } + +} diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index 97ce23202d..79d3809847 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -178,6 +178,87 @@ final class LocalBookmarkManagerTests: XCTestCase { XCTAssertNotNil(bookmarkList) } + func testWhenGetBookmarkFolderIsCalledThenAskBookmarkStoreToRetrieveFolder() throws { + // GIVEN + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + + // WHEN + _ = bookmarkManager.getBookmarkFolder(withId: #function) + + // THEN + XCTAssertTrue(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolderId, #function) + } + + func testWhenGetBookmarkFolderIsCalledAndFolderExistsInStoreThenBookmarkStoreReturnsFolder() throws { + // GIVEN + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + let folder = BookmarkFolder(id: "1", title: "Test") + bookmarkStoreMock.bookmarkFolder = folder + + // WHEN + let result = bookmarkManager.getBookmarkFolder(withId: #function) + + // THEN + XCTAssertEqual(result, folder) + } + + func testWhenGetBookmarkFolderIsCalledAndFolderDoesNotExistInStoreThenBookmarkStoreReturnsNil() throws { + // GIVEN + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + bookmarkStoreMock.bookmarkFolder = nil + + // WHEN + let result = bookmarkManager.getBookmarkFolder(withId: #function) + + // THEN + XCTAssertNil(result) + } + + // MARK: - Save Multiple Bookmarks at once + + func testWhenMakeBookmarksForWebsitesInfoIsCalledThenBookmarkStoreIsAskedToCreateMultipleBookmarks() { + // GIVEN + let (sut, bookmarkStoreMock) = LocalBookmarkManager.aManager + let newFolderName = #function + let websitesInfo = [ + WebsiteInfo(url: URL.duckDuckGo, title: "Website 1"), + WebsiteInfo(url: URL.duckDuckGo, title: "Website 2"), + WebsiteInfo(url: URL.duckDuckGo, title: "Website 3"), + WebsiteInfo(url: URL.duckDuckGo, title: "Website 4"), + ].compactMap { $0 } + XCTAssertFalse(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertNil(bookmarkStoreMock.capturedWebsitesInfo) + XCTAssertNil(bookmarkStoreMock.capturedNewFolderName) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.makeBookmarks(for: websitesInfo, inNewFolderNamed: newFolderName, withinParentFolder: .root) + + // THEN + XCTAssertTrue(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo?.count, 4) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo, websitesInfo) + XCTAssertEqual(bookmarkStoreMock.capturedNewFolderName, newFolderName) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testWhenMakeBookmarksForWebsiteInfoIsCalledThenReloadAllBookmarks() { + // GIVEN + let (sut, bookmarkStoreMock) = LocalBookmarkManager.aManager + bookmarkStoreMock.loadAllCalled = false // Reset after load all bookmarks the first time + XCTAssertFalse(bookmarkStoreMock.loadAllCalled) + let websitesInfo = [WebsiteInfo(url: URL.duckDuckGo, title: "Website 1")].compactMap { $0 } + + // WHEN + sut.makeBookmarks(for: websitesInfo, inNewFolderNamed: "Test", withinParentFolder: .root) + + // THEN + XCTAssertTrue(bookmarkStoreMock.loadAllCalled) + } + } fileprivate extension LocalBookmarkManager { @@ -204,3 +285,14 @@ fileprivate extension Bookmark { isFavorite: false) } + +private extension WebsiteInfo { + + @MainActor + init?(url: URL, title: String) { + let tab = Tab(content: .url(url, credential: nil, source: .ui)) + tab.title = title + self.init(tab) + } + +} diff --git a/UnitTests/Bookmarks/Model/WebsiteInfoTests.swift b/UnitTests/Bookmarks/Model/WebsiteInfoTests.swift new file mode 100644 index 0000000000..7b1b8e8196 --- /dev/null +++ b/UnitTests/Bookmarks/Model/WebsiteInfoTests.swift @@ -0,0 +1,77 @@ +// +// WebsiteInfoTests.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 +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class WebsiteInfoTests: XCTestCase { + + // MARK: - URL + + func testWhenInitWithTabThenSetURLWithTabURLValue() throws { + // GIVEN + let url = URL.duckDuckGo + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: url).first) + + // WHEN + let result = websiteInfo.url + + // THEN + XCTAssertEqual(result, url) + } + + // MARK: - Title + + func testWhenTitleIsNotNilThenDisplayTitleReturnsTitleValue() throws { + // GIVEN + let title = #function + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, title: title).first) + + // WHEN + let result = websiteInfo.title + + // THEN + XCTAssertEqual(result, title) + } + + func testWhenTitleIsNilAndURLConformsToRFC3986ThenDisplayTitleReturnsURLHost() throws { + // GIVEN + let url = URL.duckDuckGo + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, title: nil).first) + + // WHEN + let result = websiteInfo.title + + // THEN + XCTAssertEqual(result, url.host) + } + + func testWhenTitleIsNilAndURLDoesNotConformToRFC3986ThenDisplayTitleReturnsURLAbsoluteString() throws { + // GIVEN + let invalidURL = try XCTUnwrap(URL(string: "duckduckgo.com")) + let websiteInfo = try XCTUnwrap(WebsiteInfo.makeWebsitesInfo(url: invalidURL, title: nil).first) + + // WHEN + let result = websiteInfo.title + + // THEN + XCTAssertEqual(result, invalidURL.absoluteString) + } + +} diff --git a/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift b/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift new file mode 100644 index 0000000000..014282eff6 --- /dev/null +++ b/UnitTests/Bookmarks/Services/BookmarkFolderStoreMock.swift @@ -0,0 +1,25 @@ +// +// BookmarkFolderStoreMock.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 +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarkFolderStoreMock: BookmarkFoldersStore { + var lastBookmarkAllTabsFolderIdUsed: String? + +} diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 2ebff62e7d..73d2f96e8d 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -261,6 +261,134 @@ final class LocalBookmarkStoreTests: XCTestCase { waitForExpectations(timeout: 3, handler: nil) } + @MainActor + func testWhenSaveMultipleWebsiteInfoToANewFolderInRootFolder_ThenTheNewFolderIsCreated_AndBoomarksAreAddedToTheFolder() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let newFolderName = "Bookmark All Open Tabs" + let websites = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 50) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + var topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + XCTAssertEqual(bookmarksEntity.count, 0) + XCTAssertEqual(topLevelEntities.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: newFolderName, withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + let bookmarks = try XCTUnwrap(bookmarksEntity as? [Bookmark]) + let folders = try XCTUnwrap(topLevelEntities as? [BookmarkFolder]) + let folder = try XCTUnwrap(folders.first) + XCTAssertEqual(bookmarksEntity.count, 50) + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folder.parentFolderUUID, BookmarkEntity.Constants.rootFolderID) + XCTAssertEqual(folder.title, newFolderName) + XCTAssertEqual(Set(folder.children), Set(bookmarks)) + bookmarks.forEach { bookmark in + XCTAssertEqual(bookmark.parentFolderUUID, folder.id) + } + } + + @MainActor + func testWhenSaveMultipleWebsiteInfoToANewFolderInSubfolder_ThenTheNewFolderIsCreated_AndBoomarksAreAddedToTheFolder() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let newFolderName = "Bookmark All Open Tabs" + let websites = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 50) + let parentFolderToInsert = BookmarkFolder(id: "ABCDE", title: "Subfolder") + _ = await sut.save(folder: parentFolderToInsert, parent: nil) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + var topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + XCTAssertEqual(bookmarksEntity.count, 0) + XCTAssertEqual(topLevelEntities.count, 1) + XCTAssertEqual(topLevelEntities.first, parentFolderToInsert) + XCTAssertEqual((topLevelEntities.first as? BookmarkFolder)?.parentFolderUUID, BookmarkEntity.Constants.rootFolderID) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: newFolderName, withinParentFolder: .parent(uuid: parentFolderToInsert.id)) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + topLevelEntities = try await sut.loadAll(type: .topLevelEntities).get() + let bookmarks = try XCTUnwrap(bookmarksEntity as? [Bookmark]) + let folders = try XCTUnwrap(topLevelEntities as? [BookmarkFolder]) + let parentFolder = try XCTUnwrap(folders.first) + let subFolder = try XCTUnwrap(parentFolder.children.first as? BookmarkFolder) + XCTAssertEqual(bookmarksEntity.count, 50) + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(parentFolder.title, parentFolderToInsert.title) + XCTAssertEqual(parentFolder.children.count, 1) + XCTAssertEqual(subFolder.title, newFolderName) + XCTAssertEqual(Set(subFolder.children), Set(bookmarks)) + bookmarks.forEach { bookmark in + XCTAssertEqual(bookmark.parentFolderUUID, subFolder.id) + } + } + + @MainActor + func testWhenSaveMultipleWebsiteInfo_AndTitleIsNotNil_ThenTitleIsUsedAsBookmarkTitle() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let websiteName = "Test Website" + let websites = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, title: websiteName, occurrences: 1) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + XCTAssertEqual(bookmarksEntity.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: "Saved Tabs", withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + let bookmark = try XCTUnwrap((bookmarksEntity as? [Bookmark])?.first) + XCTAssertEqual(bookmarksEntity.count, 1) + XCTAssertEqual(bookmark.title, websiteName) + } + + @MainActor + func testWhenSaveMultipleWebsiteInfo_AndTitleIsNil_ThenURLDomainIsUsedAsBookmarkTitle() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let url = URL.duckDuckGo + let websites = WebsiteInfo.makeWebsitesInfo(url: url, title: nil, occurrences: 1) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + XCTAssertEqual(bookmarksEntity.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: "Saved Tabs", withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + let bookmark = try XCTUnwrap((bookmarksEntity as? [Bookmark])?.first) + XCTAssertEqual(bookmarksEntity.count, 1) + XCTAssertEqual(bookmark.title, url.host) + } + + @MainActor + func testWhenSaveMultipleWebsiteInfo_AndTitleIsNil_AndURLDoesNotConformToRFC3986_ThenURLAbsoluteStringIsUsedAsBookmarkTitle() async throws { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let url = try XCTUnwrap(URL(string: "duckduckgo.com")) + let websites = WebsiteInfo.makeWebsitesInfo(url: url, title: nil, occurrences: 1) + var bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + XCTAssertEqual(bookmarksEntity.count, 0) + + // WHEN + sut.saveBookmarks(for: websites, inNewFolderNamed: "Saved Tabs", withinParentFolder: .root) + + // THEN + bookmarksEntity = try await sut.loadAll(type: .bookmarks).get() + let bookmark = try XCTUnwrap((bookmarksEntity as? [Bookmark])?.first) + XCTAssertEqual(bookmarksEntity.count, 1) + XCTAssertEqual(bookmark.title, url.absoluteString) + } + // MARK: Moving Bookmarks/Folders func testWhenMovingBookmarkWithinParentCollection_AndIndexIsValid_ThenBookmarkIsMoved() async { @@ -1071,6 +1199,63 @@ final class LocalBookmarkStoreTests: XCTestCase { } } + // MARK: - Retrieve Bookmark Folder + + func testWhenFetchingBookmarkFolderWithId_AndFolderExist_ThenFolderIsReturned() async { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let folderId = "ABCDE" + let folder = BookmarkFolder(id: folderId, title: "Test") + _ = await sut.save(folder: folder, parent: nil) + + // WHEN + let result = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertEqual(result, folder) + } + + func testWhenFetchingBookmarkFolderWithId_AndFolderDoesNotExist_ThenNilIsReturned() { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let folderId = "ABCDE" + + // WHEN + let result = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertNil(result) + } + + func testWhenFetchingBookmarkFolderWithId_AndFolderHasBeenMoved_ThenFolderIsStillReturned() async { + // GIVEN + let context = container.viewContext + let sut = LocalBookmarkStore(context: context) + let folderId = "ABCDE" + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Test") + let folder2 = BookmarkFolder(id: folderId, title: "Test") + let expectedFolder = BookmarkFolder(id: folderId, title: "Test", parentFolderUUID: folder1.id) + _ = await sut.save(folder: folder1, parent: nil) + _ = await sut.save(folder: folder2, parent: nil) + + // WHEN + let firstFetchResult = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertEqual(firstFetchResult, folder2) + + // Move folder + _ = await sut.move(objectUUIDs: [folder2.id], toIndex: nil, withinParentFolder: .parent(uuid: folder1.id)) + + // WHEN + let secondFetchResult = sut.bookmarkFolder(withId: folderId) + + // THEN + XCTAssertEqual(secondFetchResult, expectedFolder) + } + // MARK: Import func testWhenBookmarksAreImported_AndNoDuplicatesExist_ThenBookmarksAreImported() { diff --git a/UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift b/UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift new file mode 100644 index 0000000000..7020d8ec28 --- /dev/null +++ b/UnitTests/Bookmarks/Services/UserDefaultsBookmarkFoldersStoreTests.swift @@ -0,0 +1,62 @@ +// +// UserDefaultsBookmarkFoldersStoreTests.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 +@testable import DuckDuckGo_Privacy_Browser + +final class UserDefaultsBookmarkFoldersStoreTests: XCTestCase { + private static let suiteName = "testing_bookmark_folders_store" + private var userDefaults: UserDefaults! + private var sut: UserDefaultsBookmarkFoldersStore! + + override func setUpWithError() throws { + try super.setUpWithError() + userDefaults = UserDefaults(suiteName: Self.suiteName) + sut = UserDefaultsBookmarkFoldersStore(userDefaults: userDefaults) + } + + override func tearDownWithError() throws { + userDefaults.removePersistentDomain(forName: Self.suiteName) + userDefaults = nil + sut = nil + try super.tearDownWithError() + } + + func testReturnBookmarkAllTabsLastFolderIdUsedWhenUserDefaultsContainsValue() { + // GIVEN + let value = "12345" + userDefaults.set(value, forKey: UserDefaultsBookmarkFoldersStore.Keys.bookmarkAllTabsFolderUsedKey) + + // WHEN + let result = sut.lastBookmarkAllTabsFolderIdUsed + + // THEN + XCTAssertEqual(result, value) + } + + func testReturnNilForBookmarkAllTabsLastFolderIdUsedWhenUserDefaultsDoesNotContainValue() { + // GIVEN + userDefaults.set(nil, forKey: UserDefaultsBookmarkFoldersStore.Keys.bookmarkAllTabsFolderUsedKey) + + // WHEN + let result = sut.lastBookmarkAllTabsFolderIdUsed + + // THEN + XCTAssertNil(result) + } +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift index 53072513c4..c8b101e148 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift @@ -125,46 +125,44 @@ final class AddEditBookmarkDialogCoordinatorViewModelTests: XCTestCase { XCTAssertEqual(bookmarkViewModelMock.selectedFolder, folder) } -} + // MARK: - Integration Test -final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { - var bookmarkName: String = "" - var bookmarkURLPath: String = "" - var isBookmarkFavorite: Bool = false - var isURLFieldHidden: Bool = false - var title: String = "" - var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] - var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { - didSet { - selectedFolderExpectation?.fulfill() + func testWhenAddFolderMultipleTimesThenFolderListIsUpdatedAndSelectedFolderIsNil() { + // GIVEN + let expectation = self.expectation(description: #function) + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkViewModelMock.selectedFolder = folder + let bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [folder] + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + let folderModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let sut = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModelMock, folderModel: folderModel) + let c = folderModel.$folders + .dropFirst(2) // Not interested in the first two events. 1.subscribing to $folders and 2. subscribing to $list. + .sink { folders in + expectation.fulfill() } - } - var cancelActionTitle: String = "" - var isOtherActionDisabled: Bool = false - var defaultActionTitle: String = "" - var isDefaultActionDisabled: Bool = false - func cancel(dismiss: () -> Void) {} - func addOrSave(dismiss: () -> Void) {} + XCTAssertNil(folderModel.selectedFolder) - var selectedFolderExpectation: XCTestExpectation? -} + // Tap Add Folder + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .folder) + XCTAssertTrue(folderModel.folderName.isEmpty) + XCTAssertEqual(folderModel.selectedFolder, folder) -final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { - let subject = PassthroughSubject() + // Create a new folder + folderModel.folderName = #function + folderModel.addOrSave {} + + // Add folder again + sut.addFolderAction() - var addFolderPublisher: AnyPublisher { - subject.eraseToAnyPublisher() + // THEN + withExtendedLifetime(c) {} + waitForExpectations(timeout: 1.0) + XCTAssertEqual(sut.viewState, .folder) + XCTAssertTrue(folderModel.folderName.isEmpty) } - var folderName: String = "" - var title: String = "" - var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] - var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? - var cancelActionTitle: String = "" - var isOtherActionDisabled: Bool = false - var defaultActionTitle: String = "" - var isDefaultActionDisabled: Bool = false - - func cancel(dismiss: () -> Void) {} - func addOrSave(dismiss: () -> Void) {} } diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift index b409df5b9c..615777c7d9 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -119,25 +119,61 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertTrue(result.isEmpty) } - func testShouldSetNameAndURLToValueWhenInitModeIsAddTabInfoIsNotNilAndURLIsNotAlreadyBookmarked() { + func testWhenInitModeIsAddAndTabInfoIsNotNilAndURLIsNotAlreadyBookmarkedThenSetURLToValue() { // GIVEN let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) // WHEN - let name = sut.bookmarkName let url = sut.bookmarkURLPath // THEN - XCTAssertEqual(name, "Test") XCTAssertEqual(url, URL.duckDuckGo.absoluteString) } + func testWhenInitAndModeIsAddAndTabInfoTitleIsNotNilAndURLIsNotAlreadyBookmarkedThenSetBookmarkNameToTitle() { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + + // THEN + XCTAssertEqual(name, "Test") + } + + func testWhenInitAndModeIsAddAndTabInfoTitleIsNilAndURLIsNotAlreadyBookmarkedThenSetBookmarkNameToURLDomain() { + // GIVEN + let url = URL.duckDuckGo + let tab = Tab(content: .url(url, source: .link), title: nil) + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + + // THEN + XCTAssertEqual(name, url.host) + } + + func testWhenInitAndModeIsAddAndTabInfoTitleIsNilAndURLDoesNotConformToRFC3986AndURLIsNotAlreadyBookmarkedThenSetBookmarkNameToURLAbsoluteString() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "duckduckgo.com")) + let tab = Tab(content: .url(url, source: .link), title: nil) + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + + // THEN + XCTAssertEqual(name, url.absoluteString) + } + func testShouldSetNameAndURLToEmptyWhenInitModeIsAddTabInfoIsNotNilAndURLIsAlreadyBookmarked() throws { // GIVEN let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") let websiteInfo = try XCTUnwrap(WebsiteInfo(tab)) - let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title ?? "", isFavorite: false) + let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title, isFavorite: false) bookmarkStoreMock.bookmarks = [bookmark] bookmarkManager.loadBookmarks() let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift index a48f18d2ab..6d54257048 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -329,7 +329,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { // THEN XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) XCTAssertTrue(bookmarkStoreMock.saveFolderCalled) - XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, #function) XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) } @@ -346,7 +346,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { // THEN XCTAssertTrue(bookmarkStoreMock.updateFolderCalled) - XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, "TEST") } func testShouldNotAskBookmarkStoreToUpdateFolderWhenNameIsNotChanged() { diff --git a/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift new file mode 100644 index 0000000000..d766d27f0d --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogCoordinatorViewModelTests.swift @@ -0,0 +1,171 @@ +// +// BookmarkAllTabsDialogCoordinatorViewModelTests.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 Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class BookmarkAllTabsDialogCoordinatorViewModelTests: XCTestCase { + private var sut: BookmarkAllTabsDialogCoordinatorViewModel! + private var bookmarkAllTabsViewModelMock: BookmarkAllTabsDialogViewModelMock! + private var bookmarkFolderViewModelMock: AddEditBookmarkFolderDialogViewModelMock! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + bookmarkAllTabsViewModelMock = .init() + bookmarkFolderViewModelMock = .init() + sut = .init(bookmarkModel: bookmarkAllTabsViewModelMock, folderModel: bookmarkFolderViewModelMock) + } + + override func tearDownWithError() throws { + cancellables = nil + bookmarkAllTabsViewModelMock = nil + bookmarkFolderViewModelMock = nil + sut = nil + try super.tearDownWithError() + } + + func testWhenInitThenViewStateIsBookmarkAllTabs() { + XCTAssertEqual(sut.viewState, .bookmarkAllTabs) + } + + func testWhenDismissActionIsCalledThenViewStateIsBookmarkAllTabs() { + // GIVEN + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .addFolder) + + // WHEN + sut.dismissAction() + + // THEN + XCTAssertEqual(sut.viewState, .bookmarkAllTabs) + + } + + func testWhenAddFolderActionIsCalledThenSetSelectedFolderOnFolderViewModelIsCalledAndReturnAddFolderViewState() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkAllTabsViewModelMock.selectedFolder = folder + XCTAssertNil(bookmarkFolderViewModelMock.selectedFolder) + XCTAssertEqual(sut.viewState, .bookmarkAllTabs) + + // WHEN + sut.addFolderAction() + + // THEN + XCTAssertEqual(bookmarkFolderViewModelMock.selectedFolder, folder) + XCTAssertEqual(sut.viewState, .addFolder) + } + + func testWhenBookmarkModelChangesThenReceiveEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.bookmarkModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testWhenBookmarkFolderModelChangesThenReceiveEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.folderModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testWhenAddFolderPublisherSendsEventThenSelectedFolderOnBookmarkAllTabsViewModelIsSet() { + // GIVEN + let expectation = self.expectation(description: #function) + bookmarkAllTabsViewModelMock.selectedFolderExpectation = expectation + let folder = BookmarkFolder(id: "ABCDE", title: #function) + XCTAssertNil(bookmarkAllTabsViewModelMock.selectedFolder) + + // WHEN + sut.folderModel.subject.send(folder) + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertEqual(bookmarkAllTabsViewModelMock.selectedFolder, folder) + } + + // MARK: - Integration Test + + func testWhenAddFolderMultipleTimesThenFolderListIsUpdatedAndSelectedFolderIsNil() { + // GIVEN + let expectation = self.expectation(description: #function) + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkAllTabsViewModelMock.selectedFolder = folder + let bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [folder] + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + let folderModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let sut = BookmarkAllTabsDialogCoordinatorViewModel(bookmarkModel: bookmarkAllTabsViewModelMock, folderModel: folderModel) + let c = folderModel.$folders + .dropFirst(2) // Not interested in the first two events. 1.subscribing to $folders and 2. subscribing to $list. + .sink { folders in + expectation.fulfill() + } + + XCTAssertNil(folderModel.selectedFolder) + + // Tap Add Folder + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .addFolder) + XCTAssertTrue(folderModel.folderName.isEmpty) + XCTAssertEqual(folderModel.selectedFolder, folder) + + // Create a new folder + folderModel.folderName = #function + folderModel.addOrSave {} + + // Add folder again + sut.addFolderAction() + + // THEN + withExtendedLifetime(c) {} + waitForExpectations(timeout: 1.0) + XCTAssertEqual(sut.viewState, .addFolder) + XCTAssertTrue(folderModel.folderName.isEmpty) + } + +} diff --git a/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift new file mode 100644 index 0000000000..dd05e42238 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/BookmarkAllTabsDialogViewModelTests.swift @@ -0,0 +1,382 @@ +// +// BookmarkAllTabsDialogViewModelTests.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 +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class BookmarkAllTabsDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + private var foldersStoreMock: BookmarkFolderStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + foldersStoreMock = .init() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + foldersStoreMock = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testWhenTitleIsCalledThenItReflectsThenNumberOfWebsites() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 10) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, String(format: UserText.Bookmarks.Dialog.Title.bookmarkOpenTabs, websitesInfo.count)) + } + + func testWhenCancelActionTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testWhenEducationalMessageIsCalledThenItReturnsTheRightMessage() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.educationalMessage + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Message.bookmarkOpenTabsEducational) + } + + func testWhenDefaultActionTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addAllBookmarks) + } + + func testWhenFolderNameFieldTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.folderNameFieldTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Field.folderName) + } + + func testWhenLocationFieldTitleIsCalledThenItReturnsTheRightTitle() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.locationFieldTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Field.location) + } + + // MARK: - State + + func testWhenInitThenFolderNameIsSetToCurrentDateAndNumberOfWebsites() throws { + // GIVEN + let date = Date(timeIntervalSince1970: 1712902304) // 12th of April 2024 + let gmtTimeZone = try XCTUnwrap(TimeZone(identifier: "GMT")) + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 5) + let sut = BookmarkAllTabsDialogViewModel( + websites: websitesInfo, + foldersStore: foldersStoreMock, + bookmarkManager: bookmarkManager, + dateFormatterConfigurationProvider: { + BookmarkAllTabsDialogViewModel.DateFormatterConfiguration(date: date, timeZone: gmtTimeZone) + } + ) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, String(format: UserText.Bookmarks.Dialog.Value.folderName, "2024-04-12", websitesInfo.count)) + } + + func testWhenInitAndTimeZoneIsPDTThenFolderNameIsSetToCurrentDateAndNumberOfWebsites() throws { + // GIVEN + let date = Date(timeIntervalSince1970: 1712902304) // 12th of April 2024 (GMT) + let pdtTimeZone = try XCTUnwrap(TimeZone(identifier: "America/Los_Angeles")) + let expectedDate = "2024-04-11" // Expected date in PDT TimeZone + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo, occurrences: 5) + let sut = BookmarkAllTabsDialogViewModel( + websites: websitesInfo, + foldersStore: foldersStoreMock, + bookmarkManager: bookmarkManager, + dateFormatterConfigurationProvider: { + BookmarkAllTabsDialogViewModel.DateFormatterConfiguration(date: date, timeZone: pdtTimeZone) + } + ) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, String(format: UserText.Bookmarks.Dialog.Value.folderName, expectedDate, websitesInfo.count)) + } + + func testWhenInitThenFoldersAreSetFromBookmarkList() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testWhenInitAndFoldersStoreLastUsedFolderIsNilThenDoNotAskBookmarkStoreForBookmarkFolder() { + // GIVEN + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = nil + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + + // WHEN + _ = BookmarkAllTabsDialogViewModel(websites: WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo), foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // THEN + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + } + + func testWhenInitAndFoldersStoreLastUsedFolderIsNotNilThenAskBookmarkStoreForBookmarkFolder() { + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = "1ABCDE" + XCTAssertFalse(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolderId) + + // WHEN + _ = BookmarkAllTabsDialogViewModel(websites: WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo), foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // THEN + XCTAssertTrue(bookmarkStoreMock.bookmarkFolderWithIdCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolderId, "1ABCDE") + } + + func testWhenFoldersStoreLastUsedFolderIsNotNilAndBookmarkStoreDoesNotContainFolderThenSelectedFolderIsNil() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = "1" + bookmarkStoreMock.bookmarkFolder = nil + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testWhenFoldersStoreLastUsedFolderIsNotNilThenSelectedFolderIsNotNil() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + foldersStoreMock.lastBookmarkAllTabsFolderIdUsed = "1" + bookmarkStoreMock.bookmarkFolder = folder + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testWhenFolderIsAddedThenFoldersListIsRefreshed() { + // GIVEN + let expectation = self.expectation(description: #function) + expectation.expectedFulfillmentCount = 2 + let folder = BookmarkFolder(id: "1", title: #function) + let folder2 = BookmarkFolder(id: "2", title: "Test") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + XCTAssertEqual(sut.folders.count, 1) + XCTAssertEqual(sut.folders.first?.entity, folder) + + // Simulate Bookmark store changing data set + bookmarkStoreMock.bookmarks = [folder, folder2] + var expectedFolder: [BookmarkFolder] = [] + let c = sut.$folders + .dropFirst() + .sink { folders in + expectedFolder = folders.map(\.entity) + expectation.fulfill() + } + + // WHEN + bookmarkManager.loadBookmarks() + + // THEN + withExtendedLifetime(c) {} + waitForExpectations(timeout: 1.0) + XCTAssertEqual(expectedFolder.count, 2) + XCTAssertEqual(expectedFolder.first, folder) + XCTAssertEqual(expectedFolder.last, folder2) + } + + // MARK: - Actions + + func testWhenIsOtherActionDisabledCalledThenReturnFalse() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testWhenFolderNameIsEmptyDefaultActionIsDisabled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testWhenFolderNameIsNotEmptyDefaultActionIsEnabled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.folderName = "TEST" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testWhenCancelIsCalledThenDismissIsCalled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testWhenAddOrSaveIsCalledAndSelectedFolderIsNilThenBookmarkStoreIsAskedToBookmarkWebsitesInfoInRootFolder() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertNil(bookmarkStoreMock.capturedWebsitesInfo) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave(dismiss: {}) + + // THEN + XCTAssertTrue(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo, websitesInfo) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + + } + + func testWhenAddOrSaveIsCalledAndSelectedFolderIsNotNilThenBookmarkStoreIsAskedToBookmarkWebsitesInfoNotInRootFolder() { + // GIVEN + let folder = BookmarkFolder(id: "ABCDE", title: "Saved Tabs") + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertNil(bookmarkStoreMock.capturedWebsitesInfo) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave(dismiss: {}) + + // THEN + XCTAssertTrue(bookmarkStoreMock.saveBookmarksInNewFolderNamedCalled) + XCTAssertEqual(bookmarkStoreMock.capturedWebsitesInfo, websitesInfo) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: "ABCDE")) + } + + func testWhenAddOrSaveIsCalledThenDismissIsCalled() { + // GIVEN + let websitesInfo = WebsiteInfo.makeWebsitesInfo(url: .duckDuckGo) + let sut = BookmarkAllTabsDialogViewModel(websites: websitesInfo, foldersStore: foldersStoreMock, bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + +} diff --git a/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift new file mode 100644 index 0000000000..6cd4175ac6 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkDialogViewModelMock.swift @@ -0,0 +1,43 @@ +// +// AddEditBookmarkDialogViewModelMock.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 +@testable import DuckDuckGo_Privacy_Browser + +final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { + var bookmarkName: String = "" + var bookmarkURLPath: String = "" + var isBookmarkFavorite: Bool = false + var isURLFieldHidden: Bool = false + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} diff --git a/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift new file mode 100644 index 0000000000..99317bf853 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/Mocks/AddEditBookmarkFolderDialogViewModelMock.swift @@ -0,0 +1,40 @@ +// +// AddEditBookmarkFolderDialogViewModelMock.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 Combine +@testable import DuckDuckGo_Privacy_Browser + +final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { + let subject = PassthroughSubject() + + var addFolderPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var folderName: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} +} diff --git a/UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift b/UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift new file mode 100644 index 0000000000..3d9e9ba3e7 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/Mocks/BookmarkAllTabsDialogViewModelMock.swift @@ -0,0 +1,43 @@ +// +// BookmarkAllTabsDialogViewModelMock.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 +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarkAllTabsDialogViewModelMock: BookmarkAllTabsDialogEditing { + var folderName: String = "" + var educationalMessage: String = "" + var folderNameFieldTitle: String = "" + var locationFieldTitle: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = true + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index 28f65fdf59..d1fafb18be 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -41,6 +41,10 @@ class MockBookmarkManager: BookmarkManager { return nil } + func getBookmarkFolder(withId id: String) -> DuckDuckGo_Privacy_Browser.BookmarkFolder? { + return nil + } + func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> DuckDuckGo_Privacy_Browser.Bookmark? { return nil } @@ -49,6 +53,8 @@ class MockBookmarkManager: BookmarkManager { return nil } + func makeBookmarks(for websitesInfo: [DuckDuckGo_Privacy_Browser.WebsiteInfo], inNewFolderNamed folderName: String, withinParentFolder parent: DuckDuckGo_Privacy_Browser.ParentFolderType) {} + func makeFolder(for title: String, parent: DuckDuckGo_Privacy_Browser.BookmarkFolder?, completion: (DuckDuckGo_Privacy_Browser.BookmarkFolder) -> Void) {} func remove(bookmark: DuckDuckGo_Privacy_Browser.Bookmark) {} diff --git a/UnitTests/Menus/MainMenuTests.swift b/UnitTests/Menus/MainMenuTests.swift index 44ec9cdb11..b3b5f4275e 100644 --- a/UnitTests/Menus/MainMenuTests.swift +++ b/UnitTests/Menus/MainMenuTests.swift @@ -18,6 +18,7 @@ import XCTest import Combine +import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser class MainMenuTests: XCTestCase { @@ -87,4 +88,26 @@ class MainMenuTests: XCTestCase { XCTAssertEqual(manager.reopenLastClosedMenuItem?.keyEquivalent, ReopenMenuItemKeyEquivalentManager.Const.keyEquivalent) XCTAssertEqual(manager.reopenLastClosedMenuItem?.keyEquivalentModifierMask, ReopenMenuItemKeyEquivalentManager.Const.modifierMask) } + + // MARK: - Bookmarks + + @MainActor + func testWhenBookmarksMenuIsInitialized_ThenSecondItemIsBookmarkAllTabs() throws { + // GIVEN + let sut = MainMenu(featureFlagger: DummyFeatureFlagger(), bookmarkManager: MockBookmarkManager(), faviconManager: FaviconManagerMock(), copyHandler: CopyHandler()) + let bookmarksMenu = try XCTUnwrap(sut.item(withTitle: UserText.bookmarks)) + + // WHEN + let result = try XCTUnwrap(bookmarksMenu.submenu?.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertEqual(result.keyEquivalent, "d") + XCTAssertEqual(result.keyEquivalentModifierMask, [.command, .shift]) + } +} + +private class DummyFeatureFlagger: FeatureFlagger { + func isFeatureOn(forProvider: F) -> Bool { + false + } } diff --git a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift index e37582c4fa..05e9c206e7 100644 --- a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift +++ b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift @@ -23,6 +23,7 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { var optionsButtonMenuRequestedPreferencesCalled = false var optionsButtonMenuRequestedAppearancePreferencesCalled = false + var optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = false func optionsButtonMenuRequestedDataBrokerProtection(_ menu: NSMenu) { @@ -36,6 +37,10 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { } + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) { + optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = true + } + func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { } diff --git a/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift b/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift new file mode 100644 index 0000000000..3888c658a1 --- /dev/null +++ b/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift @@ -0,0 +1,61 @@ +// +// MoreOptionsMenu+BookmarksTests.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 +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class MoreOptionsMenu_BookmarksTests: XCTestCase { + + func testWhenBookmarkSubmenuIsInitThenBookmarkAllTabsKeyIsCmdShiftD() throws { + // GIVEN + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init()) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertEqual(result.keyEquivalent, "d") + XCTAssertEqual(result.keyEquivalentModifierMask, [.command, .shift]) + } + + func testWhenTabCollectionCanBookmarkAllTabsThenBookmarkAllTabsMenuItemIsEnabled() throws { + // GIVEN + let tab1 = Tab(content: .url(.duckDuckGo, credential: nil, source: .ui)) + let tab2 = Tab(content: .url(.duckDuckGoEmail, credential: nil, source: .ui)) + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init(tabCollection: .init(tabs: [tab1, tab2]))) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertTrue(result.isEnabled) + } + + func testWhenTabCollectionCannotBookmarkAllTabsThenBookmarkAllTabsMenuItemIsDisabled() throws { + // GIVEN + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init(tabCollection: .init(tabs: []))) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertFalse(result.isEnabled) + } + +} diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 4b9994cb5e..209701e87a 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -155,6 +155,23 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedPreferencesCalled) } + // MARK: - Bookmarks + + @MainActor + func testWhenClickingOnBookmarkAllTabsMenuItemThenTheActionDelegateIsAlerted() throws { + // GIVEN + let bookmarksMenu = try XCTUnwrap(moreOptionMenu.item(at: 8)?.submenu) + let bookmarkAllTabsIndex = try XCTUnwrap(bookmarksMenu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) + let bookmarkAllTabsMenuItem = try XCTUnwrap(bookmarksMenu.items[bookmarkAllTabsIndex]) + bookmarkAllTabsMenuItem.isEnabled = true + + // WHEN + bookmarksMenu.performActionForItem(at: bookmarkAllTabsIndex) + + // THEN + XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedBookmarkAllOpenTabsCalled) + } + } final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index e63dad9b0a..9157000830 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -23,9 +23,12 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { var mockedCurrentTab: Tab? + var canBookmarkAllOpenTabs = false var hasItemsToTheRight = false var audioState: WKWebView.AudioState? + private(set) var tabBarViewItemBookmarkAllOpenTabsActionCalled = false + func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { } @@ -70,6 +73,14 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { + canBookmarkAllOpenTabs + } + + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { + tabBarViewItemBookmarkAllOpenTabsActionCalled = true + } + func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 208d73a29e..1c2c574b70 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -45,31 +45,33 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 0)?.title, UserText.duplicateTab) XCTAssertEqual(menu.item(at: 1)?.title, UserText.pinTab) XCTAssertTrue(menu.item(at: 2)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 3)?.title, UserText.bookmarkThisPage) - XCTAssertEqual(menu.item(at: 4)?.title, UserText.fireproofSite) + XCTAssertEqual(menu.item(at: 3)?.title, UserText.fireproofSite) + XCTAssertEqual(menu.item(at: 4)?.title, UserText.bookmarkThisPage) XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.closeTab) - XCTAssertEqual(menu.item(at: 7)?.title, UserText.closeOtherTabs) - XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTabsToTheRight) - XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.bookmarkAllTabs) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTab) + XCTAssertEqual(menu.item(at: 9)?.title, UserText.closeOtherTabs) + XCTAssertEqual(menu.item(at: 10)?.title, UserText.closeTabsToTheRight) + XCTAssertEqual(menu.item(at: 11)?.title, UserText.moveTabToNewWindow) } func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { delegate.audioState = .unmuted tabBarViewItem.menuNeedsUpdate(menu) - XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) - XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertFalse(menu.item(at: 1)?.isSeparatorItem ?? true) + XCTAssertEqual(menu.item(at: 2)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 3)?.isSeparatorItem ?? false) } func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { delegate.audioState = .muted tabBarViewItem.menuNeedsUpdate(menu) - XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) - XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertFalse(menu.item(at: 1)?.isSeparatorItem ?? true) + XCTAssertEqual(menu.item(at: 2)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 3)?.isSeparatorItem ?? false) } func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { @@ -193,4 +195,42 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertFalse(bookmarkItem?.isEnabled ?? true) } + func testWhenCanBookmarkAllOpenTabsThenBookmarkAllOpenTabsItemIsEnabled() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = true + tabBarViewItem.menuNeedsUpdate(menu) + + // WHEN + let item = try XCTUnwrap(menu.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertTrue(item.isEnabled) + } + + func testWhenCannotBookmarkAllOpenTabsThenBookmarkAllOpenTabsItemIsDisabled() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = false + tabBarViewItem.menuNeedsUpdate(menu) + + // WHEN + let item = try XCTUnwrap(menu.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertFalse(item.isEnabled) + } + + func testWhenClickingOnBookmarkAllTabsThenTheActionDelegateIsNotified() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = true + tabBarViewItem.menuNeedsUpdate(menu) + let index = try XCTUnwrap(menu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) + XCTAssertFalse(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) + + // WHEN + menu.performActionForItem(at: index) + + // THEN + XCTAssertTrue(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) + } + } diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift index efc39feac0..3415fd4439 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift @@ -425,6 +425,79 @@ final class TabCollectionViewModelTests: XCTestCase { XCTAssertEqual(events.count, 1) XCTAssertIdentical(events[0], tabCollectionViewModel.selectedTabViewModel) } + + // MARK: - Bookmark All Open Tabs + + func testWhenOneEmptyTabOpenThenCanBookmarkAllOpenTabsIsFalse() throws { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + let firstTabViewModel = try XCTUnwrap(sut.tabViewModel(at: 0)) + XCTAssertEqual(sut.tabViewModels.count, 1) + XCTAssertEqual(firstTabViewModel.tabContent, .newtab) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenOneURLTabOpenThenCanBookmarkAllOpenTabsIsFalse() throws { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + sut.replaceTab(at: .unpinned(0), with: .init(content: .url(.duckDuckGo, credential: nil, source: .ui))) + let firstTabViewModel = try XCTUnwrap(sut.tabViewModel(at: 0)) + XCTAssertEqual(sut.tabViewModels.count, 1) + XCTAssertEqual(firstTabViewModel.tabContent, .url(.duckDuckGo, credential: nil, source: .ui)) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenOneURLTabAndOnePinnedTabOpenThenCanBookmarkAllOpenTabsIsFalse() { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + sut.replaceTab(at: .unpinned(0), with: .init(content: .url(.duckDuckGo, credential: nil, source: .ui))) + sut.append(tab: .init(content: .url(.duckDuckGoEmail, credential: nil, source: .ui))) + sut.pinTab(at: 0) + XCTAssertEqual(sut.pinnedTabs.count, 1) + XCTAssertEqual(sut.tabViewModels.count, 1) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenAtLeastTwoURLTabsOpenThenCanBookmarkAllOpenTabsIsTrue() { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + let pinnedTab = Tab(content: .url(.aboutDuckDuckGo, credential: nil, source: .ui)) + sut.append(tabs: [ + pinnedTab, + .init(content: .url(.duckDuckGo, credential: nil, source: .ui)), + .init(content: .newtab), + .init(content: .bookmarks), + .init(content: .anySettingsPane), + .init(content: .url(.duckDuckGoEmail, credential: nil, source: .ui)), + ]) + sut.pinTab(at: 1) + XCTAssertEqual(sut.pinnedTabs.count, 1) + XCTAssertEqual(sut.tabViewModels.count, 6) + XCTAssertEqual(sut.pinnedTabs.first, pinnedTab) + XCTAssertNil(sut.tabViewModels[pinnedTab]) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertTrue(result) + } + } fileprivate extension TabCollectionViewModel { From 410d1a36c8ad1b62a62863ccb69cd0f56496babb Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 24 Apr 2024 15:18:34 +0100 Subject: [PATCH 016/134] DBP interrupt fixes for release (#2696) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207148631921399/f https://app.asana.com/0/1199230911884351/1207152863082294/f Tech Design URL: CC: **Description**: Combines https://github.com/duckduckgo/macos-browser/pull/2685 and https://github.com/duckduckgo/macos-browser/pull/2690 to merge them into an internal release **Steps to test this PR**: 1. Follow the steps in the two individual PRs --- [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) --- .../Scheduler/DataBrokerProtectionProcessor.swift | 14 +++++++++----- .../Scheduler/DataBrokerProtectionScheduler.swift | 4 +++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 0ba21a21f7..01df6e5e39 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -70,10 +70,11 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runAllScanOperations(pendingCompletion: completion) runOperations(operationType: .scan, priorityDate: nil, - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Scans done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) - self.calculateMisMatches() + self?.calculateMisMatches() } } @@ -88,8 +89,9 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runAllOptOutOperations(pendingCompletion: completion) runOperations(operationType: .optOut, priorityDate: nil, - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Optouts done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } @@ -100,8 +102,9 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runQueuedOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: Date(), - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Queued operations done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } @@ -112,8 +115,9 @@ final class DataBrokerProtectionProcessor { currentlyRunningOperationsForFunction = .runAllOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: nil, - showWebView: showWebView) { errors in + showWebView: showWebView) { [weak self] errors in os_log("Queued operations done", log: .dataBrokerProtection) + self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index d5f9083d07..0a0f529a9f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -261,7 +261,9 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch self.startScheduler(showWebView: showWebView) - self.userNotificationService.sendFirstScanCompletedNotification() + if errors?.oneTimeError == nil { + self.userNotificationService.sendFirstScanCompletedNotification() + } if let hasMatches = try? self.dataManager.hasMatches(), hasMatches { From 73c0b1fa59e87cd889d7a93a6d156b369cb5a732 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 24 Apr 2024 14:59:47 +0000 Subject: [PATCH 017/134] Bump version to 1.85.0 (173) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 59f3d0e6e2..bf9c44d075 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 172 +CURRENT_PROJECT_VERSION = 173 From 0cbe8054f89668daf039d5c2f8fdd2a5f33dc6a7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 24 Apr 2024 22:02:03 +0600 Subject: [PATCH 018/134] Fix downloads not appearing (#2693) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207138015653734/f --- .../View/NavigationBarViewController.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index dc181f12b1..582a8b4f28 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -644,15 +644,20 @@ final class NavigationBarViewController: NSViewController { private func subscribeToDownloads() { downloadListCoordinator.updates + .filter { update in + // filter download completion events only + if case .updated(let oldValue) = update.kind, + oldValue.progress != nil && update.item.progress == nil { + return true + } else { + return false + } + } .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] update in guard let self else { return } - if case .updated(let oldValue) = update.kind, - DownloadsPreferences.shared.shouldOpenPopupOnCompletion, - update.item.destinationURL != nil, - update.item.tempURL == nil, - oldValue.tempURL != nil, // download finished + if DownloadsPreferences.shared.shouldOpenPopupOnCompletion, !update.item.isBurner, WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window { @@ -661,11 +666,11 @@ final class NavigationBarViewController: NSViewController { downloadsDelegate: self) } else if update.item.isBurner { invalidateDownloadButtonHidingTimer() - updateDownloadsButton(updatingFromPinnedViewsNotification: false) } updateDownloadsButton() } .store(in: &downloadsCancellables) + downloadListCoordinator.progress.publisher(for: \.totalUnitCount) .combineLatest(downloadListCoordinator.progress.publisher(for: \.completedUnitCount)) .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) From a2ca57f05793f4bd40a5b2e51f7aecc97976481d Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 24 Apr 2024 20:02:43 +0100 Subject: [PATCH 019/134] Revert interrupt logic (#2699) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207162593153246/f Tech Design URL: CC: **Description**: Same as https://github.com/duckduckgo/macos-browser/pull/2698 but for the internal release **Steps to test this PR**: 1. Test the regular DBP flow 2. Specifically test editing info whilst scans are still running 3. Test opening and closing the tab repeatedly. --- ###### 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) --- .../DataBrokerProtectionProcessor.swift | 54 +++---------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 01df6e5e39..f10ca642e5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -24,13 +24,6 @@ protocol OperationRunnerProvider { func getOperationRunner() -> WebOperationRunner } -private enum DataBrokerProtectionProcessorFunction { - case runAllScanOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - case runAllOptOutOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - case runQueuedOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - case runAllOperations(pendingCompletion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) -} - final class DataBrokerProtectionProcessor { private let database: DataBrokerProtectionRepository private let config: SchedulerConfig @@ -42,8 +35,6 @@ final class DataBrokerProtectionProcessor { private let engagementPixels: DataBrokerProtectionEngagementPixels private let eventPixels: DataBrokerProtectionEventPixels - private var currentlyRunningOperationsForFunction: DataBrokerProtectionProcessorFunction? - init(database: DataBrokerProtectionRepository, config: SchedulerConfig, operationRunnerProvider: OperationRunnerProvider, @@ -66,15 +57,13 @@ final class DataBrokerProtectionProcessor { // MARK: - Public functions func runAllScanOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runAllScanOperations(pendingCompletion: completion) + operationQueue.cancelAllOperations() runOperations(operationType: .scan, priorityDate: nil, - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Scans done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) - self?.calculateMisMatches() + self.calculateMisMatches() } } @@ -85,45 +74,37 @@ final class DataBrokerProtectionProcessor { func runAllOptOutOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runAllOptOutOperations(pendingCompletion: completion) + operationQueue.cancelAllOperations() runOperations(operationType: .optOut, priorityDate: nil, - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Optouts done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } func runQueuedOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runQueuedOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: Date(), - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Queued operations done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } func runAllOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { - interruptCurrentlyRunningFunction() - currentlyRunningOperationsForFunction = .runAllOperations(pendingCompletion: completion) runOperations(operationType: .all, priorityDate: nil, - showWebView: showWebView) { [weak self] errors in + showWebView: showWebView) { errors in os_log("Queued operations done", log: .dataBrokerProtection) - self?.currentlyRunningOperationsForFunction = nil completion?(errors) } } func stopAllOperations() { - interruptCurrentlyRunningFunction() + operationQueue.cancelAllOperations() } // MARK: - Private functions @@ -202,25 +183,6 @@ final class DataBrokerProtectionProcessor { return collections } - private func interruptCurrentlyRunningFunction() { - operationQueue.cancelAllOperations() - - switch currentlyRunningOperationsForFunction { - case .runAllScanOperations(let pendingCompletion), - .runAllOptOutOperations(let pendingCompletion), - .runQueuedOperations(let pendingCompletion), - .runAllOperations(let pendingCompletion): - - if let pendingCompletion = pendingCompletion { - // There's a current limitation that if interrupted, we won't propagate the scan errors - pendingCompletion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.operationsInterrupted)) - } - case nil: - break - } - currentlyRunningOperationsForFunction = nil - } - deinit { os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) } From 5f745fe019aa53e2e1d712823c11de8362c29651 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 24 Apr 2024 19:14:51 +0000 Subject: [PATCH 020/134] Bump version to 1.85.0 (174) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index bf9c44d075..67b5d537a4 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 173 +CURRENT_PROJECT_VERSION = 174 From 4d5fe77dc4df785894315f346b3a6fee301ff470 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 24 Apr 2024 18:03:43 -0300 Subject: [PATCH 021/134] Remove debug flags --- DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 7da7bc89ca..ed7bb58e86 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -290,7 +290,7 @@ final class MoreOptionsMenu: NSMenu { var items: [NSMenuItem] = [] if DefaultSubscriptionFeatureAvailability().isFeatureAvailable && !accountManager.isUserAuthenticated { - items.append(contentsOf: makeActiveSubscriptionItems()) + items.append(contentsOf: makeInactiveSubscriptionItems()) } else { items.append(contentsOf: makeActiveSubscriptionItems()) // this adds NETP and DBP only if conditionally enabled } @@ -334,7 +334,7 @@ final class MoreOptionsMenu: NSMenu { #if DBP let dbpVisibility = DefaultDataBrokerProtectionFeatureVisibility() - if true { + if dbpVisibility.isFeatureVisible() || dbpVisibility.isPrivacyProEnabled() { let dataBrokerProtectionItem = NSMenuItem(title: UserText.dataBrokerProtectionOptionsMenuItem, action: #selector(openDataBrokerProtection), keyEquivalent: "") From 5a11ac6104a74528843feb9c11ef63c3dc71bc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 24 Apr 2024 23:51:53 +0200 Subject: [PATCH 022/134] Add parameter allowed encoding to error descriptions (#2691) Task/Issue URL: https://app.asana.com/0/1203790657351911/1207097497487052/f --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- .../xcshareddata/xcschemes/sandbox-test-tool.xcscheme | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 65b524fef3..cbb0e1aab6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12748,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 140.0.0; + version = 140.0.1; }; }; 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 feaf63045b..4fc95d491e 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" : "89442d067d2fcd77d487202b8d38be7e47ac0b5b", - "version" : "140.0.0" + "revision" : "b1501e60d245625d4a5d42a61b329364134f0879", + "version" : "140.0.1" } }, { @@ -120,7 +120,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "46989693916f56d1186bd59ac15124caef896560", "version" : "1.3.1" @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", + "location" : "https://github.com/apple/swift-syntax.git", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index 41730d7069..eb7e5e26bb 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme @@ -1,7 +1,7 @@ + version = "1.8"> Date: Wed, 24 Apr 2024 19:20:29 -0300 Subject: [PATCH 023/134] Fix SwiftLint --- .../Scheduler/DataBrokerProtectionNoOpScheduler.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift index cb3057b937..aaac238d1c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift @@ -35,7 +35,8 @@ final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { func startScheduler(showWebView: Bool) { } func stopScheduler() { } func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func runQueuedOperations(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runAllOperations(showWebView: Bool) { } func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } From 570206e73dfc4a5acd7e70f02d9c9cae7d81bf32 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 24 Apr 2024 19:29:26 -0300 Subject: [PATCH 024/134] Fix DataBrokerProtectionProcessor.swift lint --- .../Scheduler/DataBrokerProtectionProcessor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index f4e52110a6..7465df1771 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -56,7 +56,7 @@ final class DataBrokerProtectionProcessor { // MARK: - Public functions func startManualScans(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() runOperations(operationType: .scan, priorityDate: nil, From c2c11b5b90c31905edd496ebe9909cdee3ea924f Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 25 Apr 2024 10:11:04 +0200 Subject: [PATCH 025/134] Make Clear All History shortcut available without entering Main Menu (#2682) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206028920433046/f Description: Populate Clear All History menu item in HistoryMenu initializer so it's always available. --- DuckDuckGo/Menus/HistoryMenu.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index b99c7a867f..42fd97d990 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -53,7 +53,9 @@ final class HistoryMenu: NSMenu { reopenLastClosedMenuItem recentlyClosedMenuItem reopenAllWindowsFromLastSessionMenuItem - NSMenuItem.separator() + + clearAllHistorySeparator + clearAllHistoryMenuItem } reopenMenuItemKeyEquivalentManager.reopenLastClosedMenuItem = reopenLastClosedMenuItem From d06331180166d04a3ecda7ee9ab507e53cb69393 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:12:51 +0200 Subject: [PATCH 026/134] Update autoconsent to v10.6.1 (#2618) Task/Issue URL: https://app.asana.com/0/1207065820015503/1207065820015503 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v10.6.1 Description Updates Autoconsent to version v10.6.1. --- DuckDuckGo/Autoconsent/autoconsent-bundle.js | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index d7ccb86ad9..85e242339a 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); diff --git a/package-lock.json b/package-lock.json index b3d356236a..353201dd2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.5.0" + "@duckduckgo/autoconsent": "^10.6.1" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.5.0.tgz", - "integrity": "sha512-4mdp9mwBiE+IKTvN84iRA8d7eSkJ5xMaQvhvbgw7XlD1VOJlfiJPhP8PJWV+wyc7DNVHMtcdUXiD+ICw/SJBRA==" + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.6.1.tgz", + "integrity": "sha512-ptgT0sp4zmQTZHAyGR9TN/WJT9W7kTb/yvaF20FwwSIcLKd2xLe2jCDwbGTaLVSqAixWDKqzZ1Dg3l7HE159Sw==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index dbae81bb89..78eb438846 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.5.0" + "@duckduckgo/autoconsent": "^10.6.1" } } From 876e22024cce218cf3c583dc4b597df986d8681a Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 25 Apr 2024 13:18:34 +0100 Subject: [PATCH 027/134] Fix layout issue on DBP (#2700) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207165247619683/f **Description**: Fix issue when laying out DBPHomeViewController --- DuckDuckGo/DBP/DBPHomeViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 55d08d7d62..cf9e05e900 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -112,8 +112,9 @@ final class DBPHomeViewController: NSViewController { override func viewDidLayout() { super.viewDidLayout() - dataBrokerProtectionViewController.view.frame = view.bounds - errorViewController.view.frame = view.bounds + if let currentChildViewController = currentChildViewController { + currentChildViewController.view.frame = view.bounds + } } private func setupUI() { From 54eee3121c842e01f054b976b4997d64e9db1de9 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 25 Apr 2024 14:37:07 +0100 Subject: [PATCH 028/134] Call finish in the correct place (#2702) --- .../Operations/DataBrokerOperationsCollection.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index 78b76af560..81e3859c17 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -192,8 +192,6 @@ final class DataBrokerOperationsCollection: Operation { try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) } - finish() - } catch { os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) self.error = error @@ -203,6 +201,8 @@ final class DataBrokerOperationsCollection: Operation { withDataBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } } + + finish() } private func finish() { From e9577f6ec0312e28d7fd073b40512544b14aa58c Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 25 Apr 2024 13:53:44 +0000 Subject: [PATCH 029/134] Bump version to 1.85.0 (175) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 67b5d537a4..e5dcb60ef4 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 174 +CURRENT_PROJECT_VERSION = 175 From 2abb32f06073b79d8a8b9594f4ba4d8daba1d4c6 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 25 Apr 2024 19:26:04 -0700 Subject: [PATCH 030/134] Add subscription status to the macOS metadata (#2680) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207144276620677/f Tech Design URL: CC: Description: This PR adds the Privacy Pro status to the VPN metadata. It also reports if the user was a beta user (i.e. they have an old-style auth token) just in case we see anything strange related to that. --- .../VPNMetadataCollector.swift | 28 ++++++++++++++++++- .../VPNFeedbackFormViewModelTests.swift | 9 +++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index b4e463df17..925ac292d2 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -24,6 +24,7 @@ import NetworkProtection import NetworkExtension import NetworkProtectionIPC import NetworkProtectionUI +import Subscription struct VPNMetadata: Encodable { @@ -72,12 +73,19 @@ struct VPNMetadata: Encodable { let notificationsAgentIsRunning: Bool } + struct PrivacyProInfo: Encodable { + let betaParticipant: Bool + let hasPrivacyProAccount: Bool + let hasVPNEntitlement: Bool + } + let appInfo: AppInfo let deviceInfo: DeviceInfo let networkInfo: NetworkInfo let vpnState: VPNState let vpnSettingsState: VPNSettingsState let loginItemState: LoginItemState + let privacyProInfo: PrivacyProInfo func toPrettyPrintedJSON() -> String? { let encoder = JSONEncoder() @@ -138,6 +146,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { let vpnState = await collectVPNState() let vpnSettingsState = collectVPNSettingsState() let loginItemState = collectLoginItemState() + let privacyProInfo = await collectPrivacyProInfo() return VPNMetadata( appInfo: appInfoMetadata, @@ -145,7 +154,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { networkInfo: networkInfoMetadata, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } @@ -283,4 +293,20 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { ) } + func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let waitlistStore = WaitlistKeychainStore( + waitlistIdentifier: NetworkProtectionWaitlist.identifier, + keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup + ) + + let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false + + return .init( + betaParticipant: waitlistStore.isInvited, + hasPrivacyProAccount: accountManager.isUserAuthenticated, + hasVPNEntitlement: hasVPNEntitlement + ) + } + } diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index f5f727456e..ee1758f401 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -126,13 +126,20 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { notificationsAgentIsRunning: true ) + let privacyProInfo = VPNMetadata.PrivacyProInfo( + betaParticipant: false, + hasPrivacyProAccount: true, + hasVPNEntitlement: true + ) + return VPNMetadata( appInfo: appInfo, deviceInfo: deviceInfo, networkInfo: networkInfo, vpnState: vpnState, vpnSettingsState: vpnSettingsState, - loginItemState: loginItemState + loginItemState: loginItemState, + privacyProInfo: privacyProInfo ) } From 0215160228c0c905044ee649f25e435570661122 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 11:35:32 +0200 Subject: [PATCH 031/134] Hard-codes the VPN waitlist flags to ON (#2709) Task/Issue URL: https://app.asana.com/0/0/1207169061635762/f ## Description Hardcodes the waitlist remote flags to be ON, since we're relying on those flags for the PPro subscription. This is meant to be a quick fix, and a better cleanup will follow. --- .../NetworkProtectionFeatureVisibility.swift | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index c792a3e788..11bf4a988e 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -166,29 +166,11 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } private var isWaitlistBetaActive: Bool { - switch featureOverrides.waitlistActive { - case .useRemoteValue: - guard privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlistBetaActive) else { - return false - } - - return true - case .on: - return true - case .off: - return false - } + true } private var isWaitlistEnabled: Bool { - switch featureOverrides.waitlistEnabled { - case .useRemoteValue: - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(NetworkProtectionSubfeature.waitlist) - case .on: - return true - case .off: - return false - } + true } func disableForAllUsers() async { From a072d5906a94e0838f0d714543581674edcc449b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 15:23:22 +0200 Subject: [PATCH 032/134] Fix for VPN stop issues. (#2689) Task/Issue URL: https://app.asana.com/0/0/1207063924984126/f iOS PR: Not needed BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/794 ## Description: Tentative fix for an issue where the tunnel provider is taking a long time to stop. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0adc19521d..92763f1ea5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12679,7 +12679,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 138.0.0; + version = "138.0.0-1"; }; }; 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 462a0cc553..0abfe2d2de 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" : "b8f0e5db431c63943b509d522c157f870ef03ae0", - "version" : "138.0.0" + "revision" : "ad13fb66e5afc2cf27496a56f5b412f83ea507ad", + "version" : "138.0.0-1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 51d8ee4755..f0d21708fd 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: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0-1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index ad131b9869..4ae8d01c83 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0-1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 9092bb8c50..1ba19f05f3 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: "138.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "138.0.0-1"), .package(path: "../SwiftUIExtensions") ], targets: [ From d931d5c2002cb0a412880973859f28aee126f1f3 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 26 Apr 2024 16:27:14 +0200 Subject: [PATCH 033/134] Update release automation to support Privacy Pro section in release notes (#2710) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207157777571909/f Description: This change makes it possible to display a separate section in release notes, dedicated to Privacy Pro: * appcastManager.swift was updated to take pre-formatted HTML document with release notes * extract_release_notes.sh was updated to generate HTML or Asana rich text release notes, in addition to raw output. * update_asana_for_release.sh was updated to rely on extract_release_notes.sh for generating release notes for the Asana release task. * workflows were updated to use extract_release_notes.sh to generate HTML documents * extract_release_notes.sh is now unit-tested using Bats (Bash Automated Testing System). * PR checks workflow is updated with a job dedicated to running shell scripts unit tests. --- .github/workflows/bump_internal_release.yml | 8 +- .github/workflows/code_freeze.yml | 1 + .github/workflows/pr.yml | 34 +- .github/workflows/publish_dmg_release.yml | 18 +- scripts/appcast_manager/appcastManager.swift | 48 ++- scripts/extract_release_notes.sh | 133 ++++++- .../extract_release_notes.bats | 349 ++++++++++++++++++ scripts/update_asana_for_release.sh | 31 +- 8 files changed, 565 insertions(+), 57 deletions(-) create mode 100644 scripts/tests/extract_release_notes/extract_release_notes.bats diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml index 4d0d9f440a..13280743b5 100644 --- a/.github/workflows/bump_internal_release.yml +++ b/.github/workflows/bump_internal_release.yml @@ -118,10 +118,10 @@ jobs: curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ | jq -r .data.notes \ - | ./scripts/extract_release_notes.sh > release_notes.txt - release_notes="$(" ]]; then - echo "::error::Release notes are empty. Please add release notes to the Asana task and restart the workflow." + | ./scripts/extract_release_notes.sh -r > raw_release_notes.txt + raw_release_notes="$("* ]]; then + echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." exit 1 fi diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 2da53a932a..755282af5e 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -172,6 +172,7 @@ jobs: uses: ./.github/workflows/tag_release.yml with: asana-task-url: ${{ needs.create_release_branch.outputs.asana_task_url }} + base-branch: ${{ github.ref_name }} branch: ${{ needs.create_release_branch.outputs.release_branch_name }} prerelease: true secrets: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 528b219281..fd2341e1b6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -54,6 +54,36 @@ jobs: env: SHELLCHECK_OPTS: -x -P scripts -P scripts/helpers + bats: + + name: Test Shell Scripts + + runs-on: macos-13 + + steps: + - name: Check out the code + if: github.event_name == 'pull_request' || github.event_name == 'push' + uses: actions/checkout@v4 + + - name: Check out the code + if: github.event_name != 'pull_request' && github.event_name != 'push' + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref_name }} + + - name: Install Bats + run: brew install bats-core + + - name: Run Bats tests + run: bats --formatter junit scripts/tests/* > bats-tests.xml + + - name: Publish unit tests report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + check_name: "Test Report: Shell Scripts" + report_paths: 'bats-tests.xml' + tests: name: Test @@ -343,7 +373,7 @@ jobs: create-asana-task: name: Create Asana Task - needs: [swiftlint, tests, release-build, verify-autoconsent-bundle, private-api] + needs: [swiftlint, bats, tests, release-build, verify-autoconsent-bundle, private-api] if: failure() && github.ref_name == 'main' && github.run_attempt == 1 @@ -360,7 +390,7 @@ jobs: close-asana-task: name: Close Asana Task - needs: [swiftlint, tests, release-build, verify-autoconsent-bundle, private-api] + needs: [swiftlint, bats, tests, release-build, verify-autoconsent-bundle, private-api] if: success() && github.ref_name == 'main' && github.run_attempt > 1 diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 8daecea7be..2dd82d53ed 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -145,14 +145,14 @@ jobs: run: | curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}?opt_fields=notes" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ - | jq -r .data.notes \ - | ./scripts/extract_release_notes.sh > release_notes.txt - release_notes="$(" ]]; then - echo "::error::Release notes are empty. Please add release notes to the Asana task and restart the workflow." + | jq -r .data.notes > release_task_content.txt + raw_release_notes="$(./scripts/extract_release_notes.sh -r < release_task_content.txt)" + if [[ ${#raw_release_notes} == 0 || "$raw_release_notes" == *"<-- Add release notes here -->"* ]]; then + echo "::error::Release notes are empty or contain a placeholder. Please add release notes to the Asana task and restart the workflow." exit 1 fi - echo "RELEASE_NOTES_FILE=release_notes.txt" >> $GITHUB_ENV + ./scripts/extract_release_notes.sh < release_task_content.txt > release_notes.html + echo "RELEASE_NOTES_FILE=release_notes.html" >> $GITHUB_ENV - name: Set up Sparkle tools env: @@ -189,21 +189,21 @@ jobs: ./scripts/appcast_manager/appcastManager.swift \ --release-to-internal-channel \ --dmg ${DMG_PATH} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; "public") ./scripts/appcast_manager/appcastManager.swift \ --release-to-public-channel \ --version ${VERSION} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; "hotfix") ./scripts/appcast_manager/appcastManager.swift \ --release-hotfix-to-public-channel \ --dmg ${DMG_PATH} \ - --release-notes release_notes.txt \ + --release-notes-html release_notes.html \ --key sparkle_private_key ;; *) diff --git a/scripts/appcast_manager/appcastManager.swift b/scripts/appcast_manager/appcastManager.swift index c3906a3cae..e7d28209e5 100755 --- a/scripts/appcast_manager/appcastManager.swift +++ b/scripts/appcast_manager/appcastManager.swift @@ -80,6 +80,9 @@ SYNOPSIS appcastManager --release-to-internal-channel --dmg --release-notes [--key ] appcastManager --release-to-public-channel --version [--release-notes ] [--key ] appcastManager --release-hotfix-to-public-channel --dmg --release-notes [--key ] + appcastManager --release-to-internal-channel --dmg --release-notes-html [--key ] + appcastManager --release-to-public-channel --version [--release-notes-html ] [--key ] + appcastManager --release-hotfix-to-public-channel --dmg --release-notes-html [--key ] appcastManager --help DESCRIPTION @@ -109,7 +112,13 @@ DESCRIPTION exit(0) case .releaseToInternalChannel, .releaseHotfixToPublicChannel: - guard let dmgPath = arguments.parameters["--dmg"], let releaseNotesPath = arguments.parameters["--release-notes"] else { + guard let dmgPath = arguments.parameters["--dmg"] else { + print("Missing required parameters") + exit(1) + } + let releaseNotesPath = arguments.parameters["--release-notes"] + let releaseNotesHTMLPath = arguments.parameters["--release-notes-html"] + guard releaseNotesPath != nil || releaseNotesHTMLPath != nil else { print("Missing required parameters") exit(1) } @@ -117,7 +126,11 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: print("➡️ Action: Add to internal channel") print("➡️ DMG Path: \(dmgPath)") - print("➡️ Release Notes Path: \(releaseNotesPath)") + if let releaseNotesPath { + print("➡️ Release Notes Path: \(releaseNotesPath)") + } else if let releaseNotesHTMLPath { + print("➡️ Release Notes HTML Path: \(releaseNotesHTMLPath)") + } if isCI, let keyFile { print("➡️ Key file: \(keyFile)") } @@ -130,7 +143,11 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: } // Handle release notes file - handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + if let releaseNotesPath { + handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + } else if let releaseNotesHTMLPath { + handleReleaseNotesHTML(path: releaseNotesHTMLPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) + } // Extract version number from DMG file name let versionNumber = getVersionNumberFromDMGFileName(dmgURL: dmgURL) @@ -170,6 +187,10 @@ case .releaseToPublicChannel: print("Release Notes Path: \(releaseNotesPath)") let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) + } else if let releaseNotesHTMLPath = arguments.parameters["--release-notes-html"] { + print("Release Notes Path: \(releaseNotesHTMLPath)") + let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) + handleReleaseNotesHTML(path: releaseNotesHTMLPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) } else { print("👀 No new release notes provided. Keeping existing release notes.") } @@ -605,6 +626,27 @@ final class AppcastDownloader { // MARK: - Handling of Release Notes +func handleReleaseNotesHTML(path: String, updatesDirectoryURL: URL, dmgURL: URL) { + // Copy release notes file and rename it to match the dmg filename + let releaseNotesURL = URL(fileURLWithPath: path) + let destinationReleaseNotesURL = updatesDirectoryURL.appendingPathComponent(dmgURL.deletingPathExtension().lastPathComponent + ".html") + + do { + if FileManager.default.fileExists(atPath: destinationReleaseNotesURL.path) { + try FileManager.default.removeItem(at: destinationReleaseNotesURL) + print("Old release notes file removed.") + } + + // Save the converted release notes to the destination file + try FileManager.default.copyItem(at: releaseNotesURL, to: destinationReleaseNotesURL) + print("✅ New release notes HTML file copied to the updates directory.") + + } catch { + print("❌ Failed to copy and convert release notes HTML file: \(error).") + exit(1) + } +} + func handleReleaseNotesFile(path: String, updatesDirectoryURL: URL, dmgURL: URL) { // Copy release notes file and rename it to match the dmg filename let releaseNotesURL = URL(fileURLWithPath: path) diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh index 9c7db812a6..f995620dfe 100755 --- a/scripts/extract_release_notes.sh +++ b/scripts/extract_release_notes.sh @@ -7,30 +7,133 @@ # start_marker="release notes" +pp_marker="^for privacy pro subscribers:?$" end_marker="this release includes:" +placeholder="add release notes here" is_capturing=0 +is_capturing_pp=0 has_content=0 +notes= +pp_notes= -if [[ "$1" == "-t" ]]; then - # capture included tasks instead of release notes - start_marker="this release includes:" - end_marker= -fi +output="html" + +case "$1" in + -a) + # Generate Asana rich text output + output="asana" + ;; + -r) + # Generate raw output instead of HTML + output="raw" + ;; + -t) + # Capture raw included tasks' URLs instead of release notes + output="tasks" + start_marker="this release includes:" + pp_marker= + end_marker= + ;; + *) + ;; +esac + +html_escape() { + local input="$1" + sed -e 's/&/\&/g' -e 's//\>/g' <<< "$input" +} + +make_links() { + local input="$1" + sed -E 's|(https://[^ ]*)|\1|' <<< "$input" +} + +lowercase() { + local input="$1" + tr '[:upper:]' '[:lower:]' <<< "$input" +} + +print_and_exit() { + echo -ne "$notes" + exit 0 +} + +add_to_notes() { + notes+="$1" + if [[ "$output" != "asana" ]]; then + notes+="\\n" + fi +} + +add_to_pp_notes() { + pp_notes+="$1" + if [[ "$output" != "asana" ]]; then + pp_notes+="\\n" + fi +} + +add_release_note() { + local release_note="$1" + local processed_release_note= + if [[ "$output" == "raw" || "$output" == "tasks" ]]; then + processed_release_note="$release_note" + else + processed_release_note="
  • $(make_links "$(html_escape "$release_note")")
  • " + fi + if [[ $is_capturing_pp -eq 1 ]]; then + add_to_pp_notes "$processed_release_note" + else + add_to_notes "$processed_release_note" + fi +} while read -r line do - if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$start_marker" ]]; then - is_capturing=1 - elif [[ -n "$end_marker" && $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$end_marker" ]]; then - exit 0 - elif [[ $is_capturing -eq 1 && -n "$line" ]]; then - has_content=1 - echo "$line" - fi + # Lowercase each line to compare with markers + lowercase_line="$(lowercase "$line")" + + if [[ "$lowercase_line" == "$start_marker" ]]; then + # Only start capturing here + is_capturing=1 + if [[ "$output" == "asana" ]]; then + add_to_notes "
      " + elif [[ "$output" == "html" ]]; then + # Add HTML header and start the list + add_to_notes "

      What's new

      " + add_to_notes "
        " + fi + elif [[ -n "$pp_marker" && "$lowercase_line" =~ $pp_marker ]]; then + is_capturing_pp=1 + if [[ "$output" == "asana" ]]; then + add_to_pp_notes "

      For Privacy Pro subscribers

        " + elif [[ "$output" == "html" ]]; then + # If we've reached the PP marker, end the list and start the PP list + add_to_pp_notes "
      " + add_to_pp_notes "

      For Privacy Pro subscribers

      " + add_to_pp_notes "
        " + else + add_to_pp_notes "$line" + fi + elif [[ -n "$end_marker" && "$lowercase_line" == "$end_marker" ]]; then + # If we've reached the end marker, check if PP notes are present and not a placeholder, and add them verbatim to notes + # shellcheck disable=SC2076 + if [[ -n "$pp_notes" && ! "$(lowercase "$pp_notes")" =~ "$placeholder" ]]; then + notes+="$pp_notes" # never add extra newline here (that's why we don't use `add_to_notes`) + fi + if [[ "$output" != "raw" ]]; then + # End the list on end marker + add_to_notes "
      " + fi + # Print output and exit + print_and_exit + elif [[ $is_capturing -eq 1 && -n "$line" ]]; then + has_content=1 + add_release_note "$line" + fi done if [[ $has_content -eq 0 ]]; then - exit 1 + exit 1 fi -exit 0 +print_and_exit diff --git a/scripts/tests/extract_release_notes/extract_release_notes.bats b/scripts/tests/extract_release_notes/extract_release_notes.bats new file mode 100644 index 0000000000..49f9a31e40 --- /dev/null +++ b/scripts/tests/extract_release_notes/extract_release_notes.bats @@ -0,0 +1,349 @@ +#!/usr/bin/env bats + +setup() { + DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" + # make executables in ./../../ visible to PATH + PATH="$DIR/../..:$PATH" +} + +main() { + bash extract_release_notes.sh "$@" +} + +# +# Functions below define inputs and expected outputs for the tests +# + +# Placeholder release notes with placeholder Privacy Pro section +placeholder() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + <-- Add release notes here --> + + For Privacy Pro subscribers + + <-- Add release notes here --> + + This release includes: + EOF + ;; + raw) + cat <<-EOF + <-- Add release notes here --> + EOF + ;; + html) + cat <<-EOF +

      What's new

      +
        +
      • <-- Add release notes here -->
      • +
      + EOF + ;; + asana) + cat <<-EOF +
      • <-- Add release notes here -->
      + EOF + ;; + esac +} + +# Non-empty release notes with non-empty Privacy Pro section +full() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + + For Privacy Pro subscribers + + VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements. + Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only. + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + raw) + cat <<-EOF + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + For Privacy Pro subscribers + VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements. + Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only. + EOF + ;; + html) + cat <<-EOF +

      What's new

      +
        +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • +
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • +
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • +
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.
      • +
      +

      For Privacy Pro subscribers

      +
        +
      • VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements.
      • +
      • Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only.
      • +
      + EOF + ;; + asana) + cat <<-EOF +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.

      For Privacy Pro subscribers

      • VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements.
      • Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only.
      + EOF + ;; + esac +} + +# Non-empty release notes and missing Privacy Pro section +without_privacy_pro_section() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + raw) + cat <<-EOF + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + EOF + ;; + html) + cat <<-EOF +

      What's new

      +
        +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • +
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • +
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • +
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.
      • +
      + EOF + ;; + asana) + cat <<-EOF +
      • You can now find browser windows listed in the "Window" app menu and in the Dock menu.
      • We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts.
      • When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab.
      • The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed.
      + EOF + ;; + esac +} + +# Non-empty release notes and a placeholder Privacy Pro section +placeholder_privacy_pro_section() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + + For Privacy Pro subscribers + + <-- Add release notes here --> + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + *) + without_privacy_pro_section "$mode" + ;; + esac +} + +# Non-empty release notes and Privacy Pro release header as a bullet point inside regular release notes +# Privacy Pro section header should be recognized and interpreted as a separate section (like in the full example) +privacy_pro_in_regular_release_notes() { + local mode="${1:-input}" + case "$mode" in + input) + cat <<-EOF + Note: This task's description is managed automatically. + Only the Release notes section below should be modified manually. + Please do not adjust formatting. + + Release notes + + You can now find browser windows listed in the "Window" app menu and in the Dock menu. + We also added "Duplicate Tab" to the app menu so you can use it as an action in Apple Shortcuts. + When watching videos in Duck Player, clicking endscreen recommendations will now open those videos in the same tab. + The bug that duplicated sites in your browsing history has been fixed, and the visual glitching that sometimes occurred during session restore and app launch has been addressed. + For Privacy Pro subscribers + VPN updates! More detailed connection info in the VPN dashboard, plus animations and usability improvements. + Visit https://duckduckgo.com/pro for more information. Privacy Pro is currently available to U.S. residents only. + + This release includes: + + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + https://app.asana.com/0/0/0/f/ + EOF + ;; + *) + full "$mode" + ;; + esac +} + +# +# Test cases start here +# + +# bats test_tags=placeholder, raw +@test "input: placeholder | output: raw" { + run main -r <<< "$(placeholder)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder raw)" ] +} + +# bats test_tags=placeholder, html +@test "input: placeholder | output: html" { + run main -h <<< "$(placeholder)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder html)" ] +} + +# bats test_tags=placeholder, asana +@test "input: placeholder | output: asana" { + run main -a <<< "$(placeholder)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder asana)" ] +} + +# bats test_tags=full, raw +@test "input: full | output: raw" { + run main -r <<< "$(full)" + [ "$status" -eq 0 ] + [ "$output" == "$(full raw)" ] +} + +# bats test_tags=full, html +@test "input: full | output: html" { + run main -h <<< "$(full)" + [ "$status" -eq 0 ] + [ "$output" == "$(full html)" ] +} + +# bats test_tags=full, asana +@test "input: full | output: asana" { + run main -a <<< "$(full)" + [ "$status" -eq 0 ] + [ "$output" == "$(full asana)" ] +} + +# bats test_tags=no-pp, raw +@test "input: without_privacy_pro_section | output: raw" { + run main -r <<< "$(without_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(without_privacy_pro_section raw)" ] +} + +# bats test_tags=no-pp, html +@test "input: without_privacy_pro_section | output: html" { + run main -h <<< "$(without_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(without_privacy_pro_section html)" ] +} + +# bats test_tags=no-pp, asana +@test "input: without_privacy_pro_section | output: asana" { + run main -a <<< "$(without_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(without_privacy_pro_section asana)" ] +} + +# bats test_tags=placeholder-pp, raw +@test "input: placeholder_privacy_pro_section | output: raw" { + run main -r <<< "$(placeholder_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder_privacy_pro_section raw)" ] +} + +# bats test_tags=placeholder-pp, html +@test "input: placeholder_privacy_pro_section | output: html" { + run main -h <<< "$(placeholder_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder_privacy_pro_section html)" ] +} + +# bats test_tags=placeholder-pp, asana +@test "input: placeholder_privacy_pro_section | output: asana" { + run main -a <<< "$(placeholder_privacy_pro_section)" + [ "$status" -eq 0 ] + [ "$output" == "$(placeholder_privacy_pro_section asana)" ] +} + +# bats test_tags=inline-pp, raw +@test "input: privacy_pro_in_regular_release_notes | output: raw" { + run main -r <<< "$(privacy_pro_in_regular_release_notes)" + [ "$status" -eq 0 ] + [ "$output" == "$(privacy_pro_in_regular_release_notes raw)" ] +} + +# bats test_tags=inline-pp, html +@test "input: privacy_pro_in_regular_release_notes | output: html" { + run main -h <<< "$(privacy_pro_in_regular_release_notes)" + [ "$status" -eq 0 ] + [ "$output" == "$(privacy_pro_in_regular_release_notes html)" ] +} + +# bats test_tags=inline-pp, asana +@test "input: privacy_pro_in_regular_release_notes | output: asana" { + run main -a <<< "$(privacy_pro_in_regular_release_notes)" + [ "$status" -eq 0 ] + [ "$output" == "$(privacy_pro_in_regular_release_notes asana)" ] +} diff --git a/scripts/update_asana_for_release.sh b/scripts/update_asana_for_release.sh index a0aef9c83e..b5610faed7 100755 --- a/scripts/update_asana_for_release.sh +++ b/scripts/update_asana_for_release.sh @@ -48,7 +48,7 @@ fetch_current_release_notes() { curl -fLSs "${asana_api_url}/tasks/${release_task_id}?opt_fields=notes" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ | jq -r .data.notes \ - | "${cwd}"/extract_release_notes.sh + | "${cwd}"/extract_release_notes.sh -a } get_task_id() { @@ -58,19 +58,6 @@ get_task_id() { fi } -construct_release_notes() { - local escaped_release_note - - if [[ -n "${release_notes[*]}" ]]; then - printf '%s' '
        ' - for release_note in "${release_notes[@]}"; do - escaped_release_note="$(sed -e 's/&/\&/g' -e 's//\>/g' <<< "${release_note}")" - printf '%s' "
      • ${escaped_release_note}
      • " - done - printf '%s' '
      ' - fi -} - construct_this_release_includes() { if [[ -n "${task_ids[*]}" ]]; then printf '%s' '
        ' @@ -89,7 +76,7 @@ construct_release_task_description() { printf '%s' 'Please do not adjust formatting.' printf '%s' '

        Release notes

        ' - construct_release_notes + printf '%s' "$release_notes" printf '%s' '

        This release includes:

        ' construct_this_release_includes @@ -105,7 +92,7 @@ construct_release_announcement_task_description() { printf '%s' '
      \n
      ' printf '%s' '

      Release notes

      ' - construct_release_notes + printf '%s' "$release_notes" printf '%s' '\n' printf '%s' '

      This release includes:

      ' @@ -282,10 +269,8 @@ handle_internal_release() { done <<< "$(find_task_urls_in_git_log "$last_release_tag")" # 2. Fetch current release notes from Asana release task. - local release_notes=() - while read -r line; do - release_notes+=("$line") - done <<< "$(fetch_current_release_notes "${release_task_id}")" + local release_notes + release_notes="$(fetch_current_release_notes "${release_task_id}")" # 3. Construct new release task description local html_notes @@ -325,10 +310,8 @@ handle_public_release() { complete_tasks "${task_ids[@]}" # 5. Fetch current release notes from Asana release task. - local release_notes=() - while read -r line; do - release_notes+=("$line") - done <<< "$(fetch_current_release_notes "${release_task_id}")" + local release_notes + release_notes="$(fetch_current_release_notes "${release_task_id}")" # 6. Construct release announcement task description local html_notes From ec9aa0268e48e1f29bbe9645e08f619f25a76bf3 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 26 Apr 2024 14:41:25 +0000 Subject: [PATCH 034/134] Bump version to 1.85.0 (176) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index e5dcb60ef4..fe80d801b7 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 175 +CURRENT_PROJECT_VERSION = 176 From 78f919cd88e6158a168af313d17b1a6ec56c2e99 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 26 Apr 2024 21:21:30 +0600 Subject: [PATCH 035/134] remove Tab.TabContent.url (#2647) Task/Issue URL: https://app.asana.com/0/0/1207078384259685/f --- .../Autoconsent/AutoconsentUserScript.swift | 2 +- .../Common/Extensions/URLExtension.swift | 5 +++ .../View/FeedbackViewController.swift | 2 +- .../FireproofingURLExtensions.swift | 2 +- .../AddressBarButtonsViewController.swift | 22 +++++------ .../View/AddressBarTextField.swift | 2 +- .../View/AddressBarViewController.swift | 6 +-- .../NavigationBar/View/MoreOptionsMenu.swift | 37 +++++++++++-------- .../View/NavigationBarViewController.swift | 2 +- .../PinnedTabs/View/PinnedTabView.swift | 4 +- .../PrivacyDashboardPermissionHandler.swift | 2 +- .../View/PrivacyDashboardViewController.swift | 4 +- .../Model/RecentlyClosedCacheItem.swift | 2 +- .../View/RecentlyClosedMenu.swift | 2 +- DuckDuckGo/Sharing/SharingMenu.swift | 2 +- DuckDuckGo/Tab/Model/Tab.swift | 25 ++++++++----- DuckDuckGo/Tab/Model/TabContent.swift | 25 +++++++------ .../Tab/TabExtensions/TabExtensions.swift | 2 +- .../TabExtensions/TabSnapshotExtension.swift | 3 +- .../Tab/TabLazyLoader/LazyLoadable.swift | 2 +- .../Tab/View/BrowserTabViewController.swift | 2 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 4 +- .../TabBar/View/TabBarViewController.swift | 2 +- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 2 +- .../TabPreview/TabPreviewViewController.swift | 2 +- .../View/WindowControllersManager.swift | 6 +-- ...NavigationProtectionIntegrationTests.swift | 2 +- IntegrationTests/Tab/AddressBarTests.swift | 2 +- IntegrationTests/Tab/ErrorPageTests.swift | 2 +- UnitTests/Fire/Model/FireTests.swift | 2 +- UnitTests/Tab/Model/TabTests.swift | 2 +- .../TabBar/View/TabBarViewItemTests.swift | 3 -- 32 files changed, 101 insertions(+), 83 deletions(-) diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index c43f7dc27c..2000073692 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -202,7 +202,7 @@ extension AutoconsentUserScript { return } - guard [.http, .https].contains(url.navigationalScheme) else { + guard url.navigationalScheme?.isHypertextScheme == true else { // ignore special schemes os_log("Ignoring special URL scheme: %s", log: .autoconsent, type: .debug, messageData.url) replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 62b6c8fc2b..abd290dd0f 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -30,6 +30,11 @@ extension URL.NavigationalScheme { return [.http, .https, .file] } + /// HTTP or HTTPS + var isHypertextScheme: Bool { + Self.hypertextSchemes.contains(self) + } + } extension URL { diff --git a/DuckDuckGo/Feedback/View/FeedbackViewController.swift b/DuckDuckGo/Feedback/View/FeedbackViewController.swift index ff45c1a7e9..6bec572e75 100644 --- a/DuckDuckGo/Feedback/View/FeedbackViewController.swift +++ b/DuckDuckGo/Feedback/View/FeedbackViewController.swift @@ -75,7 +75,7 @@ final class FeedbackViewController: NSViewController { var currentTab: Tab? var currentTabUrl: URL? { - guard let url = currentTab?.content.url else { + guard let url = currentTab?.content.urlForWebView else { return nil } diff --git a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift index d75068166c..da4b9df331 100644 --- a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift +++ b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift @@ -52,7 +52,7 @@ extension URL { ] var canFireproof: Bool { - guard let host = self.host else { return false } + guard let host = self.host, self.navigationalScheme?.isHypertextScheme == true else { return false } return (host != Self.cookieDomain) } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 566a7c413f..878114b49b 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -279,7 +279,7 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } var isUrlBookmarked = false - if let url = tabViewModel.tab.content.url, + if let url = tabViewModel.tab.content.userEditableUrl, bookmarkManager.isUrlBookmarked(url: url) { isUrlBookmarked = true } @@ -413,7 +413,7 @@ final class AddressBarButtonsViewController: NSViewController { permissions.microphone = tabViewModel.usedPermissions.microphone } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: permissions.map { ($0, $1) }, domain: domain, delegate: self) @@ -432,7 +432,7 @@ final class AddressBarButtonsViewController: NSViewController { return } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: [(.microphone, state)], domain: domain, delegate: self) @@ -451,7 +451,7 @@ final class AddressBarButtonsViewController: NSViewController { return } - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: [(.geolocation, state)], domain: domain, delegate: self) @@ -475,7 +475,7 @@ final class AddressBarButtonsViewController: NSViewController { $0.append( (.popups, .requested($1)) ) } } else { - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty domain = url.isFileURL ? .localhost : (url.host ?? "") permissions = [(.popups, state)] } @@ -499,7 +499,7 @@ final class AddressBarButtonsViewController: NSViewController { } permissions = [(permissionType, state)] - let url = tabViewModel.tab.content.url ?? .empty + let url = tabViewModel.tab.content.urlForWebView ?? .empty let domain = url.isFileURL ? .localhost : (url.host ?? "") PermissionContextMenu(permissions: permissions, domain: domain, delegate: self) @@ -733,7 +733,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func updateBookmarkButtonImage(isUrlBookmarked: Bool = false) { - if let url = tabViewModel?.tab.content.url, + if let url = tabViewModel?.tab.content.userEditableUrl, isUrlBookmarked || bookmarkManager.isUrlBookmarked(url: url) { bookmarkButton.image = .bookmarkFilled @@ -770,11 +770,11 @@ final class AddressBarButtonsViewController: NSViewController { private func updatePrivacyEntryPointButton() { guard let tabViewModel else { return } - let urlScheme = tabViewModel.tab.content.url?.scheme - let isHypertextUrl = urlScheme == "http" || urlScheme == "https" + let url = tabViewModel.tab.content.userEditableUrl + let isHypertextUrl = url?.navigationalScheme?.isHypertextScheme == true && url?.isDuckPlayer == false let isEditingMode = controllerMode?.isEditing ?? false let isTextFieldValueText = textFieldValue?.isText ?? false - let isLocalUrl = tabViewModel.tab.content.url?.isLocalURL ?? false + let isLocalUrl = url?.isLocalURL ?? false // Privacy entry point button privacyEntryPointButton.isHidden = isEditingMode @@ -922,7 +922,7 @@ final class AddressBarButtonsViewController: NSViewController { private func bookmarkForCurrentUrl(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) -> (bookmark: Bookmark?, isNew: Bool) { guard let tabViewModel, - let url = tabViewModel.tab.content.url else { + let url = tabViewModel.tab.content.userEditableUrl else { assertionFailure("No URL for bookmarking") return (nil, false) } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 21bb3b0bfa..c9692f1546 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -275,7 +275,7 @@ final class AddressBarTextField: NSTextField { guard let selectedTabViewModel = selectedTabViewModel ?? tabCollectionViewModel.selectedTabViewModel else { return } let addressBarString = addressBarString ?? selectedTabViewModel.addressBarString - let isSearch = selectedTabViewModel.tab.content.url?.isDuckDuckGoSearch ?? false + let isSearch = selectedTabViewModel.tab.content.userEditableUrl?.isDuckDuckGoSearch ?? false self.value = Value(stringValue: addressBarString, userTyped: false, isSearch: isSearch) clearUndoManager() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 43ff508e66..cac827debd 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -239,9 +239,9 @@ final class AddressBarViewController: NSViewController { func shouldShowLoadingIndicator(for tabViewModel: TabViewModel, isLoading: Bool, error: Error?) -> Bool { if isLoading, - let url = tabViewModel.tab.content.url, - [.http, .https].contains(url.navigationalScheme), - url.isDuckDuckGoSearch == false, + let url = tabViewModel.tab.content.urlForWebView, + url.navigationalScheme?.isHypertextScheme == true, + !url.isDuckDuckGoSearch, !url.isDuckPlayer, error == nil { return true } else { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index ed7bb58e86..bfc0a1025b 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -407,10 +407,11 @@ final class MoreOptionsMenu: NSMenu { } private func addPageItems() { - guard let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url else { return } + guard let tabViewModel = tabCollectionViewModel.selectedTabViewModel, + let url = tabViewModel.tab.content.userEditableUrl else { return } + let oldItemsCount = items.count if url.canFireproof, let host = url.host { - let isFireproof = FireproofDomains.shared.isFireproof(fireproofDomain: host) let title = isFireproof ? UserText.removeFireproofing : UserText.fireproofSite let image: NSImage = isFireproof ? .burn : .fireproof @@ -418,25 +419,29 @@ final class MoreOptionsMenu: NSMenu { addItem(withTitle: title, action: #selector(toggleFireproofing(_:)), keyEquivalent: "") .targetting(self) .withImage(image) - } - addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") - .targetting(self) - .withImage(.findSearch) - .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") - - addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") - .targetting(self) - .withImage(.share) - .withSubmenu(sharingMenu) + if tabViewModel.canReload { + addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") + .targetting(self) + .withImage(.findSearch) + .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") - addItem(withTitle: UserText.printMenuItem, action: #selector(doPrint(_:)), keyEquivalent: "") - .targetting(self) - .withImage(.print) + addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") + .targetting(self) + .withImage(.share) + .withSubmenu(sharingMenu) + } - addItem(NSMenuItem.separator()) + if tabViewModel.canPrint { + addItem(withTitle: UserText.printMenuItem, action: #selector(doPrint(_:)), keyEquivalent: "") + .targetting(self) + .withImage(.print) + } + if items.count > oldItemsCount { + addItem(NSMenuItem.separator()) + } } private func makeNetworkProtectionItem() -> NSMenuItem { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 582a8b4f28..ee17298593 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -698,7 +698,7 @@ final class NavigationBarViewController: NSViewController { passwordManagementButton.menu = menu passwordManagementButton.toolTip = UserText.autofillShortcutTooltip - let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url + let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.userEditableUrl passwordManagementButton.image = .passwordManagement diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 3e067f1187..c9a0c87f8b 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -224,7 +224,9 @@ struct PinnedTabInnerView: View { .resizable() mutedTabIndicator } - } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { + } else if let domain = model.content.userEditableUrl?.host, + let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), + let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() .foregroundColor(.forString(eTLDplus1)) diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift index f1734c4a6f..fe991eade4 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardPermissionHandler.swift @@ -60,7 +60,7 @@ final class PrivacyDashboardPermissionHandler { assertionFailure("PrivacyDashboardViewController: tabViewModel not set") return } - guard let domain = tabViewModel?.tab.content.url?.host else { + guard let domain = tabViewModel?.tab.content.urlForWebView?.host else { onPermissionChange?([]) return } diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index 41e1300195..befe61b87a 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -325,7 +325,7 @@ extension PrivacyDashboardViewController { // ⚠️ To limit privacy risk, site URL is trimmed to not include query and fragment guard let currentTab = tabViewModel?.tab, - let currentURL = currentTab.content.url?.trimmingQueryItemsAndFragment() else { + let currentURL = currentTab.content.urlForWebView?.trimmingQueryItemsAndFragment() else { throw BrokenSiteReportError.failedToFetchTheCurrentURL } let blockedTrackerDomains = currentTab.privacyInfo?.trackerInfo.trackersBlocked.compactMap { $0.domain } ?? [] @@ -335,7 +335,7 @@ extension PrivacyDashboardViewController { // current domain's protection status let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig - let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: currentTab.content.url?.host) + let protectionsState = configuration.isFeature(.contentBlocking, enabledForDomain: currentTab.content.urlForWebView?.host) let webVitals = await calculateWebVitals(performanceMetrics: currentTab.brokenSiteInfo?.performanceMetrics, privacyConfig: configuration) diff --git a/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift b/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift index 3314b3da2a..2d313b152c 100644 --- a/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift +++ b/DuckDuckGo/RecentlyClosed/Model/RecentlyClosedCacheItem.swift @@ -35,7 +35,7 @@ extension RecentlyClosedTab: RecentlyClosedCacheItemBurning { } private func contentContainsDomains(_ baseDomains: Set, tld: TLD) -> Bool { - if let host = tabContent.url?.host, let baseDomain = tld.eTLDplus1(host), baseDomains.contains(baseDomain) { + if let host = tabContent.urlForWebView?.host, let baseDomain = tld.eTLDplus1(host), baseDomains.contains(baseDomain) { return true } else { return false diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index cdbeec8739..3c24609a30 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -79,7 +79,7 @@ private extension NSMenuItem { case .url, .subscription, .identityTheftRestoration: image = recentlyClosedTab.favicon image?.size = NSSize.faviconSize - title = recentlyClosedTab.title ?? recentlyClosedTab.tabContent.url?.absoluteString ?? "" + title = recentlyClosedTab.title ?? recentlyClosedTab.tabContent.userEditableUrl?.absoluteString ?? "" if title.count > MainMenu.Constants.maxTitleLength { title = String(title.truncated(length: MainMenu.Constants.maxTitleLength)) diff --git a/DuckDuckGo/Sharing/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift index 992ddb6103..bdb91d56bf 100644 --- a/DuckDuckGo/Sharing/SharingMenu.swift +++ b/DuckDuckGo/Sharing/SharingMenu.swift @@ -49,7 +49,7 @@ final class SharingMenu: NSMenu { guard let tabViewModel = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.selectedTabViewModel, tabViewModel.canReload, !tabViewModel.isShowingErrorPage, - let url = tabViewModel.tab.content.url else { return nil } + let url = tabViewModel.tab.content.userEditableUrl else { return nil } let sharingData = DuckPlayer.shared.sharingData(for: tabViewModel.title, url: url) ?? (tabViewModel.title, url) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index a799ebbb04..99f9fcbf2c 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -461,7 +461,7 @@ protocol NewWindowPolicyDecisionMaker { if let url = webView.url { let content = TabContent.contentFromURL(url, source: .webViewUpdated) - if self.content.isUrl, self.content.url == url { + if self.content.isUrl, self.content.urlForWebView == url { // ignore content updates when tab.content has userEntered or credential set but equal url as it comes from the WebView url updated event } else if content != self.content { self.content = content @@ -579,7 +579,7 @@ protocol NewWindowPolicyDecisionMaker { @MainActor var currentHistoryItem: BackForwardListItem? { webView.backForwardList.currentItem.map(BackForwardListItem.init) - ?? (content.url ?? navigationDelegate.currentNavigation?.url).map { url in + ?? (content.urlForWebView ?? navigationDelegate.currentNavigation?.url).map { url in BackForwardListItem(kind: .url(url), title: webView.title ?? title, identity: nil) } } @@ -608,7 +608,11 @@ protocol NewWindowPolicyDecisionMaker { let canGoBack = webView.canGoBack let canGoForward = webView.canGoForward - let canReload = self.content.userEditableUrl != nil + let canReload = if case .url(let url, credential: _, source: _) = content, !(url.isDuckPlayer || url.isDuckURLScheme) { + true + } else { + false + } if canGoBack != self.canGoBack { self.canGoBack = canGoBack @@ -721,7 +725,7 @@ protocol NewWindowPolicyDecisionMaker { if startupPreferences.launchToCustomHomePage, let customURL = URL(string: startupPreferences.formattedCustomHomePageURL) { - setContent(.url(customURL, credential: nil, source: .ui)) + setContent(.contentFromURL(customURL, source: .ui)) } else { setContent(.newtab) } @@ -895,9 +899,10 @@ protocol NewWindowPolicyDecisionMaker { } func requestFireproofToggle() { - guard let url = content.userEditableUrl, - let host = url.host - else { return } + guard case .url(let url, _, _) = content, + url.navigationalScheme?.isHypertextScheme == true, + !url.isDuckPlayer, + let host = url.host else { return } _ = FireproofDomains.shared.toggle(domain: host) } @@ -992,7 +997,7 @@ protocol NewWindowPolicyDecisionMaker { if cachedFavicon != favicon { favicon = cachedFavicon } - } else if oldValue?.url?.host != url.host { + } else if oldValue?.urlForWebView?.host != url.host { // If the domain matches the previous value, just keep the same favicon favicon = nil } @@ -1031,7 +1036,7 @@ extension Tab: FaviconUserScriptDelegate { for documentUrl: URL) { guard documentUrl != .error else { return } faviconManagement.handleFaviconLinks(faviconLinks, documentUrl: documentUrl) { favicon in - guard documentUrl == self.content.url, let favicon = favicon else { + guard documentUrl == self.content.urlForWebView, let favicon = favicon else { return } self.favicon = favicon.image @@ -1098,7 +1103,7 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift // credential is removed from the URL and set to TabContent to be used on next Challenge self.content = .url(navigationAction.url.removingBasicAuthCredential(), credential: credential, source: .webViewUpdated) // reload URL without credentialss - request.url = self.content.url! + request.url = self.content.urlForWebView! navigator.load(request) } } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index d052a791b9..369a036ba2 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -129,6 +129,8 @@ extension TabContent { if let settingsPane = url.flatMap(PreferencePaneIdentifier.init(url:)) { return .settings(pane: settingsPane) + } else if url?.isDuckPlayer == true, let (videoId, timestamp) = url?.youtubeVideoParams { + return .url(.duckPlayer(videoId, timestamp: timestamp), credential: nil, source: source) } else if let url, let credential = url.basicAuthCredential { // when navigating to a URL with basic auth username/password, cache it and redirect to a trimmed URL return .url(url.removingBasicAuthCredential(), credential: credential, source: source) @@ -190,19 +192,20 @@ extension TabContent { } } - var url: URL? { - userEditableUrl - } + // !!! don‘t add `url` property to avoid ambiguity with the `.url` enum case + // use `userEditableUrl` or `urlForWebView` instead. + /// user-editable URL displayed in the address bar var userEditableUrl: URL? { - switch self { - case .url(let url, credential: _, source: _) where !(url.isDuckPlayer || url.isDuckURLScheme): - return url - default: - return nil + let url = urlForWebView + if let url, url.isDuckPlayer, + let (videoID, timestamp) = url.youtubeVideoParams { + return .duckPlayer(videoID, timestamp: timestamp) } + return url } + /// `real` URL loaded in the web view var urlForWebView: URL? { switch self { case .url(let url, credential: _, source: _): @@ -290,10 +293,10 @@ extension TabContent { var canBeBookmarked: Bool { switch self { - case .subscription, .identityTheftRestoration, .dataBrokerProtection: + case .newtab, .onboarding, .none: return false - default: - return isUrl + case .url, .settings, .bookmarks, .subscription, .identityTheftRestoration, .dataBrokerProtection: + return true } } diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 7624a298b5..139c9c2b97 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -179,7 +179,7 @@ extension TabExtensionsBuilder { HistoryTabExtension(isBurner: args.isTabBurner, historyCoordinating: dependencies.historyCoordinating, trackersPublisher: contentBlocking.trackersPublisher, - urlPublisher: args.contentPublisher.map { content in content.isUrl ? content.url : nil }, + urlPublisher: args.contentPublisher.map { content in content.isUrl ? content.urlForWebView : nil }, titlePublisher: args.titlePublisher) } add { diff --git a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift index 66f84f5ebd..4aaee77a92 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift @@ -151,7 +151,8 @@ final class TabSnapshotExtension { @MainActor func renderWebViewSnapshot() async { - guard let webView, let tabContent, let url = tabContent.url else { + guard let webView, let tabContent, + let url = tabContent.userEditableUrl, !url.isDuckURLScheme else { // Previews of native views are rendered in renderNativePreview() return } diff --git a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift index 9d2d84628d..7fb5446cb5 100644 --- a/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift +++ b/DuckDuckGo/Tab/TabLazyLoader/LazyLoadable.swift @@ -37,7 +37,7 @@ protocol LazyLoadable: AnyObject, Identifiable { extension Tab: LazyLoadable { var isUrl: Bool { content.isUrl } - var url: URL? { content.url } + var url: URL? { content.urlForWebView } var loadingFinishedPublisher: AnyPublisher { navigationStatePublisher.compactMap { $0 } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 327dc8975f..9c2f65619f 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -776,7 +776,7 @@ extension BrowserTabViewController: NSDraggingDestination { return true } - selectedTab.setContent(.url(url, source: .appOpenUrl)) + selectedTab.setContent(.contentFromURL(url, source: .appOpenUrl)) return true } diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 42ac6fcd45..ef1365ec9c 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -233,11 +233,11 @@ final class TabViewModel { } private func updateCanBeBookmarked() { - canBeBookmarked = !isShowingErrorPage && (tab.content.url ?? .blankPage) != .blankPage + canBeBookmarked = !isShowingErrorPage && tab.content.canBeBookmarked } private var tabURL: URL? { - return tab.content.url + return tab.content.userEditableUrl } private var tabHostURL: URL? { diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 1fe33c029c..ecce54a428 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1041,7 +1041,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), - let url = tabViewModel.tab.content.url else { + let url = tabViewModel.tab.content.userEditableUrl else { os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) return } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 02b3ab4af5..b7e81cacc9 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -259,7 +259,7 @@ final class TabBarViewItem: NSCollectionViewItem { }.store(in: &cancellables) tabViewModel.tab.$content.sink { [weak self] content in - self?.currentURL = content.url + self?.currentURL = content.userEditableUrl }.store(in: &cancellables) tabViewModel.$usedPermissions.assign(to: \.usedPermissions, onWeaklyHeld: self).store(in: &cancellables) diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 9e888b4146..cdf6d3e721 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -185,7 +185,7 @@ extension TabPreviewViewController { let title: String var tabContent: Tab.TabContent let shouldShowPreview: Bool - var addressBarString: String { tabContent.url?.absoluteString ?? "Default" } + var addressBarString: String { tabContent.userEditableUrl?.absoluteString ?? "Default" } var snapshot: NSImage? { let image = NSImage(size: size) diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 10758ce56d..1bea6538a8 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -174,12 +174,12 @@ extension WindowControllersManager { let firstTab = tabCollection.tabs.first, case .newtab = firstTab.content, !newTab { - firstTab.setContent(url.map { .url($0, source: source) } ?? .newtab) + firstTab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) } else if let tab = tabCollectionViewModel.selectedTabViewModel?.tab, !newTab { - tab.setContent(url.map { .url($0, source: source) } ?? .newtab) + tab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) } else { let newTab = Tab(content: url.map { .url($0, source: source) } ?? .newtab, shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode) - newTab.setContent(url.map { .url($0, source: source) } ?? .newtab) + newTab.setContent(url.map { .contentFromURL($0, source: source) } ?? .newtab) tabCollectionViewModel.append(tab: newTab) } } diff --git a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift index f06aa2ade3..9cbc7831bf 100644 --- a/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift +++ b/IntegrationTests/NavigationProtection/NavigationProtectionIntegrationTests.swift @@ -77,7 +77,7 @@ class NavigationProtectionIntegrationTests: XCTestCase { for i in 0.. Date: Fri, 26 Apr 2024 21:35:57 +0600 Subject: [PATCH 036/134] Trusted url indicator (#2665) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206381886331933/f PR task: https://app.asana.com/0/0/1207078384259689/f --- DuckDuckGo.xcodeproj/project.pbxproj | 6 - .../Chevron-Right-12-light.svg | 10 + .../Chevron-Right-12.svg | 10 + .../Chevron-Right-12.imageset/Contents.json | 22 ++ .../Contents.json | 15 ++ ...entity-Theft-Restoration-Multicolor-16.pdf | Bin 0 -> 3211 bytes .../Contents.json | 12 + ...rsonalInformationRemoval-Multicolor-16.pdf | Bin 0 -> 5305 bytes .../Contents.json | 12 + .../Settings-Multicolor-16.pdf | Bin 0 -> 7715 bytes .../NSAttributedStringExtension.swift | 35 ++- .../Extensions/NSStoryboardExtension.swift | 25 -- .../Common/Extensions/NSViewExtension.swift | 5 + .../Common/Extensions/URLExtension.swift | 19 +- .../View/AppKit/LoadingProgressView.swift | 2 +- DuckDuckGo/Menus/MainMenuActions.swift | 4 +- .../AddressBarButtonsViewController.swift | 54 ++--- .../View/AddressBarTextField.swift | 5 + .../View/AddressBarViewController.swift | 8 +- .../NavigationBar/View/MoreOptionsMenu.swift | 4 +- .../View/RecentlyClosedMenu.swift | 4 +- DuckDuckGo/Tab/Model/Tab.swift | 5 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 219 ++++++++++++------ IntegrationTests/Tab/AddressBarTests.swift | 2 +- .../Tab/ViewModel/TabViewModelTests.swift | 30 ++- 25 files changed, 347 insertions(+), 161 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf delete mode 100644 DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cf6f987387..54382a2539 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -637,7 +637,6 @@ 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FF67726B602B100D42879 /* FirefoxDataImporter.swift */; }; 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8A27DB69BC00471A10 /* PreferencesGeneralView.swift */; }; 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */; }; 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */; }; 3706FC95293F65D500E42796 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.swift */; }; @@ -1361,7 +1360,6 @@ 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */; }; @@ -3216,7 +3214,6 @@ 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStoryboardExtension.swift; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.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 = ""; }; @@ -7357,7 +7354,6 @@ B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */, AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */, 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */, - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, AA5C8F58258FE21F00748EB7 /* NSTextFieldExtension.swift */, 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */, @@ -10122,7 +10118,6 @@ 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */, - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */, B6B5F58A2B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */, 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */, @@ -11464,7 +11459,6 @@ AA8EDF2424923E980071C2E8 /* URLExtension.swift in Sources */, B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg new file mode 100644 index 0000000000..2b4a602355 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg new file mode 100644 index 0000000000..4faab69801 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json new file mode 100644 index 0000000000..7fea6d5282 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Chevron-Right-12.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Chevron-Right-12-light.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..c878d4b14f --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Identity-Theft-Restoration-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..30563fa58347e6eeb312cd1b29f83a1b4bcd5cc3 GIT binary patch literal 3211 zcmdT`dpwle8V-})d|OT-B$v-!lDS|g62pv32{XFvsc4LEGBWdxncyr9k7bZI zbt%E#82|_%9)!cA01$L@194PNvSs@$yV};9eCNZ`6LXx`p78U@y<2Mzjc8p^yS1wK_9?rP=aiajKRI@X zyml-S>22fsGtvf)sc&w$vMV$AHhpdd&kY(Hb=P{PuH{ZNjS{zC-T}WXRnf4r7q3%0 z-=UvyCTf4<&pHvdfodGAWUneC*ltznlJ%dCVuy?xI`D;7A+1rzqB_)5J1H>Mhs2<3nd#=*PGo9d@6mVL?%CW%A;kk7sTPCl(cV7h>0wn|u?p8VGL8-GaY0 zoHaDC$Ik6%`JZiy*%{hRI8mhO5uV1cOj~KD)gM9_$6UPFW0jCKvFTu2#hka={#)1? zYoH#@cKv1Ro9bsj##Xv7!IV6OpYG{z9{se-*26m>k`=g{JRiru?{?tNL=^^XfAixj z;~sJB_paQVUoT_kY5yf%BWDQ|?A^0nQ|qspTkbSo^(a{V;N@Zex|*kZ2{-qJHdCzA z?ARws-@ENdsaD&(=2mTeM&P+!y7@)5A^R)R@?L*bP1rTRyqdDe;J|sEaqY_?^?9@H ztIXkxqjN>rFRC}HFgbsZ+d~e#UlHE3XW;8*E3*tON$UtqpTA(^U`9?@0KGV|0z1oH1eH*>v)oXpXyEpl0^V;cjjDZegomiAk z{y{xDx9kUN{Rj)Eq`Oy=+ZH`rMA1vRWX2i@uublBR=HL38Pjz-yhCJbc_S6HV|Elg z#~iwgJ-&rI+;Z4a)2>i#9of{7s`F8!j=1IhjTdR@`PY9DY|HLy(=+qf5fADK^M7mJ zl%Je%_EL3i_SH44RoOl3X|o=f+vWWT`h*LweM&}GM-JAI6N{k9n*H{ zyw`UsZcg#b%12|I!b1KL_54Ey+S=c7QtE8eS7|m!s;s@QI^1S>#^92Ezs`a#_Az2A zfeY?zKG?>8vMhwTy!Bp;%ew3}7xW76LuxVg0qLhBf6a=!i(A@inkgyC)2Jt$PabAu z#n?G@8?m$*lC1JQD?FRJE+sXtZ?JaH_|fjzyL|z=!*^D*Z3g{QWA;mE3s*qzt;Li` zhg<%cK_0il1~`q&pE`#|>vw<~-DRh>fGn_Z|8LDB0jzW}dP_Ph;>%R{Bt_*z9XrMo&ei z#K2gh^~@rvy^J{viaTB$Y~u4Ix6kj7G8aDvQ`cm1;+3tt#g{z3+*_WhQ9eHSQ!XiN z7ejT(xXP|{yLNAuBfo z2WJO#f)sX8z-Sfv54Fbe|G3uRyrDP_3!?duK`%NmrNMPM*?hs2LuD(NIRM~S=xyXL`Bw}6eyZ? zxwepshYbV}6(%%gji0>i`I7F^^M)Mg57M+q$#s|WWQ62zY|4%L z#{I$}G21Exa7bvq;csUNC6r~!k)&AKMFFBo^5pSgkr3R7swm}=f|y$;bz%S%!D0Td zdJJV!N;~vgOzKH5m?wgG$cktLCcz+7Hj2Pya%f7NP)K$HbhE2C5y)0S^aM0;a>rh9 zEaFR;v40_gOkpD6r%|CN6e?C!p4^KmVn)MTr~AeWAU1#p2?RhUNYP)A5z+) zaz&AV$%%#p02*FDZzQ-jLYNCJ25mSIi)A6k(O?*fFXAH07mm)^4xzC?f-4j;1tMv5 Oh|36gz|xWuNc|@|u|MPh literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..a30ef3d53e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "PersonalInformationRemoval-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fbabea452359ca82df36653248989b67e106aeaf GIT binary patch literal 5305 zcmbVQU2j}95PiSD!Y`FtqDA|SV78QOyXYTbqzR5`rT!78Knzx_Ta_4KKF_H5YhF6H0BE%DV4+ugh4d*uOM z$g0EfcDueARxh@HU2V1(-#%C8ub2OA4#S^=)n@!DGY`%l91Mbpe%mZZq_C1g{sMbvoGM za#4#BMRQA+vKhRN#b)Og`gp!Bgb-zH24WP!0jIc}$Ywaju7UYry~7pHXDvEp{0xxb zL-j!w?V`36zJtE{#bL0uFPM;ixXUYCMva*C4T>`>|>vKep_A;l=>HMi$kA0-i4=p$xf zaIJYvHS4X#HRxz#LR2A4qN#8aEO*8tyF_0JVG6mDNcZ9$6D#+nU;1P!Wn;0E{v z(UWiXDC?(knB9Kum z8I^;4L+d*v0c(9Qh}{mdBbbEL3c)jHQ3lk`SkmBPs1-hvh_Mo+Hri4_pdLmbN06;C zAT?kv2-JbZtweV~ahA!DadHW6@2vwT2c&PdAQNO8@)(RSsfx;kb^<&GDsVnx5Ks(^ z43KmvqT*0vd@X{YdccWN3yU!h`UJ~)>{Y;(z(xeF3*z7kn}l1Ne`Oy^XRaBmQh_YF znV3uhTP4_u6cK#^Tu_BcYbq5=gm{CHl@LzJR?I^Ppa+3@7)Y8FoDV<&Ps3B*mf)}t zjL~|+`t-*9fa0k!F zOB?%?T+tjLqhUE3HDs?%9z+TxSA&kF^(4!z|8laiOz2Ou4c3C%v+y@c8f-%kiN@0C z3TH*It5}G*LL4OLNcZ?ZDNkkS3 ziQx#)lM&V;%GFTRnVVCVH-qZH&?`FSh)j@m))*;#ur)x$SPKLA0crD)ljM53qiN0P zyCz;d3X!QoW*b1weFt=CY$a-h55%0dj)d8z5u#Sv+167h+LB2YMUhg7YzA6JtRIS^ zI<(M;F7&IK%aAs-)L^ajnE{yrg1}dgG~3fx5BIR+mRiV@B6kYd5bc(!){l+p zIETb;Yq~D5Xj@K8__ZP3J+(2V$L@a0Rbpgl)RrkPiBQ1e0|>p@B!Gj{#B5oy0Pu=uOB1V7y$1N{DJ$aQJPyqE2ky74*kQfzH01*mU1R(`*aJrnx7)&Qg z3i`gE9rRPrbo6#pE+L%y^t_XZgwsM^+arlh9eMmPmQa)rV-%7;)s_<(UlAs?#IErH z*+dYdXQX<`XZJGWXSLd=pbwuk+P+s*c%Ko9Doi96r#j@unH%4^sRzFOY`Q{a;kTD+MF#J9B7_g9xN zRmll1)U4-x{{egLiTwv43rYCkSxAxt9;mP%{Cs^}U+>@D-TLu%`*y&K%`lx>{wllx zpvwf@e)zZ@M+))Ue)!zG3x$kcvQmz#Wed w`fyP#eRQOa^TUytJhcmY+?QQ$hf{ej>n0n_~Fr`Z+?3DFQH!&HUIzs literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..d4b2052646 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Settings-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3b41002c28e578e1df10814f393e42ce63821bd GIT binary patch literal 7715 zcma*sQIFloaRuP_`z!9t0(Jo3VUz482^a?A-8cx6AR9Svh9BhF@mhg2lW63?@vqN! ziX*Koy<{GCsk&sdy6V)a)5UMT|L!-xXt(WhY1irc^oRevT(7_U<@Hy;x;#Aokbhq0 z&+T{r^6>cMhd*5x##g!P`NR8%*Y7TO-#z@#oBM}X|M=_cx4-@TU-!?K|Gu=_RDaj$ zmtw&iy8y>I=x+@^NS#}?D-|7&Av^LF(0+UDCat^MGHN0-({ zKVqEP*m#rmrmauIOWJL3(>{*t%$c!ey>{#SK5=HgjlLa=_1bOeZR*2%qCLh~_PVbf z%d}gs-R9-+bd00z+b+Vlt@Uki*~81c?lN)fZQZx|+5~3l^H{AXl>0mh!eQaX<}4?^ zt?h!tI1fI{?rrKvbFtsXG4?)HptofjEbBBc>ru|lqucLwO91AzpT4cj#?9^4k8Y7G zZw05%Ec3pMqsOu>f_ELaV{0>S;#i+8v9qm1tcSJYY@3gn&E}4FTmuEUo%d@OwmwZk zK%3{?3;S(4+G4-8*tX8nw%(?B+XN$viIW7&TVK|el*^TdniS=}fFM7%zAkNXF&%4fTYln>{7d!GM+MA_<^^8afNt5c zR$#P@dGYdf6Z<&0x@TT(?cl0_f#I~ZH96fRQWoyP`PlZ=vJ)SUZJVssEz#j6L6OnD zi5z71o-o1mz73XTou;yEYoFHaaoG3Xzz7(pC18Ae*=}Ma{PAlCq~veyJ1~?*pf{Fn zYfWA}*QR4?*4kxxZbHXbWmaqLlC`cQUhs7i%p(~U>v-zXDcBn>M9@8B>L3sOfi*Z} z<6Pl3#~^$L;w5jLW??VTukZ`O9I*qLPaP;7!4vDP_c{AytSU~i9b*BJb3GN^dsXyM z3-l37;P)>tDsioGLK~f=DCd~llF_QX7wB6)|Mq{a-X!$T{hKJke>C9&HI@8 z=C8*;t}F-m%9J1gX4Z>;(_RI+?3S@PCaJBeRqv~ddbF*gyu<1)-sgIUs7x{T2BREI^AsZiL%9UD$m zCdWJ}Ikrs^?WhxDj)4wh8-gfqmE)A~T0F`Gpl6cd&93WS3<7$dT7X5Zy25{nrz_q9 zX%YBdHCk)Vk_*(~`VG-8=bE-O**NbyqJeAAtN$&waL*9S?9CoJDqx!>3tsLo2f`b3 zewN>Xl9QXmGwP$lwU0J~8@^2}fyjMwJtn3$)^VC3V*;Xk^(q)CQ_digERrH#^f{~= zaz%~?-_osZU5 zi2kb6-e3L{X7D?>^&4{9^vidN!X&F(u#Gp89~2K1cmo3+{it5m6*uM;3kjF{gm2QH zRH#O_*y7~&zGCyj(jwJRY7a_(nsSpE%Fn$mpx!Pdnq9|vDL|~NzgWRSFv&~gTNN;hQJRXhxK;lQ+{Hp?fU~c6n0^28?=Fv*fB*kWn|a+Pbe>*Er4R(AZD2`n zMucBpoZ1G1tS9PpA+;&Cz(&uUU_!)A@xWw1la-bN=hU(CcU#MhnNPaxvR8NpJ|^MWguWm zYi-+dgdlV0lJ*?Iqn8mnWkjT*0W~$*T}%qf?wN3QS})T~>Q%bcG0LXn6SuSP1lKN3LE%(Q^Qo%OMVo$V09(DgFYbw#f?s!K{?aR^sGBJsDN-Y_wDKUBIAM>m#}EY)i2zJ?pVSE=33oqLd40WRcb&b>2og=8kg} zSs)l4X=@feL@2SyeU?g3muGxYHpDzb1Sf;aVYNl3&y6e#qGZwg@Sm;GCOg%edd|DG zWL-dOp3^1J+gj>43Y8X%LQU>vx8%e^;RrcmEV!Z5_z>>Z`?U=|jGPIwmJ|}X62YeT zS}V2$z_a|{w^KnKtAqs(a3@m5Aw}7qD+Ol#>b9XAHYp*0(<-ndnDGqzpy0AO;Iya# znbS-IaM&*v7Zi{Qzy*J^5%)y>zVysG-o(hSqnu3@9de<>By81rQdq)|YB&NvFXgxF zxTSW0(-BwoOL@YalBe-S;g@bx&O5#YfOX8$UB=Eq=nT66O;zv4grOx;YIJq))v4qw zne15VMSPKtQK%4T*=nHrT)4lWYMVJ>mEwtl5)O|~s6o*9Ai8#6jjURGHSCBb^sl9y z^Z|x`tTV`z^F@eIrxQV_3_PMct4Xd~IipN-e6bH`)hD>3zsfsK5gAI0j>QtxG`Nzs zG7n=PuZzgm*J>YB6QnV9X(*b?efl4mP-H-Qlv)ws zD1bCR>N;gtJ6UBEDe91vjDvJGN7H6c8jurNpdt$NsA#d_tO@LDF9#q+kE#v{RRFaa zA&LHEm9(929D@6t1=t+8cfvbP4uo9UCCwlrYw98gRa$F0z49$<%D)%oFyeW&hEB># z4@+mP&P%)ZyTWEIazrQ5Nze8|cX)kl$W$;-j821_*3{$FYzZ9C6JQHA;9riyAK#BR z9XMk}v`74>6muE9d!YZ)ti=^j8i) zY8V%$z{xZ{UO+#mGsl|8DvlfHg>pozPNf06k%G5JXs3WtZc96tVX|Gd|$%_L8 zCXO><%K!vUiKAFRef=j@ z0L9W0<$&C?R7pZ{HdNyzE58LVzo&Wmz1DOhaa@lTXTHe8c$8C{eubxU@=;?Qfs{=$ z&e@9mA|2~+kQxlZVr~)(JD*x1zT4vP1>5=%Cai%aRyDlUAvo{ou zTm6QS&{VCLF%gmoN1(1!h-qtDcIHqdYvwA#8tF*|sVd6<%r9|3BQeQs1A|I5)1m)H zc}3Hoc{U3OV%(V`uY{Xz=vP^9KkEx}P???A^vlraTuKwWdnVA=5Jb;bMKLaA1rxG*#eHm)dIktiH)C2l1iOx&tVW}BHnjzSo zipd}#)18!_zJgf2Q-f0Se8ZZgci_inI8k%1XvEW^6q6;lo4wXOs5NWhQEecd->@Y| zDQji2#=6G~ir$Y(7e&=bE5Uir%KVQ0XMSVxI24vAj50Kj^1En zg|p0}GoGx;7L4i~EKZKaorcUn!-7hKUqr0qxEX2%-@q)o%J#j=CSxO2a-XVQEy0pK zW~$=6=_nA3GXv45j#arLi(?yPa+ao&byz=V(F#6_nk6kcu1a8?BGD%G7gsalf&kDo zKd$=BrL%z~2^3_)Q_KwOfDNX096*&Jn3DWJOazZhx2fF${h zdJ96`366+VLUsBy>tKO6NR9<^A%9}RKmZeXeAR8!gfp9`q%yBFgV7K4V4c#=b+3$V zd|k*$a;34W+)NWRB^F@L7~TO$zpk@ZX3llGrk73gs3P?PS)MDYV6>Z9YkY)dj$CA4 zw(2!vBuYB-ZZjCQG$|#JJP&xtY)6%*krKOzwGLz$T_p(z?PX?fRS-~q=Ny12jjsNB zhnd!p5(H`$s$+8ikx7b)MP_THm)7s(Vv5AIbdeAasWe#*rw{`%`DWxWI>M+rnl@Z!l9O?n7&tNV2&Lr4Tq-6hD0k zy!-pd$EOd^*Wdl#he3aL|M9>7^>DrW{`HR!FNS}8_~Fg#UqAij`a8Aq<#)cQn)B1& zPgfsTdiC)9^yl~Y56{9ensF@5@KdH2t6 ze&FggJ9u^d_?~Z{{_NTImH*EypRJ^;e6~_H`26XGKW+Tm*B@TLefsflAN|Ywhd*A3 z?#sE?{P*f~2rbMnf52Z?*T+vL9p-OMsOM`Ff$_y%=6GM4K>3sD!~557-afp)_>6n^ zPu~g0&z_&&J^cLovo}Bd{0z$7tEZ extension NSAttributedString { /// These values come from Figma. Click on the text in Figma and choose Code > iOS to see the values. @@ -31,6 +32,31 @@ extension NSAttributedString { ]) } + convenience init(image: NSImage, rect: CGRect) { + let attachment = NSTextAttachment() + attachment.image = image + attachment.bounds = rect + self.init(attachment: attachment) + } + + convenience init(@NSAttributedStringBuilder components: () -> [NSAttributedString]) { + let components = components() + guard !components.isEmpty else { + self.init() + return + } + guard components.count > 1 else { + self.init(attributedString: components[0]) + return + } + let result = NSMutableAttributedString(attributedString: components[0]) + for component in components[1...] { + result.append(component) + } + + self.init(attributedString: result) + } + } extension NSMutableAttributedString { @@ -48,12 +74,3 @@ extension NSMutableAttributedString { } } - -extension NSTextAttachment { - func setImageHeight(height: CGFloat, offset: CGPoint = .zero) { - guard let image = image else { return } - let ratio = image.size.width / image.size.height - - bounds = CGRect(x: bounds.origin.x + offset.x, y: bounds.origin.y + offset.y, width: ratio * height, height: height) - } -} diff --git a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift b/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift deleted file mode 100644 index 9e50fa79ce..0000000000 --- a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSStoryboardExtension.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 AppKit - -extension NSStoryboard { - - static var bookmarks = NSStoryboard(name: "Bookmarks", bundle: .main) - -} diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index b82095c1f0..333dce57cf 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -79,6 +79,11 @@ extension NSView { return self } + var isShown: Bool { + get { !isHidden } + set { isHidden = !newValue } + } + func makeMeFirstResponder() { guard let window = window else { os_log("%s: Window not available", type: .error, className) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index abd290dd0f..f55e780cf3 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -142,7 +142,7 @@ extension URL { // base url for Error Page Alternate HTML loaded into Web View static let error = URL(string: "duck://error")! - static let dataBrokerProtection = URL(string: "duck://dbp")! + static let dataBrokerProtection = URL(string: "duck://personal-information-removal")! #if !SANDBOX_TEST_TOOL static func settingsPane(_ pane: PreferencePaneIdentifier) -> URL { @@ -409,6 +409,10 @@ extension URL { return false } + var isEmailProtection: Bool { + self.isChild(of: .duckDuckGoEmailLogin) || self == .duckDuckGoEmail + } + enum DuckDuckGoParameters: String { case search = "q" case ia @@ -552,7 +556,7 @@ extension URL { return false } - func stripUnsupportedCredentials() -> String { + func strippingUnsupportedCredentials() -> String { if self.absoluteString.firstIndex(of: "@") != nil { let authPattern = "([^:]+):\\/\\/[^\\/]*@" let strippedURL = self.absoluteString.replacingOccurrences(of: authPattern, with: "$1://", options: .regularExpression) @@ -563,7 +567,14 @@ extension URL { } public func isChild(of parentURL: URL) -> Bool { - guard let parentURLHost = parentURL.host, self.isPart(ofDomain: parentURLHost) else { return false } - return pathComponents.starts(with: parentURL.pathComponents) + if scheme == parentURL.scheme, + port == parentURL.port, + let parentURLHost = parentURL.host, + self.isPart(ofDomain: parentURLHost), + pathComponents.starts(with: parentURL.pathComponents) { + return true + } else { + return false + } } } diff --git a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift index 56f6b2de2b..9c444e511b 100644 --- a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift @@ -31,7 +31,7 @@ final class LoadingProgressView: NSView, CAAnimationDelegate { private var targetProgress: Double = 0.0 private var targetTime: CFTimeInterval = 0.0 - var isShown: Bool { + var isProgressShown: Bool { progressMask.opacity == 1.0 } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index ec129e39df..eca7f8004d 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -938,8 +938,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(findInPage), #selector(findInPageNext), #selector(findInPagePrevious): - return activeTabViewModel?.canReload == true // must have content loaded - && view.window?.isKeyWindow == true // disable in full screen + return activeTabViewModel?.canFindInPage == true // must have content loaded + && view.window?.isKeyWindow == true // disable in video full screen case #selector(findInPageDone): return getActiveTabAndIndex()?.tab.findInPage?.isActive == true diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 878114b49b..f277f679b6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -275,7 +275,7 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } bookmarkButton.setAccessibilityIdentifier("AddressBarButtonsViewController.bookmarkButton") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true - var showBookmarkButton: Bool { + var shouldShowBookmarkButton: Bool { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } var isUrlBookmarked = false @@ -287,7 +287,7 @@ final class AddressBarButtonsViewController: NSViewController { return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) } - bookmarkButton.isHidden = !showBookmarkButton + bookmarkButton.isShown = shouldShowBookmarkButton } func openBookmarkPopover(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) { @@ -299,7 +299,7 @@ final class AddressBarButtonsViewController: NSViewController { let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() if !bookmarkPopover.isShown { - bookmarkButton.isHidden = false + bookmarkButton.isShown = true bookmarkPopover.isNew = result.isNew bookmarkPopover.bookmark = bookmark bookmarkPopover.show(positionedBelow: bookmarkButton) @@ -319,7 +319,7 @@ final class AddressBarButtonsViewController: NSViewController { }() if query.permissions.contains(.camera) - || (query.permissions.contains(.microphone) && microphoneButton.isHidden && !cameraButton.isHidden) { + || (query.permissions.contains(.microphone) && microphoneButton.isHidden && cameraButton.isShown) { button = cameraButton } else { assert(query.permissions.count == 1) @@ -342,9 +342,7 @@ final class AddressBarButtonsViewController: NSViewController { return } } - guard !button.isHidden, - !permissionButtons.isHidden - else { return } + guard button.isShown, permissionButtons.isShown else { return } (popover.contentViewController as? PermissionAuthorizationViewController)?.query = query popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) @@ -389,7 +387,7 @@ final class AddressBarButtonsViewController: NSViewController { func updateButtons() { stopAnimationsAfterFocus() - clearButton.isHidden = !(isTextFieldEditorFirstResponder && !(textFieldValue?.isEmpty ?? true)) + clearButton.isShown = isTextFieldEditorFirstResponder && !textFieldValue.isEmpty updatePrivacyEntryPointButton() updateImageButton() @@ -690,15 +688,15 @@ final class AddressBarButtonsViewController: NSViewController { } private func updatePermissionButtons() { - permissionButtons.isHidden = isTextFieldEditorFirstResponder - || isAnyTrackerAnimationPlaying - || (tabViewModel?.isShowingErrorPage ?? true) + guard let tabViewModel else { return } + + permissionButtons.isShown = !isTextFieldEditorFirstResponder + && !isAnyTrackerAnimationPlaying + && !tabViewModel.isShowingErrorPage defer { showOrHidePermissionPopoverIfNeeded() } - guard let tabViewModel else { return } - geolocationButton.buttonState = tabViewModel.usedPermissions.geolocation let (camera, microphone) = PermissionState?.combineCamera(tabViewModel.usedPermissions.camera, @@ -771,21 +769,24 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel else { return } let url = tabViewModel.tab.content.userEditableUrl + let isNewTabOrOnboarding = [.newtab, .onboarding].contains(tabViewModel.tab.content) let isHypertextUrl = url?.navigationalScheme?.isHypertextScheme == true && url?.isDuckPlayer == false let isEditingMode = controllerMode?.isEditing ?? false let isTextFieldValueText = textFieldValue?.isText ?? false let isLocalUrl = url?.isLocalURL ?? false // Privacy entry point button - privacyEntryPointButton.isHidden = isEditingMode - || isTextFieldEditorFirstResponder - || !isHypertextUrl - || tabViewModel.isShowingErrorPage - || isTextFieldValueText - || isLocalUrl - imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true - || !privacyEntryPointButton.isHidden - || isAnyTrackerAnimationPlaying + privacyEntryPointButton.isShown = !isEditingMode + && !isTextFieldEditorFirstResponder + && isHypertextUrl + && !tabViewModel.isShowingErrorPage + && !isTextFieldValueText + && !isLocalUrl + + imageButtonWrapper.isShown = view.window?.isPopUpWindow != true + && (isHypertextUrl || isTextFieldEditorFirstResponder || isEditingMode || isNewTabOrOnboarding) + && privacyEntryPointButton.isHidden + && !isAnyTrackerAnimationPlaying } private func updatePrivacyEntryPointIcon() { @@ -796,7 +797,7 @@ final class AddressBarButtonsViewController: NSViewController { guard !isAnyShieldAnimationPlaying else { return } switch tabViewModel.tab.content { - case .url(let url, _, _): + case .url(let url, _, _), .identityTheftRestoration(let url), .subscription(let url): guard let host = url.host else { break } let isNotSecure = url.scheme == URL.NavigationalScheme.http.rawValue @@ -824,8 +825,7 @@ final class AddressBarButtonsViewController: NSViewController { let trackerAnimationImageProvider = TrackerAnimationImageProvider() private func animateTrackers() { - guard !privacyEntryPointButton.isHidden, - let tabViewModel else { return } + guard privacyEntryPointButton.isShown, let tabViewModel else { return } switch tabViewModel.tab.content { case .url(let url, _, _): @@ -835,7 +835,7 @@ final class AddressBarButtonsViewController: NSViewController { } var animationView: LottieAnimationView - if url.scheme == "http" { + if url.navigationalScheme == .http { animationView = shieldDotAnimationView } else { animationView = shieldAnimationView @@ -878,7 +878,7 @@ final class AddressBarButtonsViewController: NSViewController { shieldAnimations: Bool = true, badgeAnimations: Bool = true) { func stopAnimation(_ animationView: LottieAnimationView) { - if animationView.isAnimationPlaying || !animationView.isHidden { + if animationView.isAnimationPlaying || animationView.isShown { animationView.isHidden = true animationView.stop() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index c9692f1546..b4807c4cd6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -856,6 +856,11 @@ extension AddressBarTextField { } } +extension AddressBarTextField.Value? { + var isEmpty: Bool { + self?.isEmpty ?? true + } +} // MARK: - NSTextFieldDelegate extension AddressBarTextField: NSTextFieldDelegate { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index cac827debd..f53910b5ec 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -225,9 +225,9 @@ final class AddressBarViewController: NSViewController { passiveTextField.stringValue = "" return } - tabViewModel.$passiveAddressBarString + tabViewModel.$passiveAddressBarAttributedString .receive(on: DispatchQueue.main) - .assign(to: \.stringValue, onWeaklyHeld: passiveTextField) + .assign(to: \.attributedStringValue, onWeaklyHeld: passiveTextField) .store(in: &tabViewModelCancellables) } @@ -259,7 +259,7 @@ final class AddressBarViewController: NSViewController { .sink { [weak self] value in guard tabViewModel.isLoading, let progressIndicator = self?.progressIndicator, - progressIndicator.isShown + progressIndicator.isProgressShown else { return } progressIndicator.increaseProgress(to: value) @@ -274,7 +274,7 @@ final class AddressBarViewController: NSViewController { if shouldShowLoadingIndicator(for: tabViewModel, isLoading: isLoading, error: error) { progressIndicator.show(progress: tabViewModel.progress, startTime: tabViewModel.loadingStartTime) - } else if progressIndicator.isShown { + } else if progressIndicator.isProgressShown { progressIndicator.finishAndHide() } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index bfc0a1025b..4d2dbb909a 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -421,12 +421,14 @@ final class MoreOptionsMenu: NSMenu { .withImage(image) } - if tabViewModel.canReload { + if tabViewModel.canFindInPage { addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") .targetting(self) .withImage(.findSearch) .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") + } + if tabViewModel.canReload { addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") .targetting(self) .withImage(.share) diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index 3c24609a30..798e5f153f 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -71,10 +71,10 @@ private extension NSMenuItem { image = TabViewModel.Favicon.home title = UserText.tabHomeTitle case .settings: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.settings title = UserText.tabPreferencesTitle case .bookmarks: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.bookmarks title = UserText.tabPreferencesTitle case .url, .subscription, .identityTheftRestoration: image = recentlyClosedTab.favicon diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 99f9fcbf2c..2ec1087956 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -466,7 +466,10 @@ protocol NewWindowPolicyDecisionMaker { } else if content != self.content { self.content = content } - } else if self.content.isUrl { + } else if self.content.isUrl, + // DuckURLSchemeHandler redirects duck:// address to a simulated request + // ignore webView.url temporarily switching to `nil` + self.content.urlForWebView?.isDuckPlayer != true { // when e.g. opening a download in new tab - web view restores `nil` after the navigation is interrupted // maybe it worths adding another content type like .interruptedLoad(URL) to display a URL in the address bar self.content = .none diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index ef1365ec9c..e9726a5dc0 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -26,12 +26,14 @@ final class TabViewModel { enum Favicon { static let home = NSImage.homeFavicon + static let duckPlayer = NSImage.duckPlayerSettings static let burnerHome = NSImage.burnerTabFavicon - static let preferences = NSImage.preferences - static let bookmarks = NSImage.bookmarks - static let dataBrokerProtection = NSImage.dbpIcon - static let subscription = NSImage.subscriptionIcon - static let identityTheftRestoration = NSImage.itrIcon + static let settings = NSImage.settingsMulticolor16 + static let bookmarks = NSImage.bookmarksFolder + static let emailProtection = NSImage.emailProtectionIcon + static let dataBrokerProtection = NSImage.personalInformationRemovalMulticolor16 + static let subscription = NSImage.privacyPro + static let identityTheftRestoration = NSImage.identityTheftRestorationMulticolor16 } private(set) var tab: Tab @@ -62,7 +64,8 @@ final class TabViewModel { var loadingStartTime: CFTimeInterval? @Published private(set) var addressBarString: String = "" - @Published private(set) var passiveAddressBarString: String = "" + @Published private(set) var passiveAddressBarAttributedString = NSAttributedString() + var lastAddressBarTextFieldValue: AddressBarTextField.Value? @Published private(set) var title: String = UserText.tabHomeTitle @@ -80,6 +83,19 @@ final class TabViewModel { !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } + var canFindInPage: Bool { + guard !isShowingErrorPage else { return false } + switch tab.content { + case .url(let url, _, _): + return !(url.isDuckPlayer || url.isDuckURLScheme) + case .subscription, .identityTheftRestoration: + return true + + case .newtab, .settings, .bookmarks, .onboarding, .dataBrokerProtection, .none: + return false + } + } + init(tab: Tab, appearancePreferences: AppearancePreferences = .shared, accessibilityPreferences: AccessibilityPreferences = .shared) { @@ -117,7 +133,7 @@ final class TabViewModel { case .url(let url, _, source: .webViewUpdated), .url(let url, _, source: .link): - guard !url.isEmpty, url != .blankPage else { fallthrough } + guard !url.isEmpty, url != .blankPage, !url.isDuckPlayer else { fallthrough } // Only display the Tab content URL update matching its Security Origin // see https://github.com/mozilla-mobile/firefox-ios/wiki/WKWebView-navigation-and-security-considerations @@ -215,9 +231,8 @@ final class TabViewModel { } private func subscribeToPreferences() { - appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in - guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } - self.updatePassiveAddressBarString(showURL: newValue, url: url, hostURL: host) + appearancePreferences.$showFullURL.dropFirst().sink { [weak self] showFullURL in + self?.updatePassiveAddressBarString(showFullURL: showFullURL) }.store(in: &cancellables) accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } @@ -236,56 +251,62 @@ final class TabViewModel { canBeBookmarked = !isShowingErrorPage && tab.content.canBeBookmarked } - private var tabURL: URL? { - return tab.content.userEditableUrl - } - - private var tabHostURL: URL? { - return tabURL?.root + private func updateAddressBarStrings() { + updateAddressBarString() + updatePassiveAddressBarString() } - private func updateAddressBarStrings() { - guard tab.content.isUrl, let url = tabURL else { - addressBarString = "" - passiveAddressBarString = "" - return - } + private func updateAddressBarString() { + addressBarString = { + guard ![.none, .onboarding, .newtab].contains(tab.content), + let url = tab.content.userEditableUrl else { return "" } - if url.isFileURL { - addressBarString = url.absoluteString - passiveAddressBarString = url.absoluteString - return - } + if url.isBlobURL { + return url.strippingUnsupportedCredentials() + } + return url.absoluteString + }() + } - if url.isDataURL { - addressBarString = url.absoluteString - passiveAddressBarString = "data:" - return + private func updatePassiveAddressBarString(showFullURL: Bool? = nil) { + let showFullURL = showFullURL ?? appearancePreferences.showFullURL + passiveAddressBarAttributedString = switch tab.content { + case .newtab, .onboarding, .none: + .init() // empty + case .settings: + .settingsTrustedIndicator + case .bookmarks: + .bookmarksTrustedIndicator + case .dataBrokerProtection: + .dbpTrustedIndicator + case .subscription: + .subscriptionTrustedIndicator + case .identityTheftRestoration: + .identityTheftRestorationTrustedIndicator + case .url(let url, _, _) where url.isDuckPlayer: + .duckPlayerTrustedIndicator + case .url(let url, _, _) where url.isEmailProtection: + .emailProtectionTrustedIndicator + case .url(let url, _, _): + NSAttributedString(string: passiveAddressBarString(with: url, showFullURL: showFullURL)) } + } + private func passiveAddressBarString(with url: URL, showFullURL: Bool) -> String { if url.isBlobURL { - let strippedUrl = url.stripUnsupportedCredentials() - addressBarString = strippedUrl - passiveAddressBarString = strippedUrl - return - } + url.strippingUnsupportedCredentials() - guard let hostURL = tabHostURL else { - // also lands here for about:blank and about:home - addressBarString = "" - passiveAddressBarString = "" - return - } + } else if url.isDataURL { + "data:" - addressBarString = url.absoluteString - updatePassiveAddressBarString(showURL: appearancePreferences.showFullURL, url: url, hostURL: hostURL) - } + } else if !showFullURL && url.isFileURL { + url.lastPathComponent - private func updatePassiveAddressBarString(showURL: Bool, url: URL, hostURL: URL) { - if showURL { - passiveAddressBarString = url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) - } else { - passiveAddressBarString = hostURL.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() + } else if !showFullURL && url.host?.isEmpty == false { + url.root?.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() ?? "" + + } else /* display full url */ { + url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) } } @@ -332,41 +353,33 @@ final class TabViewModel { } } + // swiftlint:disable:next cyclomatic_complexity private func updateFavicon(_ tabFavicon: NSImage?? = .none /* provided from .sink or taken from tab.favicon (optional) if .none */) { guard !isShowingErrorPage else { favicon = errorFaviconToShow(error: tab.error) return } - switch tab.content { + favicon = switch tab.content { case .dataBrokerProtection: - favicon = Favicon.dataBrokerProtection - return + Favicon.dataBrokerProtection + case .newtab where tab.burnerMode.isBurner: + Favicon.burnerHome case .newtab: - if tab.burnerMode.isBurner { - favicon = Favicon.burnerHome - } else { - favicon = Favicon.home - } - return + Favicon.home case .settings: - favicon = Favicon.preferences - return + Favicon.settings case .bookmarks: - favicon = Favicon.bookmarks - return + Favicon.bookmarks case .subscription: - favicon = Favicon.subscription - return + Favicon.subscription case .identityTheftRestoration: - favicon = Favicon.identityTheftRestoration - return - case .url, .onboarding, .none: break - } - - if let favicon: NSImage? = tabFavicon { - self.favicon = favicon - } else { - self.favicon = tab.favicon + Favicon.identityTheftRestoration + case .url(let url, _, _) where url.isDuckPlayer: + Favicon.duckPlayer + case .url(let url, _, _) where url.isEmailProtection: + Favicon.emailProtection + case .url, .onboarding, .none: + tabFavicon ?? tab.favicon } } @@ -426,3 +439,61 @@ extension TabViewModel: TabDataClearing { } } + +private extension NSAttributedString { + + private typealias Component = NSAttributedString + + private static let spacer = NSImage() // empty spacer image attachment for Attributed Strings below + + private static let iconBaselineOffset: CGFloat = -3 + private static let iconSize: CGFloat = 16 + private static let iconSpacing: CGFloat = 6 + private static let chevronSize: CGFloat = 12 + private static let chevronSpacing: CGFloat = 12 + + private static let duckDuckGoWithChevronAttributedString = NSAttributedString { + // logo + Component(image: .homeFavicon, rect: CGRect(x: 0, y: iconBaselineOffset, width: iconSize, height: iconSize)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // DuckDuckGo + Component(string: UserText.duckDuckGo) + + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + // chevron + Component(image: .chevronRight12, rect: CGRect(x: 0, y: -1, width: chevronSize, height: chevronSize)) + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + } + + private static func trustedIndicatorAttributedString(with icon: NSImage, title: String) -> NSAttributedString { + NSAttributedString { + duckDuckGoWithChevronAttributedString + + // favicon + Component(image: icon, rect: CGRect(x: 0, y: iconBaselineOffset, width: icon.size.width, height: icon.size.height)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // title + Component(string: title) + } + } + + static let settingsTrustedIndicator = trustedIndicatorAttributedString(with: .settingsMulticolor16, + title: UserText.settings) + static let bookmarksTrustedIndicator = trustedIndicatorAttributedString(with: .bookmarksFolder, + title: UserText.bookmarks) + static let dbpTrustedIndicator = trustedIndicatorAttributedString(with: .personalInformationRemovalMulticolor16, + title: UserText.tabDataBrokerProtectionTitle) + static let subscriptionTrustedIndicator = trustedIndicatorAttributedString(with: .privacyPro, + title: UserText.subscription) + static let identityTheftRestorationTrustedIndicator = trustedIndicatorAttributedString(with: .identityTheftRestorationMulticolor16, + title: UserText.identityTheftRestorationOptionsMenuItem) + static let duckPlayerTrustedIndicator = trustedIndicatorAttributedString(with: .duckPlayerSettings, + title: UserText.duckPlayer) + static let emailProtectionTrustedIndicator = trustedIndicatorAttributedString(with: .emailProtectionIcon, + title: UserText.emailProtectionPreferences) + +} diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index 50d74ead32..e5297ed96b 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -215,7 +215,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "", "\(idx)") } else { XCTAssertFalse(isAddressBarFirstResponder, "\(idx)") - XCTAssertEqual(addressBarValue, tab.content.isUrl ? tab.content.userEditableUrl!.absoluteString : "", "\(idx)") + XCTAssertEqual(addressBarValue, tab.content == .newtab ? "" : tab.content.userEditableUrl!.absoluteString, "\(idx)") } } } diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 9dbaa4a0fd..d06378f00f 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -74,10 +74,32 @@ final class TabViewModelTests: XCTestCase { } @MainActor - func testWhenURLIsFileURLThenAddressBarIsFilePath() { + func testWhenURLIsFileURLAndShowFullUrlIsDisabledThenAddressBarIsFileName() { let urlString = "file:///Users/Dax/file.txt" let url = URL.makeURL(from: urlString)! - let tabViewModel = TabViewModel.forTabWithURL(url) + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: false)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) + + let addressBarStringExpectation = expectation(description: "Address bar string") + + tabViewModel.simulateLoadingCompletion(url, in: tabViewModel.tab.webView) + + tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in + XCTAssertEqual(tabViewModel.addressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, url.lastPathComponent) + addressBarStringExpectation.fulfill() + } .store(in: &cancellables) + waitForExpectations(timeout: 1, handler: nil) + } + + @MainActor + func testWhenURLIsFileURLAndShowFullUrlIsEnabledThenAddressBarIsFilePath() { + let urlString = "file:///Users/Dax/file.txt" + let url = URL.makeURL(from: urlString)! + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: true)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) let addressBarStringExpectation = expectation(description: "Address bar string") @@ -85,7 +107,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, urlString) addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) @@ -103,7 +125,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, "data:") + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, "data:") addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) From 7e629fabe0d3c8bc0109558bfcc4c68183721a80 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 17:48:12 +0200 Subject: [PATCH 037/134] macOS: Add pixels to track VPN wake and stop attempts (#2694) Task/Issue URL: https://app.asana.com/0/414235014887631/1207099030609186/f iOS PR:https://github.com/duckduckgo/iOS/pull/2785 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/797 ## Description Adds pixels to track VPN wake and stop attempts. --- DuckDuckGo.xcodeproj/project.pbxproj | 10 ++-- .../xcshareddata/swiftpm/Package.resolved | 9 --- .../NetworkProtectionPixelEvent.swift | 38 ++++++++++++ ...rkProtectionSubscriptionEventHandler.swift | 2 +- .../MacPacketTunnelProvider.swift | 60 +++++++++++-------- .../MacTransparentProxyProvider.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 9 files changed, 83 insertions(+), 44 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 54382a2539..34df2d094e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1089,7 +1089,6 @@ 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 */; }; - 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; 4B2D06322A11C1D300DE1F49 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; @@ -1494,7 +1493,6 @@ 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; - 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; @@ -3279,6 +3277,7 @@ 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAppEvents.swift; sourceTree = ""; }; 7B2E52242A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAgentNotificationsPresenter.swift; sourceTree = ""; }; 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarPopoverManager.swift; sourceTree = ""; }; + 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../../BrowserServicesKit; sourceTree = ""; }; 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionSimulateFailureMenu.swift; sourceTree = ""; }; 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 7B4CE8E626F02134009134B1 /* TabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTests.swift; sourceTree = ""; }; @@ -4716,6 +4715,7 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( + 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -10508,7 +10508,6 @@ 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, - 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, @@ -10670,7 +10669,6 @@ buildActionMask = 2147483647; files = ( 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, - 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, @@ -12726,7 +12724,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = "138.0.0-1"; + version = 140.0.3; }; }; 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { @@ -12742,7 +12740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 140.0.2; + version = 140.0.3; }; }; 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 d0e88eeb67..c903f1eed8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "3.0.0" } }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/BrowserServicesKit", - "state" : { - "revision" : "4340e2840f6ca634e61caffd1ced913603ddf0eb", - "version" : "140.0.2" - } - }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 2446ab0ac5..0a29c23807 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -34,10 +34,18 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionTunnelStartSuccess case networkProtectionTunnelStartFailure(_ error: Error) + case networkProtectionTunnelStopAttempt + case networkProtectionTunnelStopSuccess + case networkProtectionTunnelStopFailure(_ error: Error) + case networkProtectionTunnelUpdateAttempt case networkProtectionTunnelUpdateSuccess case networkProtectionTunnelUpdateFailure(_ error: Error) + case networkProtectionTunnelWakeAttempt + case networkProtectionTunnelWakeSuccess + case networkProtectionTunnelWakeFailure(_ error: Error) + case networkProtectionEnableAttemptConnecting case networkProtectionEnableAttemptSuccess case networkProtectionEnableAttemptFailure @@ -119,6 +127,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionTunnelStartFailure: return "netp_tunnel_start_failure" + case .networkProtectionTunnelStopAttempt: + return "netp_tunnel_stop_attempt" + + case .networkProtectionTunnelStopSuccess: + return "netp_tunnel_stop_success" + + case .networkProtectionTunnelStopFailure: + return "netp_tunnel_stop_failure" + case .networkProtectionTunnelUpdateAttempt: return "netp_tunnel_update_attempt" @@ -128,6 +145,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionTunnelUpdateFailure: return "netp_tunnel_update_failure" + case .networkProtectionTunnelWakeAttempt: + return "netp_tunnel_wake_attempt" + + case .networkProtectionTunnelWakeSuccess: + return "netp_tunnel_wake_success" + + case .networkProtectionTunnelWakeFailure: + return "netp_tunnel_wake_failure" + case .networkProtectionEnableAttemptConnecting: return "netp_ev_enable_attempt" @@ -300,9 +326,15 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, .networkProtectionTunnelStartFailure, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, + .networkProtectionTunnelStopFailure, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, .networkProtectionTunnelUpdateFailure, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, + .networkProtectionTunnelWakeFailure, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, @@ -343,7 +375,9 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { return error case .networkProtectionControllerStartFailure(let error), .networkProtectionTunnelStartFailure(let error), + .networkProtectionTunnelStopFailure(let error), .networkProtectionTunnelUpdateFailure(let error), + .networkProtectionTunnelWakeFailure(let error), .networkProtectionClientFailedToParseRedeemResponse(let error), .networkProtectionWireguardErrorCannotSetNetworkSettings(let error), .networkProtectionRekeyFailure(let error), @@ -356,8 +390,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionControllerStartSuccess, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, + .networkProtectionTunnelStopAttempt, + .networkProtectionTunnelStopSuccess, .networkProtectionTunnelUpdateAttempt, .networkProtectionTunnelUpdateSuccess, + .networkProtectionTunnelWakeAttempt, + .networkProtectionTunnelWakeSuccess, .networkProtectionEnableAttemptConnecting, .networkProtectionEnableAttemptSuccess, .networkProtectionEnableAttemptFailure, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index b1e4a36a7d..ecdc324254 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -17,11 +17,11 @@ // import Combine +import Common import Foundation import Subscription import NetworkProtection import NetworkProtectionUI -import Common final class NetworkProtectionSubscriptionEventHandler { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 842ff33ad8..2451f4e44c 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -233,6 +233,24 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .tunnelStopAttempt(let step): + switch step { + case .begin: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopAttempt, + frequency: .standard, + includeAppVersionParameter: true) + case .failure(let error): + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopFailure(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .success: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelStopSuccess, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + } case .tunnelUpdateAttempt(let step): switch step { case .begin: @@ -251,6 +269,24 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .tunnelWakeAttempt(let step): + switch step { + case .begin: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeAttempt, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .failure(let error): + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeFailure(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true) + case .success: + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionTunnelWakeSuccess, + frequency: .dailyAndCount, + includeAppVersionParameter: true) + } } } @@ -421,30 +457,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { try? loadDefaultPixelHeaders(from: options) } - // MARK: - Start/Stop Tunnel - - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - super.stopTunnel(with: reason) { - Task { - completionHandler() - - // From what I'm seeing in my tests the next call to start the tunnel is MUCH - // less likely to fail if we force this extension to exit when the tunnel is killed. - // - // Ref: https://app.asana.com/0/72649045549333/1204668639086684/f - // - exit(EXIT_SUCCESS) - } - } - } - - override func cancelTunnelWithError(_ error: Error?) { - Task { - super.cancelTunnelWithError(error) - exit(EXIT_SUCCESS) - } - } - // MARK: - Pixels private func setupPixels(defaultHeaders: [String: String] = [:]) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift index 5d9b0c0fa4..1c4993210e 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -27,7 +27,7 @@ import PixelKit final class MacTransparentProxyProvider: TransparentProxyProvider { - static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + static var vpnProxyLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "VPN Proxy") private var cancellables = Set() diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8c8bea9c39..f493ccd18a 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: "140.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 81a9dca9a3..71c4ddae58 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 32884eef9d..dad997f561 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: "140.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), .package(path: "../SwiftUIExtensions") ], targets: [ From 2723af26aab25580162551bbd546341f09af8cb7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 26 Apr 2024 22:03:00 +0600 Subject: [PATCH 038/134] duck page suggestions (#2666) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207078378083698/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/796 iOS PR: https://github.com/duckduckgo/iOS/pull/2784 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 9 ++++++++ .../Common/Extensions/URLExtension.swift | 4 ++++ .../View/AddressBarTextField.swift | 10 +++++---- .../View/AddressBarViewController.swift | 2 +- .../Model/PreferencesSection.swift | 2 +- .../Model/SuggestionContainer.swift | 22 +++++++++++++++++-- .../ViewModel/SuggestionViewModel.swift | 15 +++++++++++-- .../Tab/SearchNonexistentDomainTests.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Model/SuggestionContainerTests.swift | 2 +- .../SuggestionContainerViewModelTests.swift | 2 +- 14 files changed, 61 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 34df2d094e..84ae54f753 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12740,7 +12740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 140.0.3; + version = 141.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 c903f1eed8..9e19038261 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "3.0.0" } }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "89680cf5a4d784a1677676562bd96091dc153fc3", + "version" : "141.0.0" + } + }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index f55e780cf3..07683d29cc 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -148,6 +148,10 @@ extension URL { static func settingsPane(_ pane: PreferencePaneIdentifier) -> URL { return settings.appendingPathComponent(pane.rawValue) } + + var isSettingsURL: Bool { + isChild(of: .settings) && (pathComponents.isEmpty || PreferencePaneIdentifier(url: self) != nil) + } #endif enum Invalid { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index b4807c4cd6..c3494d313d 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -233,7 +233,7 @@ final class AddressBarTextField: NSTextField { case .suggestion(let suggestionViewModel): let suggestion = suggestionViewModel.suggestion switch suggestion { - case .website, .bookmark, .historyEntry: + case .website, .bookmark, .historyEntry, .internalPage: restoreValue(Value(stringValue: suggestionViewModel.autocompletionString, userTyped: true)) case .phrase(phrase: let phase): restoreValue(Value.text(phase, userTyped: false)) @@ -259,7 +259,7 @@ final class AddressBarTextField: NSTextField { switch self.value { case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { - case .phrase, .website, .bookmark, .historyEntry: return false + case .phrase, .website, .bookmark, .historyEntry, .internalPage: return false case .unknown: return true } case .text(_, userTyped: true), .url(_, _, userTyped: true): return false @@ -418,7 +418,8 @@ final class AddressBarTextField: NSTextField { switch suggestion { case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), .historyEntry(title: _, url: let url, allowedInTopHits: _), - .website(url: let url): + .website(url: let url), + .internalPage(title: _, url: let url): finalUrl = url userEnteredValue = url.absoluteString case .phrase(phrase: let phrase), @@ -802,7 +803,8 @@ extension AddressBarTextField { self = Suffix.visit(host: host) case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), - .historyEntry(title: _, url: let url, allowedInTopHits: _): + .historyEntry(title: _, url: let url, allowedInTopHits: _), + .internalPage(title: _, url: let url): if let title = suggestionViewModel.title, !title.isEmpty, suggestionViewModel.autocompletionString != title { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index f53910b5ec..7976bb9dc6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -364,7 +364,7 @@ final class AddressBarViewController: NSViewController { case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { case .phrase, .unknown: self.mode = .editing(isUrl: false) - case .website, .bookmark, .historyEntry: self.mode = .editing(isUrl: true) + case .website, .bookmark, .historyEntry, .internalPage: self.mode = .editing(isUrl: true) } } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index bff6930013..777b1a54dd 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -98,7 +98,7 @@ enum PreferencesSectionIdentifier: Hashable, CaseIterable { } -enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { +enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable, CaseIterable { case defaultBrowser case privateSearch case webTrackingProtection diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 5d4435eb83..94f4a59e46 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -30,15 +30,17 @@ final class SuggestionContainer { private let historyCoordinating: HistoryCoordinating private let bookmarkManager: BookmarkManager + private let startupPreferences: StartupPreferences private let loading: SuggestionLoading private var latestQuery: Query? fileprivate let suggestionsURLSession = URLSession(configuration: .ephemeral) - init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager) { + init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared) { self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating + self.startupPreferences = startupPreferences self.loading = suggestionLoading self.loading.dataSource = self } @@ -91,7 +93,23 @@ extension SuggestionContainer: SuggestionLoadingDataSource { return historyCoordinating.history ?? [] } - func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { + @MainActor func internalPages(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.InternalPage] { + [ + // suggestions for Bookmarks&Settings + .init(title: UserText.bookmarks, url: .bookmarks), + .init(title: UserText.settings, url: .settings), + ] + PreferencePaneIdentifier.allCases.map { + // preference panes URLs + .init(title: UserText.settings + " → " + $0.displayName, url: .settingsPane($0)) + } + { + guard startupPreferences.launchToCustomHomePage, + let homePage = URL(string: startupPreferences.formattedCustomHomePageURL) else { return [] } + // home page suggestion + return [.init(title: UserText.homePage, url: homePage)] + }() + } + + @MainActor func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { bookmarkManager.list?.bookmarks() ?? [] } diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index d7b8c5910c..c3a6d1aad7 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -94,7 +94,8 @@ struct SuggestionViewModel: Equatable { } else { return title ?? url.toString(forUserInput: userStringValue) } - case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _): + case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), + .internalPage(title: let title, url: _): return title case .unknown(value: let value): return value @@ -113,7 +114,8 @@ struct SuggestionViewModel: Equatable { } else { return title } - case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _): + case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), + .internalPage(title: let title, url: _): return title } } @@ -155,6 +157,8 @@ struct SuggestionViewModel: Equatable { dropScheme: true, dropTrailingSlash: true) } + case .internalPage: + return " – " + UserText.duckDuckGo } } @@ -174,6 +178,13 @@ struct SuggestionViewModel: Equatable { return .favoritedBookmarkSuggestion case .unknown: return .web + case .internalPage(title: _, url: let url) where url == .bookmarks: + return .bookmarksFolder + case .internalPage(title: _, url: let url) where url.isSettingsURL: + return .settingsMulticolor16 + case .internalPage(title: _, url: let url): + guard url == URL(string: StartupPreferences.shared.formattedCustomHomePageURL) else { return nil } + return .home16 } } diff --git a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift index b715d829e5..6ace162307 100644 --- a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift +++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift @@ -241,7 +241,7 @@ final class SearchNonexistentDomainTests: XCTestCase { addressBar.suggestionContainerViewModel = SuggestionContainerViewModel(isHomePage: true, isBurner: false, suggestionContainer: suggestionContainer) suggestionContainer.getSuggestions(for: enteredString) - suggestionLoadingMock.completion!(.init(topHits: [.website(url: url)], duckduckgoSuggestions: [], historyAndBookmarks: []), nil) + suggestionLoadingMock.completion!(.init(topHits: [.website(url: url)], duckduckgoSuggestions: [], localSuggestions: []), nil) addressBar.suggestionViewControllerDidConfirmSelection(addressBar.suggestionViewController) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index f493ccd18a..743db41a57 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: "140.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 71c4ddae58..a5da27a4c4 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "140.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index dad997f561..32f9d32616 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: "140.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index db15131a1c..fbd8316ade 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -44,7 +44,7 @@ final class SuggestionContainerTests: XCTestCase { withExtendedLifetime(cancellable) { waitForExpectations(timeout: 1) } - XCTAssertEqual(suggestionContainer.result?.all, result.topHits + result.duckduckgoSuggestions + result.historyAndBookmarks) + XCTAssertEqual(suggestionContainer.result?.all, result.topHits + result.duckduckgoSuggestions + result.localSuggestions) } func testWhenStopGettingSuggestionsIsCalled_ThenNoSuggestionsArePublished() { diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index cee0f7bec3..42beab1350 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -154,7 +154,7 @@ extension SuggestionResult { ] return SuggestionResult(topHits: topHits, duckduckgoSuggestions: [], - historyAndBookmarks: []) + localSuggestions: []) } } From 429d8974c87d896f68427ba7c3836d108478e3bd Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 26 Apr 2024 18:33:13 +0200 Subject: [PATCH 039/134] macOS: VPN Metadata Improvements (#2704) Task/Issue URL: https://app.asana.com/0/0/1207169059489741/f iOS: https://github.com/duckduckgo/iOS/pull/2791 BSK: https://github.com/duckduckgo/BrowserServicesKit/pull/799 ## Description - Add last IPC, controller, tunnel errors to VPN metadata - Updates VPN feedback metadata to include `ipsec` tunnel count. - Add `lastExtensionVersionRun` to VPN metadata in the App Store build --- DuckDuckGo.xcodeproj/project.pbxproj | 14 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../VPNOperationErrorRecorder.swift | 189 ++++++++++++++++++ .../NetworkProtectionTunnelController.swift | 3 + ...NetworkProtectionIPCTunnelController.swift | 9 +- .../MacPacketTunnelProvider.swift | 3 + .../VPNMetadataCollector.swift | 23 ++- .../TunnelControllerIPCService.swift | 13 ++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../TunnelControllerIPCClient.swift | 6 + .../TunnelControllerIPCServer.swift | 12 ++ LocalPackages/SubscriptionUI/Package.swift | 2 +- .../VPNFeedbackFormViewModelTests.swift | 3 +- 14 files changed, 267 insertions(+), 18 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 84ae54f753..27d34cce98 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1480,6 +1480,10 @@ 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; + 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; + 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */; }; 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; @@ -3277,10 +3281,10 @@ 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAppEvents.swift; sourceTree = ""; }; 7B2E52242A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAgentNotificationsPresenter.swift; sourceTree = ""; }; 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarPopoverManager.swift; sourceTree = ""; }; - 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../../BrowserServicesKit; sourceTree = ""; }; 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionSimulateFailureMenu.swift; sourceTree = ""; }; 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 7B4CE8E626F02134009134B1 /* TabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTests.swift; sourceTree = ""; }; + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; 7B5291882A1697680022E406 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeveloperID.xcconfig; sourceTree = ""; }; 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemScheduler.swift; sourceTree = ""; }; @@ -4715,7 +4719,6 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( - 7B427BA82BD81D8C0014AE6C /* BrowserServicesKit */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -8106,6 +8109,7 @@ 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */, + 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, ); path = AppAndExtensionAndAgentTargets; sourceTree = ""; @@ -9495,6 +9499,7 @@ 1DDD3EC52B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, + 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, 3706FAE3293F65D500E42796 /* ChromiumDataImporter.swift in Sources */, 3706FAE5293F65D500E42796 /* BackForwardListItemViewModel.swift in Sources */, @@ -10544,6 +10549,7 @@ 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, + 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, @@ -10580,6 +10586,7 @@ 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, + 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -11300,6 +11307,7 @@ B69B503C2726A12500758A2B /* StatisticsStore.swift in Sources */, 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */, 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, + 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, @@ -12740,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.0.0; + version = 141.0.1; }; }; 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 9e19038261..5ddc6f2245 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" : "89680cf5a4d784a1677676562bd96091dc153fc3", - "version" : "141.0.0" + "revision" : "786272601414243b391c22d370bf807e406a0e71", + "version" : "141.0.1" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift new file mode 100644 index 0000000000..7fa58ab46d --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift @@ -0,0 +1,189 @@ +// +// VPNOperationErrorRecorder.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 NetworkProtectionIPC + +@objc +final class ErrorInformation: NSObject, Codable { + let domain: String + let code: Int + + init(_ error: Error) { + let nsError = error as NSError + + domain = nsError.domain + code = nsError.code + } +} + +/// This class provides information about VPN operation errors. +/// +/// To be used in combination with ``VPNOperationErrorRecorder`` +/// +final class VPNOperationErrorHistory { + + private let ipcClient: TunnelControllerIPCClient + private let defaults: UserDefaults + + init(ipcClient: TunnelControllerIPCClient, + defaults: UserDefaults = .netP) { + + self.ipcClient = ipcClient + self.defaults = defaults + } + + /// The earliest error is the one that best represents the latest failure + /// + var lastStartError: ErrorInformation? { + lastIPCStartError ?? lastControllerStartError + } + + var lastStartErrorDescription: String { + lastStartError.map { errorInformation in + "Error domain=\(errorInformation.domain) code=\(errorInformation.code)" + } ?? "none" + } + + private var lastIPCStartError: ErrorInformation? { + defaults.vpnIPCStartError + } + + private var lastControllerStartError: ErrorInformation? { + defaults.controllerStartError + } + + var lastTunnelError: ErrorInformation? { + get async { + await withCheckedContinuation { (continuation: CheckedContinuation) in + ipcClient.fetchLastError { error in + if let error { + continuation.resume(returning: ErrorInformation(error)) + } else { + continuation.resume(returning: nil) + } + } + } + } + } + + var lastTunnelErrorDescription: String { + get async { + await lastTunnelError.map { errorInformation in + "Error domain=\(errorInformation.domain) code=\(errorInformation.code)" + } ?? "none" + } + } +} + +/// This class records information about recent errors during VPN operation. +/// +/// To be used in combination with ``VPNOperationErrorHistory`` +/// +final class VPNOperationErrorRecorder { + + private let defaults: UserDefaults + + init(defaults: UserDefaults = .netP) { + self.defaults = defaults + } + + // IPC Errors + + func beginRecordingIPCStart() { + defaults.vpnIPCStartError = nil + } + + func recordIPCStartFailure(_ error: Error) { + defaults.vpnIPCStartError = ErrorInformation(error) + } + + // VPN Controller Errors + + func beginRecordingControllerStart() { + defaults.controllerStartError = nil + + // This needs a special note because it may be non-obvious. The thing is users + // can start the VPN directly from the menu app, and in this case we want IPC + // errors to be cleared because they have priority in the reporting. Additionally + // if the controller is starting the VPN we can safely assume there was no IPC + // error in the current start attempt, so resetting ipc start errors should be fine, + // regardless. + defaults.vpnIPCStartError = nil + } + + func recordControllerStartFailure(_ error: Error) { + defaults.controllerStartError = ErrorInformation(error) + } +} + +fileprivate extension UserDefaults { + private var vpnIPCStartErrorKey: String { + "vpnIPCStartError" + } + + @objc + dynamic var vpnIPCStartError: ErrorInformation? { + get { + guard let payload = data(forKey: vpnIPCStartErrorKey) else { + return nil + } + + return try? JSONDecoder().decode(ErrorInformation.self, from: payload) + } + + set { + guard let newValue, + let payload = try? JSONEncoder().encode(newValue) else { + + removeObject(forKey: vpnIPCStartErrorKey) + return + } + + set(payload, forKey: vpnIPCStartErrorKey) + } + } +} + +fileprivate extension UserDefaults { + private var controllerStartErrorKey: String { + "controllerStartError" + } + + @objc + dynamic var controllerStartError: ErrorInformation? { + get { + guard let payload = data(forKey: controllerStartErrorKey) else { + return nil + } + + return try? JSONDecoder().decode(ErrorInformation.self, from: payload) + } + + set { + guard let newValue, + let payload = try? JSONEncoder().encode(newValue) else { + + removeObject(forKey: controllerStartErrorKey) + return + } + + set(payload, forKey: controllerStartErrorKey) + } + } +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index c2565dc405..b1bebcadbb 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -480,6 +480,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Starts the VPN connection /// func start() async { + VPNOperationErrorRecorder().beginRecordingControllerStart() PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt, frequency: .dailyAndCount) controllerErrorStore.lastErrorMessage = nil @@ -525,6 +526,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr frequency: .dailyAndCount) } } catch { + VPNOperationErrorRecorder().recordControllerStartFailure(error) + PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true ) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index 7e05352066..a39fcb6de4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -51,16 +51,19 @@ final class NetworkProtectionIPCTunnelController { private let loginItemsManager: LoginItemsManaging private let ipcClient: NetworkProtectionIPCClient private let pixelKit: PixelFiring? + private let errorRecorder: VPNOperationErrorRecorder init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, - pixelKit: PixelFiring? = PixelKit.shared) { + pixelKit: PixelFiring? = PixelKit.shared, + errorRecorder: VPNOperationErrorRecorder = VPNOperationErrorRecorder()) { self.featureVisibility = featureVisibility self.loginItemsManager = loginItemsManager self.ipcClient = ipcClient self.pixelKit = pixelKit + self.errorRecorder = errorRecorder } // MARK: - Login Items Manager @@ -84,9 +87,11 @@ extension NetworkProtectionIPCTunnelController: TunnelController { @MainActor func start() async { + errorRecorder.beginRecordingIPCStart() pixelKit?.fire(StartAttempt.begin) func handleFailure(_ error: Error) { + errorRecorder.recordIPCStartFailure(error) log(error) pixelKit?.fire(StartAttempt.failure(error), frequency: .dailyAndCount) } @@ -156,7 +161,7 @@ extension NetworkProtectionIPCTunnelController: TunnelController { } } -// MARK: - Start Attempts +// MARK: - Start Attempts: Pixels extension NetworkProtectionIPCTunnelController { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 2451f4e44c..3c71dc41a7 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -308,6 +308,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #else let defaults = UserDefaults.netP #endif + + NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = AppVersion.shared.versionAndBuildNumber + let settings = VPNSettings(defaults: defaults) let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 925ac292d2..1e9dd5d333 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -50,7 +50,8 @@ struct VPNMetadata: Encodable { struct VPNState: Encodable { let onboardingState: String let connectionState: String - let lastErrorMessage: String + let lastStartErrorDescription: String + let lastTunnelErrorDescription: String let connectedServer: String let connectedServerIP: String } @@ -119,11 +120,16 @@ protocol VPNMetadataCollector { final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter + private let ipcClient: TunnelControllerIPCClient + private let defaults: UserDefaults - init() { + init(defaults: UserDefaults = .netP) { let ipcClient = TunnelControllerIPCClient() ipcClient.register() + self.ipcClient = ipcClient + self.defaults = defaults + self.statusReporter = DefaultNetworkProtectionStatusReporter( statusObserver: ipcClient.connectionStatusObserver, serverInfoObserver: ipcClient.serverInfoObserver, @@ -163,7 +169,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private func collectAppInfoMetadata() -> VPNMetadata.AppInfo { let appVersion = AppVersion.shared.versionAndBuildNumber - let versionStore = NetworkProtectionLastVersionRunStore(userDefaults: .netP) + let versionStore = NetworkProtectionLastVersionRunStore(userDefaults: defaults) let isInternalUser = NSApp.delegateTyped.internalUserDecider.isInternalUser let isInApplicationsDirectory = Bundle.main.isInApplicationsDirectory @@ -232,7 +238,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { func collectVPNState() async -> VPNMetadata.VPNState { let onboardingState: String - switch UserDefaults.netP.networkProtectionOnboardingStatus { + switch defaults.networkProtectionOnboardingStatus { case .completed: onboardingState = "complete" case .isOnboarding(let step): @@ -244,13 +250,16 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } } + let errorHistory = VPNOperationErrorHistory(ipcClient: ipcClient, defaults: defaults) + let connectionState = String(describing: statusReporter.statusObserver.recentValue) - let lastErrorMessage = statusReporter.connectionErrorObserver.recentValue ?? "none" + let lastTunnelErrorDescription = await errorHistory.lastTunnelErrorDescription let connectedServer = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation ?? "none" let connectedServerIP = statusReporter.serverInfoObserver.recentValue.serverAddress ?? "none" return .init(onboardingState: onboardingState, connectionState: connectionState, - lastErrorMessage: lastErrorMessage, + lastStartErrorDescription: errorHistory.lastStartErrorDescription, + lastTunnelErrorDescription: lastTunnelErrorDescription, connectedServer: connectedServer, connectedServerIP: connectedServerIP) } @@ -279,7 +288,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectVPNSettingsState() -> VPNMetadata.VPNSettingsState { - let settings = VPNSettings(defaults: .netP) + let settings = VPNSettings(defaults: defaults) return .init( connectOnLoginEnabled: settings.connectOnLogin, diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index 971dc24522..b88c3f20b8 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -127,6 +127,19 @@ extension TunnelControllerIPCService: IPCServerInterface { completion(nil) } + func fetchLastError(completion: @escaping (Error?) -> Void) { + Task { + guard #available(macOS 13.0, *), + let connection = await tunnelController.connection else { + + completion(nil) + return + } + + connection.fetchLastDisconnectError(completionHandler: completion) + } + } + func resetAll(uninstallSystemExtension: Bool) async { try? await networkExtensionController.deactivateSystemExtension() } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 743db41a57..6a112190f2 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: "141.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a5da27a4c4..6e9e546e9a 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index 61996e3945..a3aafbbe11 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -172,6 +172,12 @@ extension TunnelControllerIPCClient: IPCServerInterface { }, xpcReplyErrorHandler: completion) } + public func fetchLastError(completion: @escaping (Error?) -> Void) { + xpc.execute(call: { server in + server.fetchLastError(completion: completion) + }, xpcReplyErrorHandler: completion) + } + public func debugCommand(_ command: DebugCommand) async throws { guard let payload = try? JSONEncoder().encode(command) else { return diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index 0d72a0d7ce..ef5a8d015a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -45,6 +45,10 @@ public protocol IPCServerInterface: AnyObject { /// func stop(completion: @escaping (Error?) -> Void) + /// Fetches the last error directly from the tunnel manager. + /// + func fetchLastError(completion: @escaping (Error?) -> Void) + /// Debug commands /// func debugCommand(_ command: DebugCommand) async throws @@ -71,6 +75,10 @@ protocol XPCServerInterface { /// func stop(completion: @escaping (Error?) -> Void) + /// Fetches the last error directly from the tunnel manager. + /// + func fetchLastError(completion: @escaping (Error?) -> Void) + /// Debug commands /// func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) @@ -174,6 +182,10 @@ extension TunnelControllerIPCServer: XPCServerInterface { serverDelegate?.stop(completion: completion) } + func fetchLastError(completion: @escaping (Error?) -> Void) { + serverDelegate?.fetchLastError(completion: completion) + } + func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) { guard let command = try? JSONDecoder().decode(DebugCommand.self, from: payload) else { completion(IPCError.cannotDecodeDebugCommand) diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 32f9d32616..b3e2665354 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: "141.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index ee1758f401..17b489a705 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -103,7 +103,8 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { let vpnState = VPNMetadata.VPNState( onboardingState: "onboarded", connectionState: "connected", - lastErrorMessage: "none", + lastStartErrorDescription: "none", + lastTunnelErrorDescription: "none", connectedServer: "Paoli, PA", connectedServerIP: "123.123.123.123" ) From e965388df49895b3e2671ade11383edf8076bbcd Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 26 Apr 2024 19:57:47 +0200 Subject: [PATCH 040/134] Update BSK to 141.1.1 (#2713) Task/Issue URL: https://app.asana.com/0/414235014887631/1207182733795625/f Description: Update BSK to 141.1.1 No changes to macOS. Contained updates are to prepare for Stripe repurchase flow on macOS. Steps to test this PR: Verify that tests are green. --- ###### 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 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 27d34cce98..536b2b14f4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12748,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.0.1; + version = 141.1.1; }; }; 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 5ddc6f2245..0b63176b7c 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" : "786272601414243b391c22d370bf807e406a0e71", - "version" : "141.0.1" + "revision" : "9ebcfd17a2dd1422407a24e9e4331a46c3b7733a", + "version" : "141.1.1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 6a112190f2..1a2906ce45 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: "141.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6e9e546e9a..761b9691ca 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index b3e2665354..408f9cbfe2 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: "141.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 92592bbfde536a89a5182a2216153d1152d96c5f Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 26 Apr 2024 12:51:33 -0700 Subject: [PATCH 041/134] Update BSK for iOS on-demand changes (#2668) Task/Issue URL: https://app.asana.com/0/414235014887631/1206396485779556/f Tech Design URL: CC: Description: This PR updates BSK for an iOS on-demand change. Nothing on macOS should change. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 536b2b14f4..086948af44 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12748,7 +12748,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.1.1; + version = 141.1.2; }; }; 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 0b63176b7c..64aca2b97a 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" : "9ebcfd17a2dd1422407a24e9e4331a46c3b7733a", - "version" : "141.1.1" + "revision" : "f8c73292d4d6724ec81f98bd29dbb2061f1a8cf6", + "version" : "141.1.2" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 1a2906ce45..7657755407 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: "141.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 761b9691ca..45900c0243 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 408f9cbfe2..7c57479333 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: "141.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), .package(path: "../SwiftUIExtensions") ], targets: [ From 36aa51f3be75f3705cf9b8bb24d766d031c96e5b Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Sun, 28 Apr 2024 21:28:02 +0200 Subject: [PATCH 042/134] Automatically clear data upon quitting (#2600) Task/Issue URL: https://app.asana.com/0/1177771139624306/1205062321200340/f **Description**: Adding Burn on Quit feature for macOS. Upon standard exit, data are cleared and the fire animation is presented. If macOS is restarting, the app delays the restart until all data have been cleared. In the event of an unexpected termination, such as a crash or force quit, data clearing is handled at the next startup. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + DuckDuckGo/Application/AppDelegate.swift | 19 + DuckDuckGo/Application/AutoClearHandler.swift | 125 +++ .../Burn-Original-large.pdf | Bin 43425 -> 0 bytes .../Images/BurnAlert.imageset/Contents.json | 2 +- .../Images/BurnAlert.imageset/Fire-96x96.pdf | Bin 0 -> 17864 bytes .../Common/Extensions/NSAlertExtension.swift | 31 + DuckDuckGo/Common/Localizables/UserText.swift | 13 + .../Utilities/UserDefaultsWrapper.swift | 4 + DuckDuckGo/Localizable.xcstrings | 779 +++++++++++++++++- .../Model/DataClearingPreferences.swift | 35 + .../Preferences/Model/SearchPreferences.swift | 5 + .../Model/StartupPreferences.swift | 25 +- .../View/PreferencesDataClearingView.swift | 15 +- .../View/PreferencesGeneralView.swift | 17 +- .../View/PreferencesRootView.swift | 3 +- .../StatePersistenceService.swift | 1 + .../Assets.xcassets/Colors/Contents.json | 6 + .../LinkBlueColor.colorset/Contents.json | 78 ++ .../SyncUI/Resources/Localizable.xcstrings | 146 +++- .../AppDelegate/AutoClearHandlerTests.swift | 83 ++ .../DataClearingPreferencesTests.swift | 4 + 22 files changed, 1362 insertions(+), 35 deletions(-) create mode 100644 DuckDuckGo/Application/AutoClearHandler.swift delete mode 100644 DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf create mode 100644 LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json create mode 100644 LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json create mode 100644 UnitTests/AppDelegate/AutoClearHandlerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 086948af44..1caeda173f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -128,6 +128,8 @@ 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */; }; 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */; }; 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; + 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; }; + 1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; }; 1DFAB51D2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; }; 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; }; @@ -2821,6 +2823,7 @@ 1DDF075F28F815AD00EDFBE3 /* BWStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWStatus.swift; sourceTree = ""; }; 1DDF076028F815AD00EDFBE3 /* BWError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWError.swift; sourceTree = ""; }; 1DDF076128F815AD00EDFBE3 /* BWResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWResponse.swift; sourceTree = ""; }; + 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoClearHandler.swift; sourceTree = ""; }; 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtension.swift; sourceTree = ""; }; 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtensionTests.swift; sourceTree = ""; }; 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; @@ -6404,6 +6407,7 @@ 858A798226A8B75F00A75A42 /* CopyHandler.swift */, 1D36E65A298ACD2900AA485D /* AppIconChanger.swift */, CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */, + 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */, ); path = Application; sourceTree = ""; @@ -10131,6 +10135,7 @@ 3706FC96293F65D500E42796 /* HorizontallyCenteredLayout.swift in Sources */, 3706FC97293F65D500E42796 /* BookmarksOutlineView.swift in Sources */, 3706FC98293F65D500E42796 /* CountryList.swift in Sources */, + 1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, 4B37EE732B4CFF0800A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */, 3706FC99293F65D500E42796 /* PreferencesSection.swift in Sources */, B6C8CAA82AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, @@ -10952,6 +10957,7 @@ 37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */, 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, + 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3776582F27F82E62009A6B35 /* AutofillPreferences.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 5b6d2a1eb4..fef5473cfe 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -74,6 +74,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let internalUserDecider: InternalUserDecider let featureFlagger: FeatureFlagger private var appIconChanger: AppIconChanger! + private var autoClearHandler: AutoClearHandler! private(set) var syncDataProviders: SyncDataProviders! private(set) var syncService: DDGSyncing? @@ -315,6 +316,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #if DBP DataBrokerProtectionAppEvents().applicationDidFinishLaunching() #endif + + setUpAutoClearHandler() } func applicationDidBecomeActive(_ notification: Notification) { @@ -352,6 +355,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } stateRestorationManager?.applicationWillTerminate() + // Handling of "Burn on quit" + if let terminationReply = autoClearHandler.handleAppTermination() { + return terminationReply + } + return .terminateNow } @@ -550,6 +558,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PixelKit.fire(GeneralPixel.importDataInitial, frequency: .legacyInitial) } } + + private func setUpAutoClearHandler() { + autoClearHandler = AutoClearHandler(preferences: .shared, + fireViewModel: FireCoordinator.fireViewModel, + stateRestorationManager: stateRestorationManager) + autoClearHandler.handleAppLaunch() + autoClearHandler.onAutoClearCompleted = { + NSApplication.shared.reply(toApplicationShouldTerminate: true) + } + } + } func updateSubscriptionStatus() { diff --git a/DuckDuckGo/Application/AutoClearHandler.swift b/DuckDuckGo/Application/AutoClearHandler.swift new file mode 100644 index 0000000000..f1044dae5f --- /dev/null +++ b/DuckDuckGo/Application/AutoClearHandler.swift @@ -0,0 +1,125 @@ +// +// AutoClearHandler.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 Combine + +final class AutoClearHandler { + + private let preferences: DataClearingPreferences + private let fireViewModel: FireViewModel + private let stateRestorationManager: AppStateRestorationManager + + init(preferences: DataClearingPreferences, + fireViewModel: FireViewModel, + stateRestorationManager: AppStateRestorationManager) { + self.preferences = preferences + self.fireViewModel = fireViewModel + self.stateRestorationManager = stateRestorationManager + } + + @MainActor + func handleAppLaunch() { + burnOnStartIfNeeded() + restoreTabsIfNeeded() + resetTheCorrectTerminationFlag() + } + + var onAutoClearCompleted: (() -> Void)? + + @MainActor + func handleAppTermination() -> NSApplication.TerminateReply? { + guard preferences.isAutoClearEnabled else { return nil } + + if preferences.isWarnBeforeClearingEnabled { + switch confirmAutoClear() { + case .alertFirstButtonReturn: + // Clear and Quit + performAutoClear() + return .terminateLater + case .alertSecondButtonReturn: + // Quit without Clearing Data + appTerminationHandledCorrectly = true + restoreTabsOnStartup = true + return .terminateNow + default: + // Cancel + return .terminateCancel + } + } + + performAutoClear() + return .terminateLater + } + + func resetTheCorrectTerminationFlag() { + appTerminationHandledCorrectly = false + } + + // MARK: - Private + + private func confirmAutoClear() -> NSApplication.ModalResponse { + let alert = NSAlert.autoClearAlert() + let response = alert.runModal() + return response + } + + @MainActor + private func performAutoClear() { + fireViewModel.fire.burnAll { [weak self] in + self?.appTerminationHandledCorrectly = true + self?.onAutoClearCompleted?() + } + } + + // MARK: - Burn On Start + // Burning on quit wasn't successful + + @UserDefaultsWrapper(key: .appTerminationHandledCorrectly, defaultValue: false) + private var appTerminationHandledCorrectly: Bool + + @MainActor + @discardableResult + func burnOnStartIfNeeded() -> Bool { + let shouldBurnOnStart = preferences.isAutoClearEnabled && !appTerminationHandledCorrectly + guard shouldBurnOnStart else { return false } + + fireViewModel.fire.burnAll() + return true + } + + // MARK: - Burn without Clearing Data + + @UserDefaultsWrapper(key: .restoreTabsOnStartup, defaultValue: false) + private var restoreTabsOnStartup: Bool + + @MainActor + @discardableResult + func restoreTabsIfNeeded() -> Bool { + let isAutoClearEnabled = preferences.isAutoClearEnabled + let restoreTabsOnStartup = restoreTabsOnStartup + self.restoreTabsOnStartup = false + if isAutoClearEnabled && restoreTabsOnStartup { + stateRestorationManager.restoreLastSessionState(interactive: false) + return true + } + + return false + } + +} diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Burn-Original-large.pdf deleted file mode 100644 index 2208d560684cf0225cd340b626bb4ff0110e1e21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43425 zcmeHw2b@#I_CKJ4fY?Rxp@v18VfU8YB=@eni|NVkrYE~ekdkuK+a@>JB#I3M74=yV zQ9+tCr9J@>l-@)|ks>11LbD)96Wf38P1%LrMYHeye*f(Ina^jlH*;ss%$)BzXXc!l zJGYNUEGnvaJxP2*#nCiHL~a&a0=^Cj0e zDq{8c$q3C~Jzy;h;X%4kh+=^*uyl|j=nSy{Rr*pqxQvhT2^`}JL?Rf$5FScM2uvy< z1O!ivpa_mvl<~w;E)Nq4aW0RGiBL=+#u2^*mGEIy$ibv&#SlmnA>%YkC<1_qQq~B? z@E`8hr;kJ}Z3!cUZcs)Lq#i#-Mj)x*N|9pH9&(V7g2l(7YBK1e+$hh<0FkMxe~!4~1) zj?!Fx#e)8v%hsN(nYHh-cI`X1`*wM!Vp@PQ)H0&fs5wfKvO27km4z~l(pX(&lm*xh zm}W?ZNk|0l`9rP-F$C<-c0~}OgrUS>yaZ+;B^Y2D!s7BvI6MFm4=sU*KtexBI_NAg z$U^_|u*}yGNE8ZEWRQxofNX)vM3GRqg`CyaD5VTJJWdbk=r1lsVH8GyOd@*arM@B=jh@FfAsS$v(FdDjt3UeJEO=cc(gLtQ(V)sb%=pR=#iu#X7oPNP#g zCU$U>ZPJrlI!#^pubD4UXBK*o^m0F1QCWIs;$|D=J4@|2rj3=~>~T%``r6q`*d3n; zpIbNOh527n^Z)hC7dKUvZebt3@zZy9Z2S6&`@#oT*K$VccHOlU>T|{v}WJKq9dHjdgfduc@{G5+&is%^`<{?yL7`?YyhTW@*2WAE-WO2c<7s!%*HQ}T*C zKlx1g*77ws%=%ht`_cJq*H^z19t?hS_WfDB4P}4CPOdsMdG)PB9lhVs_DK5r4O>6p zg=^Owz5DtjJ~%mZK>IaUs~2o8|FY**s_#B{rLxz%JH_$t+pjwJoq6`)N1~mE7I(bn zoNiieVAY9<6MGH4v1VwGfotEt@7*`H-g@7hJ|l}3j^44Z$MpU0&)OO2KKA4j8^;8S zx{V~qPU~m?$GQy%p7?&%!>kTH@7c0qclr05SN-Gh%C&DU*|BKzb4TW^=oGSFelWZd zerwcq1G^oKeE(zdoxIYWyUj#m^!gLG-UHu%Wti$khI*6+4v6r$?>}WN*T_q(NWbedB|n_g{PV#RY5a z-3C9jpyRV#zxBD>F4tbExU&8ALiw&M1mY`hl!zaF<3)65fRd^u6HDgV=U%;Y#Ia8= zI}IN@f-ma+*)tPw#$^*#t{K~o-*Nmbakl>W@xA-sJ3H~Gc9Z)}e`E0uxNEzocTWDe z@~QRRZ@SU;dgpeRbzyb5t{s}3TT*;?uTBS^-%z!{vZwQ}V_qBD=~>TvMb(>E!9V`- zMDb@wyS2Ng2i~dw*u{|vch}8(gMXy@Opgi4=`Z#jrM=!r@-!c2e)A+FUpHJUBOZ(NYdv1JFyV=|Ro}+!2 z3kERvE9k)%tbH_62dlqAG)t+x|AKxD{Jk@8w zw{MQ^I1(21oj>;IP2*p;OnbrekM`qN{xkufvD=n-Zlq+&>ea#r@7Q|xW9R3;x>7S^ z-q%!=3l<*_{56aR-D^1 z;@phn0RAnMb;XL)r%zXSr|;Ql_=JCc($y>PIsWd)8>v07o*Z7c8GU)-=7(NB)_4BA zH+aVuS|(P{TleUo+rAuj+qs{T3DAKdrro{u}9 zKdV1jv29w-+*iiD;P_^EWZH`010SCJ1ovB58uAvc7KPp!|%T3vgNBE2w(fu$bYWx{G%*<`~98g zJv?~$_0=Os5ARrZ#eCM4MS`my?`jrY{Veoc%<54jqG=D^p68OTzl&pS!O>!myF&zw}?ttXV-tx|4uDnZFK4lCn-=#SBcz9I!X;_)4yz}~>#qE(t$BPHDYOa0t ziTBpcU3b~KuhvzqTfWYG{Syw!$s)* z(#u@+1M8aS&ptb-&#=Dxd#{|jp#QwFaAm1P;CcO&c0>P-{cj$yZEUT9XT0a>$%i!T zS>JE(b?kv-J&pyA-EiyEhs0f{lb>#1^v$APyN|mkZJXhSj`UpCZ-@|@`j4aE9=YSw ze{Nejb&wUlCHRzE+H379)5cvN;p@-6a$?U@zC-rDT>bdPFF!s0Hr{gonehtiGSoi4 z>Sg7Jf{a!z$iD8J&<(v}{^+xV_gx;^Fu{Ji`!(0T z;T5*upZNLCI}R>CG5y!O9=PKV)*q#ROn+>m9vae*T%Jug8AI9$Oecy@^i$*;5+e5$2JabLYrB^?9wRd#;(I5Bx z{mGZ@i0LNYqZ2ncdj@WMq36@j+}*QZ&tXL836Y7V6RyVh*3Bp#Gx387ohGPDKVFJ2 zef^z9OTUY~bF{MVrsd<72jk`6p8rsJVBGN~n*)2kU$IKZx(Vxirb}=+XV0`fzCE(N z^HbNBue<%l+Xt7gIQ3}x$g&U0Wo1xm_0ZKrz4PY|ed*oK`=;!(9M>NIhj1AzHTB!W zo^bNuFR!1c&OP-@*_qgh;!}GM+_&@W;$hDYdvX8x{l(YG#tuAo?QJ)VIDGslboP!D zD{ozOrTj{FSN!@P1Cwhfqv62^CXHMw@Ce>~MAh-P-uRuSJKi01PVjh@T(RlCUi@`s zb)%Nn{+jIH3*#*+KK0cnH(OU*M=cm>eejx_N7arx-g)K9dluaD&T#+gy;sh>(!B1O z$KRI^{MVf)f7|pvkU{fy_jdl)EnA;FGI8r!Tbb=H+nM!c&zv5!W8(ui{!)E?UvppL z_R;J5Y=~`Cu8V#d{c7}AD^=l2O`q?S;}us``c-AGiRG1w-Oo}YZ@(9->Z@GOi=Ka} z&m`59nU0xP&Ak7GV=upgDf!5}Oh~m?)LMG$T~K@`YZTX1#w-)ED9O zj^6nbWS%-_+?=;Q-}Bv<-<5x^j@qK*LJ> z*%bz1|M#KK^(RLT9KyTnRUm>ZYD;^+w2A-09d}K=>9Qv~ZN8FRd2*;(Y<}?b5ynTo z(~b#}hM$N3+oIJXb>+l(H8ar{aCI=?6MvZec5ei?haWq0adAH6g8tw1XI<;fx6Z$Gp7 z%%Ir0*s1#aR~9|EexYjlmTxB3*Y=&a=L>S@^yAAG-gWUpmjfS3E4df63`}r+1(Jj4uxC+VSxZp<^qTBZ03D zw)aoI@4zq0^{;N}*754#{_4H{J!Qn$^Xhi5w;%lb_@yu3`SPH_~~o+yf$yrZIi}KIzQPndDj%#lqFO9P8~O`R)T#!rz*=sL!Gai*H!`H0O&9b|fEnZ%- zeCE5o-hK7Go8J59iY_Z2UGc}t)XLMV!mGYp?OOfSnqh0cSZi9leVumQmi1NZH*Jt^ zSi4cMapk7+P4B*6`u;nc@y&04!24jyhujYrf5iQ0@fO~eB_H!YUit~~$+E3wTUUHK z=+o8P#M?G(S8V@ahib>xoyMKJKD+m`1G^l%j(qO_{KW3~?q9xmXwMaUp4@xw-f{bS z?wh)Q!2bCMcn97)C_eb%m-;Wi_{#p(w_iuU{`H$j4|O{H0dOT@NUh1-QEQYSFrpW*s7Q>2^h#^g2eJmqv~!yC z(jdE)DHjT-%Qg5{2<^*eDip!M5heIMj1Lk^cqoU9f@y9E@cqFh@JNwM-WG=fR!@*| zWkC~%K+;&yPI*E>+NooqbO5IWZd5PrPq(IC4$8&oR>6Fj3mP^A`YZL~I2VKrvMwV+ z2R$-%3qvvB{exBmr+|Ta1_9w0oRfZ=-`5j_N?X2~?tM6rW;z;ViU4 z$|LZ5tkDdN4Ssgo!v>CbC<2;QO!pHSg@F`>ws_0cxdn z*?TwF?Ihy!p-wDN{Kyz z=IhR)nC1$|Mh2rmnt-4+BSSzIWS1t>eEs1@LVf`&!41%34wRE>Ahcx;d#RHio~R0I zIPCY>t+aSUAZr=aaI2wpbbiW1`AJ5-a#GLenZQq7m`c;96~*tN^{>=#4FaAfiXEiW z8uL>{6~b5~SgekCfB~`k%b;}D3rVC?FQm-0?BYh00d|#U?UWcqD=TQ@P>dkO2yCE@ z16;}kRDp>ogPP=R)IsJR9g$7`B`OU$FQOp=A#M-_{|hZ?l(RV+p@<itIp`t;& zStu6_BK?(CyC+D6qV56delgTCWiBFTaK{3+AXrw2Uc_XY^B3|sjcgV@`hM6NiITK7 zmltK#xv?NMhypFQz#NsPYr52ymXG1If0-1qH7C0fB zKml&Lv=X7fC=t4hQlZNy6Kc(sqI#E1=yFwy1)(ZYeW;2dC>9EoVnIFqrxgn@jYNQ} z#X_Y)D%2VziF%tX5w*(`xHI=xFOduD?JBVVHiH_>GPhAD)rFY*48dH+#)bakHH3qX zYFz>y=&qn%qDyyKpwJ1lN*(B$L||olFBj0g(}GO7k5($ESI7Wc$9bMW6z)z@-&|G{6x+D5wNrDRqGVQiV>D(AG#~U`fkkH%pAl zu*@w?)F!kZjgTQ|q~va8!s5Y2x}+;E@oQ5$gRibCY367|Dx)^&2?PqzeT7)41>&MLOGH2*Y&37(60uMu(JPG-sZpnLD_oX@ z%OXiK1Vc7?y;LdJVS1HFT2tw<$0eysu}2>eRll)f%j;3e?r{l2oLQ9fzzy(14f- zL=sxW#JGSkVs(6}$=vvhA(&1x2Js|mv1xSGhG3;$&-2w)lN=$(%&x?xay!?>gG}`> zUaiKxNpa9B*Cr83oeb8cs#7G2!#u>ot4AX$hG3N?7zkB+1OboI#Zw5`sRU%fHSv^B zsjZQ ztNdQC$D-#l1clLrnQQPV5?nvm9dQOAqr>2{=oGLtT$uodWN7Cw49IFj24GBhDNHZ5N+Hdg)BO!S*+1GYJ&biz>?(p z30ZY8xM>j~IotX5ZB^;{|(GDaX(rL0EI z!^4O~tdUnKcnJkNkyKUsWtwV{g{$+4aSyJ<1J=sQh&s*?)LCtcT91x2m@31#J}f6f zHbP{;)pkROs>D#ER$UKlt;rgwlZi!k7l$AB5Os(=84pYOAq)Rh~ z!IMFH)Qkts7E0&|dT@1}M{V-qQa8dj$^E#V?<1W7TnlLt4&;exbVA%wPoWA?JfxxHAsECpk%W=$at1<>-y4A3ST#H4XS)dzmy_c9!X{s}0rlZk0k1w0 zbyecEMz6x6z>Rt}=&CU$(`xLd6cYBkNlt(vSeLMxWd7=y-T>>pHhZlw#HogALRK!9 z@WObQD-<}51_RGlr-T*elurY@YKcIMs*0+FxX*-yf(c#Nnq-H#sM5y}jD&PXHrMKs zyMyKwj4Ne+zr-GsKoS_Q^db^MSBE7v8V?wF;!;W<xIFPPp5TOhh@ncR4R_v zI6RRSz8Hcn-t|PJkw~&u;Z)+9K(&r5q4*(6&!&PxqRJvi9WI@*j!;2@AkU7#gs|3Z zu`>iMAwmah_$IrVji|whBJ|6pa<+uflEJu~Co?1iMxEO1is*3z&l=N9tBt5oAyi16 zHkHd3u?2V@l_{PyBV04?X9y;ECZ|aqHNl}6VU5ciT#H{WQ)>`$(qeWwksz)_*gja0 zNUHTZEb5CVqI{c)sFSM%=8)K(0M@L|%0~^{sL7~K6RdHgA-2&{U7fPo*jAO&L)aYg z2;r*|2Dl-kwYG{9COLIAe8>=}OY-EdlvgD%alC}W!FBQVGJ`>Gu!v1KYKo;tJ=j^D zvY70ZHbX68!62hsE;G22Qg4l3X2Pm!>O@s8W5lHLD+FG9t=v$r3?+%m5FW3uRn&6) zdRq;o7PBoXy+>tU8j)+sswBw$*(gx zQhc3EtoItQl-*0{cp|oo&*rgB_LNB#GCDUoM!Av&XhD;EW^V*?9-*`B^ZKv zH{%jS8G`@0_SK*a!AlL?`L>=R*erK1(Jizp4x!re@&>0Z?U7yVv*i;Tp#Aq|9bGy? zpj4JBf?#V~&`v7E6|tDdQEKJ#35<(zifvrdTI|FzxR@Yeyx7Kf@Er~g-)@J~&gg|@ z8=9D*D-PLXbZ`&Q1r?Pz0BH{FM7RXkS!}b}c*U60<|rlzI~P;}+Xx)NcrfW;x(-M; zD&B;2Hbl}W57Tt67=>{H#R!g2Di$ILfnWlhOTc0d!i8~BuG!6$Z&HU2M4df!>__Cc zMxzzsNGJq+emV@4X4^&go0J!m5l@_SNF$*D6CC~Xa6uUpJ}mV#;6d}v1NcS~fbn*O zQ;d;TXEDZc62&+GqS%f&VZPNyqCAJa3~Exmp-pt)DSg|~6qS6aH!9v7^-v~ul|L}) z)-_YUxoW@$zZS4bley@A1BonnjlgLX`N;^b+!F=C(qy9>H2dg~kDjc%Fg1Gv1oZL> z2{G~AQY*8hR1viTIicYLG2d)jLz5dqLg@_6h=&d1*$4;5D@M66R*YFOARZ2$qnPBw zIA*tjp${jr^cxj#xsG}_8BB)@0X-BoAaE&ym2$boFrNt*rdz_~YMDqh`p_>^>1K-sQk6D}^&ys<0{=>D2TY=Zp7U8FnH2$V}i zdK=|xnaG6@{FOlK|0;B8Krd9rtY$Y=-m*pu9Hn5JZ-k14HIa}LY$mVJL_nyx47x;) z1}0rVClgc#3xmzaqA#R*QSL@GfrSSPE2VO4w8j(l*!-kAPDa9Xt16`QYB|%bmU3K* zjv-FQZb{S2cUXKKJ8%`qC@20HXwN=14xDpQC_X|WKq&%g>+ zj~juPqf3)+X~@oHYRz&^Lt&=nQHI?N3u@gS@CX63+)~;oxtaD~L?Zz9WK`&6!c1A_ z=bx9NXm)9IY7p$!XG$_je_k>IMopB<%@m@4{dpNzgrw&Lxl-xhe_qb!kAaoPTruWn zi=~>^g&eoQ#eWttp&xA7Xe)?uS_2p-ZMTj7U%-ROq8_#cSFYKWF#^#$WTt!rWPP(LNG&WD{6*iN11%|3T6Zsu~F3&6|vt?>Qq+|ZK`c08;#CX<^jRcNY5V+W2T z0gBhQu*0%qmJfD0WzA3TvQ-r#c3c`e+H|%h#cS1E1*}20M)PvEwh+R^fnb^X;o|!;bjM}yfhC*v&OdIG>@LKFd?!w1sWC10EvWhU|#s;y!%3f zkC|_#(Q8wd6C6jLiI?P~e7Tj?yubsm1<2G@Xy}0vFT0-Jw$RIQG#~VG+ZysjFB=yv zH1xotP!>IK*`{_waI>4a^1?5xvU%Z0WT&cy1|JBJW#I#Q*oK^SG&h0F2R$?kd4A{> zs`BNgoM4rxEn$~iqss@oTzDog><~0N!7Vh^0~1Xpm}mlr0lZPC4WWk+S&rs|9@5G< zS;6i_0-!0&L5K|ado+A)#$iBm9L)>9+@m~sYF~D^EHv$7W}V>6T+XB6Yda1fg0xEg za;`>R_~F^{vf$vOjVC}3z(>zIa~tdm-e1q{>dQy_a*M2aftL$z7aDlX$Or6a2E1(K zzTmM#h^&>*2RwwERoJ}H125GpP=_5X%Vy%#%=%v2Sy)H1r{CuF93_%lFvttP+;)mW z(>}&yXG|wp>T6RYU#nuBx!G!7=y40xVP~eDV9d)re3IMwR`|xVRSrA9RSUGa=|G|B z9)ljA9{JL{;B(uP3m!fq3q(FcUn|#%3RU+QpMgP-v7G!iGjYjTvwXnInU=f^Cz@qS zVX2-zOxjt+tjzP4tZYCpFSTp^K`9h{4inJ$+zCa@}|et^WTbsOOx;e3Tu$si!qtw+r=w_>0s%>wx`tt9!%gNckY2wbyxRdqY+K zmbN!{I$7?Nri?tcd^%T)r#!XhaHmG+hcSn6$s*QdG^Y~T)5)5#){P1>$BbnIp3ISE zOiGJ|TJBn!3IE#%kBOLvqv_cx?HGdbD%1bG)aL)kQ9HS*NV!j0KNe z?wQGakjoXy%VyG`)z)_?r$xC@mow@Oy<$mNy>@R!msX)=gMYq%DA#>s#T2mopOiXdc=+?;LF#6PAoL7e02mJ-2ybmn)Um zkcVRb5l^9jV~RrXRwFm{e>19>empC^>fL6Xt%k#-@{<4TXqA`v=k~-G;+z3&^vZ5o zXglEL%uqhyH9V7-A9{uQbY1%S9(ub_n|ds{VSyB=A-qa|y3A3-^m1C3B?Uj}%RL;P4|u%n5_?{X*RVCZ(7*%xW7{Hj zNUILxXf1eIef_t<`=6MvH%F84x#+i^>6?GCSs8dIYbki2pCe``BbnDsYQ$2O-(!nd zBS{v*1-rM(pbK(kR`%1?iOHxv;tA8QdS%{^lup%&r4?O3-KAjJD?n1#OTIcOxA)=S zy=Z33!M{tgY5Bh^Y08sJr2NgUOE9PDt-smTlqZ)+`I}vrU{2Fpf3vG8PcD)2H@hyu zoTj%f#jY-8gA!rzx=4~@fwzyB7u6RHs^|u;qEv*m2I#Ah&D)K`f?4$cvNDKy=`Wa6 zh{0=Q`-@BIcQPYjGX^-M9_A3mFy0S_`wd9v3Pt+2@RH?w`zx(>unRpDb>BN+0Hh2# zE=?{w5z5eOG7%a889bDq>@SQ(g2n30+u8?IFg>B$orsXmZe3s&2EKA17LUsXPoL2@ zm=705F&1;h3O1yHSkr}R7zaj)hBPW-1+QF=(5zwJ6O2I$@!&E($|rD)D-elb1VeZz zAt5lSfDjNoF@hpEUQxyqOSwEuB*eKqE+#@Tffz^l5>Q6UGDq7_3x^P}{8otefX O2(Me8J`%OG+y4XduDN0W diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json index 48111f0d3f..3f5be128a3 100644 --- a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Burn-Original-large.pdf", + "filename" : "Fire-96x96.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf b/DuckDuckGo/Assets.xcassets/Images/BurnAlert.imageset/Fire-96x96.pdf new file mode 100644 index 0000000000000000000000000000000000000000..297359ff71c645b5cf4fc43b5e3171962c063b8a GIT binary patch literal 17864 zcmeI)S#M;=bqDbG{uK8`fE_^1yDxwtAX*W`f@Mt}0t9)WIMj%ahEzySAlvYF&+lKi zySYlk(PG2_Il#>2>8e{*r_S;}OV4{>eEjMA&34*snyRiYzWURqs($yo>ge~U=dZ56 zsanJ@k^CHed3^rrwEB;#-qz)(YW5HRxf}8E#p~m<^B*1Z>6`PD>$8jVfBW-yr&Vv> z2On&%t}joIUvK_edG)fs`tpDXzPnlM)phmPs*u0C`0nmUS8u*Du32Zke)H|k zYmQ&OT<83)I{UgzI_&xG`wrx?LK&T9{D9%X%ahZqasgi)UtgbIp0CeM`RDWF>&vsex9PUy+&67k zz2EiQWf4Zf9$WL7vCJeJUf3?R{rSX;_~Iy zqbE9W?r_GZu5f$xqmM4Wy|{ew-SNq3b@cJ+|D2ties+2M=T)w54}9zM(O$x;2=q>N z-#Y8|fTOpI@$KPnecp4jhrI7>aiKBlg%Tv1S>}G4#z!b8n~pge=BBZ;xgDpevFovBd*-U!*5fqKqmRbUF>=ab z9+#=JUcDX1zH94hWXMNA9U>L^1 z!Fzo2ggc8H#L?ni(cEWbsPAaTMVdCJgSC53W?5Q0b2)Quq0D}SN*uc| zt%y=T&s}EYRX^}iMOfQqC_5+M<1n{&X~kh6AmpQs$SfJzZ|km`2b}K6H3rxt39s=o zYoqq=YpSa`M_4?VJUhYCkejI<#@5`2N_4XMX0x|V6&*eLqN%P<&Np9g&Nq*E_!ykk z!!J{R@bCoxAwC*!*`~h>K60?Q2kPGgAI;mg?FUBd z{efPL?Kn5p%@cXue@xW|>KF6IFx2xvG@t5P_kR5!7a>KYpApg?7VTaC{`VjJcW+7x zM0hHo`!x{~XTHD3|1Sus9kxr;jJyxu1<=406@r3 z`Dx)S7T&O?&Oiqj=b3+)n`PwpM=ppb?KzZw8{O}l3~#3n?rOKz>;`_&JLfw7Jq~;t zrX?<`=f=jkf3O4USh)HCxEV|tE3~d@I=F+QT6%#OQ^=^)UCk>^u!A_QjStPZWmp0o zxVwQ{hK(SD-~r&J1p;R1YP2b~zB9G&05ns9FwhB(fhM?2-qN@k{F(YpZKg#S?1(*F zg*Tl5Fng~}1qPOP1f)$1~^$Z1;h`NST-4DJ)OrVS>H!>EeM|0c6fS&X8T9fjV$Ch$o{kO_47F0$dN-lzTvJtw@Bsxf}7E%n^dl z6PVf&F#-W_gIi?I?8O-gQArAb#F7QM6Ck#+w*()e2MXo1%``$+d5`-7Kh5s(T&e&! zoM)so&VWWNMmH8tcYRG%(QIt73pYYV7rQVU51eel)z&Q!0C7gr#=$MS`9@ISKC_=z z2n2i@*DW(NXLh}vdOy)%D3$dv1W+-SgZh0(uQN56ds#Zq05Et6 zgyK^_3kc05NEQM*KCiDII$^@zbS zH#Mj{+KXc>)`gl~)4-r*<-O3t&Ba6oy|bE5>*}*kTQ8Gf@iCg&q5$y+17_{E7K5xY zPv15PFjgOA0CClC@b5jAw?SZDz8lWf=+67xPDYqij=RGO zeTmi>*Pd~)3uYh|znBD)z zBQtyExNKomiH^Er3d@bLd+y0C3(Exrw-Ks`-|mPY3m-`%S!8VzEa3t-!$C1wM&{P;JI8m7d;%nP*qxjmLZ9V8zLgqI zKz=|SKOFn))U5c?H3E4^*&*&HB;F3$y@I_S`<=bMX z@_Dkqhle|f=r!BCTPM-fVbMc0^ z!n)|;0w23@5RD7iUHH1KCy(E4{4oCA1%&(RByxv?NX%m#(Oz=`n|6t*9~Y3D^x7Q( zxtm{8^7!n<_3`y-$*z3TmVaJ+eth*`x3d5F^?vgI(Ww3CvnulUv|LBZ2fu&w`t1DT z>g@VK@!;Y6e>88f3;Pep`JwGT{*t3l*2>?{Ie0Q}|5yj#^J1T8DL4K_SN{2xYoNb& zPVbRA=a-*-_(MW*N7fF|rwSZ<;*I|7Qs$l^?~v;{`f26L{jJ|Q^!m$Q5ZLsK>!aM7&x9= z{R{a`Y4pFJ*1#a)BB!ZRTqPwU8<>Ln>qm>M@--=xgZi?yOV`Q=YHE;AlyNSd4)Wro zbS%=`DuhcH>NJqxE#+-l%$fqo^GSiOHDpQ4CJ;Tu`qDycl_521P+8u)Nda4uyOJf9#v9UJAk8Cy6+yN0K!ktl_gb5k&4DZZr#lD$p# zOJ?0oss(m2NJm(a3`i-{x*@GU@-?MiqKQOWR{~w7r`OzZ#zbzsQU6r09#T^2HD2H@Sr5%-!%(hNHQKOvEI|)fr*NQ3WiNPUlM&nw$R680a!=vfyFSN3xvsEY5Npll; zA;%MYl$vO4ZmH)jG8Add(V4@&&;rGzw%9dE={Zdn>V3|gj;_)^q)eoZB9f8&kf1Vd z>@jtfHPt8*Qc~LZzmsI@sZ>@*>V8WZ1czGk%NClA&rQ*Js^1hXK_wuJY3@X2DOJbp zhX_Tc*@V;pS=>ix)^;)}o>+$zW*8Z>)%a@Lur~RmIZ3D|7u1ucSP|thMj<DVWAQ+`G5T;?DTMo$)Jj531 za4Tg1pHfN{779lBW7Lq8#<`_fGLQI;M3k9e7s$Y*Sb4IcKa=_&qW8HCIV_uBQpYMv ztmI$@4>Y@98lF>NOA2do65eOSL#c@?0^)fo(H8!>rdW2DOdO1|D(j&3L9y5y;-^AL zCR1T0D{%#6AOf92j3sCl3r%uKvul}C>fcZSMN#I0Go&hU9;^<*O{F;%w&J1jm=97N zETt5lsub2D9s1Br=1|mG@)mo?M19ikLK({4)05~Z+mvFfy=7T-otBx2LXoD0FE8aG z9gWeah)9=$r@FpALHy^>CQHTk?ua&eAm%`h&0*nK{1H8V2!~Rnu`Yowod>c1JsxOm z2uf(U5@jEk>$Y;tbSV+UA=OAVItSuW)xq&6dRDATkPEpeY+(#k+fzBx2}O^>3Z>$- z>!s{ndgdrFH7JZvYlF5-bw@YEX7JLbwLvR_$the*46h9s8V<=hZR2u+h_H{Wv`rkM z!ZhW#1Nxg*$0D=~X^8cV2~zzn4uG*Kt}7Mjat!w0@U#<>WW`0yWwg|;8OH*l;4r&0 ztfMQXCKzDF+62fHxyj9hM&^POi8T;Oz@%4`do`vBDOu%U^EUzuSRB*De6KT z8Sp@knBW+{fx)drh0({jgp{FyB*F!3xvSOua|Jx0qRhzU>XOO#0CJbSRa=eTShL1- zGB-JNGApcsFn@**RM^0C((% zwG}MUV@U=eQFDM@j#P`T#FVQ}a>yKtfE$a?bUXHAJ-sNAAU6E~(0DLBD%8N{x8@H%#8Zol= zq92A$1qbw610ZqZ+H=5SG>uC9UCsbmvNxFxFr`Jw-IMf&O);CEh{STE)&ad46*LUg z1``XP>UNP|v)!(Pm(B_o#4V!n+-b{U|6k4A~uCp03-Y=>(=tZp1KM6Zc=y3KQUS`XZKtf625OKn%-7m1`{8FN~s zHx3Ce(t4r!ouOh9Y$XdPxE%s?6H0++B50n>*Jp~_Ad(-?A=b7K>CtpT0r3P;jH{m~ zB;MB*ATeGExNrt0^=2e%l~^T2?bwbPV1a>TITi^r$lEYqcUwpn(}xfeBhMc@g?mbz znI(jXy7DuQI*Epn(p}GDun5A_*l{ExTw;TEU9P zB{O<5;-Q?n$O;e1G3MM9&|+d7By3BEQ7jNl#p?6lBs?G?E+Kts#iW;N{pl!^XiFT@JPGMBkha(ME2T*-j*ixN4hqiTN
      z2l`X&xS_Z%;2b5s!cQ*#GPBo2!f0r@yU!bN2GLtAreVd2w+q kG~P*9b@bxwFQ>UlvT=2Ne0g2&R!}>&oA=)P NSAlert { + let alert = NSAlert() + alert.messageText = UserText.warnBeforeQuitDialogHeader + alert.alertStyle = .warning + alert.icon = .burnAlert + alert.addButton(withTitle: UserText.clearAndQuit) + alert.addButton(withTitle: UserText.quitWithoutClearing) + alert.addButton(withTitle: UserText.cancel) + + let checkbox = NSButton(checkboxWithTitle: UserText.warnBeforeQuitDialogCheckboxMessage, + target: DataClearingPreferences.shared, + action: #selector(DataClearingPreferences.toggleWarnBeforeClearing)) + checkbox.state = DataClearingPreferences.shared.isWarnBeforeClearingEnabled ? .on : .off + checkbox.lineBreakMode = .byWordWrapping + checkbox.translatesAutoresizingMaskIntoConstraints = false + + // Create a container view for the checkbox with custom padding + let containerView = NSView(frame: NSRect(x: 0, y: 0, width: 240, height: 25)) + containerView.addSubview(checkbox) + + NSLayoutConstraint.activate([ + checkbox.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor, constant: -10), // Slightly up for better visual alignment + checkbox.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor) + ]) + + alert.accessoryView = containerView + + return alert + } + @discardableResult func runModal() async -> NSApplication.ModalResponse { await withCheckedContinuation { continuation in diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index a201afff72..bd4d6505d3 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -53,6 +53,8 @@ struct UserText { static let pasteAndGo = NSLocalizedString("paste.and.go", value: "Paste & Go", comment: "Paste & Go button") static let pasteAndSearch = NSLocalizedString("paste.and.search", value: "Paste & Search", comment: "Paste & Search button") static let clear = NSLocalizedString("clear", value: "Clear", comment: "Clear button") + static let clearAndQuit = NSLocalizedString("clear.and.quit", value: "Clear and Quit", comment: "Button to clear data and quit the application") + static let quitWithoutClearing = NSLocalizedString("quit.without.clearing", value: "Quit Without Clearing", comment: "Button to quit the application without clearing data") static let `continue` = NSLocalizedString("`continue`", value: "Continue", comment: "Continue button") static let bookmarkDialogAdd = NSLocalizedString("bookmark.dialog.add", value: "Add", comment: "Button to confim a bookmark creation") static let newFolderDialogAdd = NSLocalizedString("folder.dialog.add", value: "Add", comment: "Button to confim a bookmark folder creation") @@ -1077,6 +1079,17 @@ struct UserText { static let fireproofCheckboxTitle = NSLocalizedString("fireproof.checkbox.title", value: "Ask to Fireproof websites when signing in", comment: "Fireproof settings checkbox title") static let fireproofExplanation = NSLocalizedString("fireproof.explanation", value: "When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button.", comment: "Fireproofing mechanism explanation") static let manageFireproofSites = NSLocalizedString("fireproof.manage-sites", value: "Manage Fireproof Sites…", comment: "Fireproof settings button caption") + static let autoClear = NSLocalizedString("auto.clear", value: "Auto-Clear", comment: "Header of a section in Settings. The setting configures clearing data automatically after quitting the app.") + static let automaticallyClearData = NSLocalizedString("automatically.clear.data", value: "Automatically clear tabs and browsing data when DuckDuckGo quits", comment: "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.") + static let warnBeforeQuit = NSLocalizedString("warn.before.quit", value: "Warn me that tabs and data will be cleared when quitting", comment: "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.") + static let warnBeforeQuitDialogHeader = NSLocalizedString("warn.before.quit.dialog.header", value: "Clear tabs and browsing data and quit DuckDuckGo?", comment: "A header of warning before clearing data on the application termination.") + static let warnBeforeQuitDialogCheckboxMessage = NSLocalizedString("warn.before.quit.dialog.checkbox.message", value: "Warn me every time", comment: "A label after checkbox to configure the warning before clearing data on the application termination.") + static let disableAutoClearToEnableSessionRestore = NSLocalizedString("disable.auto.clear.to.enable.session.restore", + value: "Disable auto-clear on quit to turn on session restore.", + comment: "Information label in Settings. It tells user that to enable session restoration setting they have to disable burn on quit. Auto-Clear should match the string with 'auto.clear' key") + static let showDataClearingSettings = NSLocalizedString("show.data.clearing.settings", + value: "Open Data Clearing Settings", + comment: "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key") // MARK: Crash Report static let crashReportTitle = NSLocalizedString("crash-report.title", value: "DuckDuckGo Privacy Browser quit unexpectedly.", comment: "Title of the dialog where the user can send a crash report") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 84c3835b1e..92e2f23e0b 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -55,6 +55,8 @@ public struct UserDefaultsWrapper { case grammarCheckEnabledOnce = "grammar.check.enabled.once" case loginDetectionEnabled = "fireproofing.login-detection-enabled" + case autoClearEnabled = "preferences.auto-clear-enabled" + case warnBeforeClearingEnabled = "preferences.warn-before-clearing-enabled" case gpcEnabled = "preferences.gpc-enabled" case selectedDownloadLocationKey = "preferences.download-location" case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location" @@ -78,6 +80,8 @@ public struct UserDefaultsWrapper { case lastCrashReportCheckDate = "last.crash.report.check.date" case fireInfoPresentedOnce = "fire.info.presented.once" + case appTerminationHandledCorrectly = "app.termination.handled.correctly" + case restoreTabsOnStartup = "restore.tabs.on.startup" case restorePreviousSession = "preferences.startup.restore-previous-session" case launchToCustomHomePage = "preferences.startup.launch-to-custom-home-page" diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 2b07fcd86f..b226cf84c7 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -482,10 +482,56 @@ } }, "1." : { - - }, - "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "1." + } + } + } }, "about.app_name" : { "comment" : "Application name to be displayed in the About dialog", @@ -2367,6 +2413,66 @@ } } }, + "auto.clear" : { + "comment" : "Header of a section in Settings. The setting configures clearing data automatically after quitting the app.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Auto-Clear" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrado automático" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacement automatique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellazione automatica" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatyczne czyszczenie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpeza automática" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоочистка" + } + } + } + }, "autoconsent.checkbox.title" : { "comment" : "Autoconsent settings checkbox title", "extractionState" : "extracted_with_value", @@ -5607,6 +5713,66 @@ } } }, + "automatically.clear.data" : { + "comment" : "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs und Browserdaten automatisch löschen, wenn DuckDuckGo beendet wird" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Automatically clear tabs and browsing data when DuckDuckGo quits" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borra automáticamente las pestañas y los datos de navegación cuando se cierra DuckDuckGo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer automatiquement les onglets et les données de navigation lorsque DuckDuckGo se ferme" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella automaticamente schede e dati di navigazione quando chiudi DuckDuckGo" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen en browsegegevens automatisch wissen wanneer DuckDuckGo stopt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatycznie czyść karty i dane przeglądania przy wychodzeniu z DuckDuckGo" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar automaticamente os separadores e os dados de navegação quando o DuckDuckGo fecha" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматически закрывает вкладки и стирает данные о посещении сайтов при завершении работы DuckDuckGo." + } + } + } + }, "Birthday" : { "comment" : "Title of the section of the Identities manager where the user can add/modify a date of birth", "localizations" : { @@ -6263,11 +6429,59 @@ "comment" : "Message that warns user that specific Bitwarden app vesions are not compatible with this app", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die folgenden Bitwarden-Versionen sind mit DuckDuckGo inkompatibel: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Bitte kehre zu einer älteren Version zurück, indem du die folgenden Schritte ausführst:" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please revert to an older version by following these steps:" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las siguientes versiones de Bitwarden son incompatibles con DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Vuelve a una versión anterior siguiendo estos pasos:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les versions suivantes de Bitwarden sont incompatibles avec DuckDuckGo : v2024.3.0, v2024.3.2, v2024.4.0 et v2024.4.1. Veuillez revenir à une version antérieure en procédant comme suit :" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le seguenti versioni di Bitwarden non sono compatibili con DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. È necessario tornare a una versione precedente seguendo questi passaggi:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De volgende versies van Bitwarden zijn niet compatibel met DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Volg deze stappen om terug te gaan naar een oudere versie:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następujące wersje Bitwarden są niezgodne z DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Przywróć starszą wersję, wykonując następujące czynności:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As seguintes versões do Bitwarden são incompatíveis com o DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0 e v2024.4.1. Reverte para uma versão mais antiga seguindo estes passos:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "С DuckDuckGo несовместимы следующие версии Bitwarden: 2024.3.0, 2024.3.2, 2024.4.0, 2024.4.1. Вернитесь к более старой версии, выполнив следующие действия:" + } } } }, @@ -6275,11 +6489,59 @@ "comment" : "First step to downgrade Bitwarden", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "V2014.2.1 herunterladen" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Download v2014.2.1" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descargar v2014.2.1" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Télécharger v2014.2.1" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scarica la versione v2014.2.1" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download v2014.2.1" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pobierz v2014.2.1" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfere a versão v2014.2.1" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скачайте версию 2014.2.1" + } } } }, @@ -6287,11 +6549,59 @@ "comment" : "Second step to downgrade Bitwarden", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Öffne die heruntergeladene DMG-Datei und ziehe die Bitwarden-Anwendung auf den Ordner „/Applications“." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Abre el archivo DMG descargado y arrastra la aplicación Bitwarden a\nla carpeta /Applications." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Ouvrez le fichier DMG téléchargé et faites glisser l'application Bitwarden vers\nle dossier /Applications." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Apri il file DMG scaricato e trascina l'applicazione Bitwarden nella\n/cartella Applications." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Open het gedownloade DMG-bestand en sleep de Bitwarden-app naar\n de map /Applications." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Otwórz pobrany plik DMG i przeciągnij aplikację Bitwarden do\nfolderu /Applications." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Abre o ficheiro DMG transferido e arrasta a aplicação Bitwarden para\na pasta /Applications." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "2. Откройте загруженный файл DMG и перетащите приложение Bitwarden в стандартную папку «Программы»." + } } } }, @@ -11099,6 +11409,66 @@ } } }, + "clear.and.quit" : { + "comment" : "Button to clear data and quit the application", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Löschen und Beenden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Clear and Quit" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar y salir" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer et quitter" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella ed esci" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wissen en stoppen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyczyść i wyjdź" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar e sair" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить и выйти" + } + } + } + }, "close.other.tabs" : { "comment" : "Menu item", "extractionState" : "extracted_with_value", @@ -13219,6 +13589,66 @@ } } }, + "disable.auto.clear.to.enable.session.restore" : { + "comment" : "Information label in Settings. It tells user that to enable session restoration setting they have to disable burn on quit. Auto-Clear should match the string with 'auto.clear' key", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deaktiviere das automatische Löschen beim Beenden, um die Sitzungswiederherstellung einzuschalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Disable auto-clear on quit to turn on session restore." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desactiva el borrado automático al salir para activar Restaurar sesión." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Désactivez l'effacement automatique à la fermeture pour activer la restauration de session." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disabilita la cancellazione automatica all'uscita per attivare il ripristino della sessione." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schakel automatisch wissen uit wanneer u stopt om het herstellen van de sessie in te schakelen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyłącz automatyczne czyszczenie przy wychodzeniu, aby włączyć przywracanie sesji." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desativa a limpeza automática ao sair para ativar a restauração da sessão." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чтобы активировать восстановление сеанса, отключите автоочистку данных при выходе." + } + } + } + }, "disable.email.protection.mesage" : { "comment" : "Message for alert shown when user disables email protection", "extractionState" : "extracted_with_value", @@ -13690,9 +14120,6 @@ } } } - }, - "Download v2014.2.1" : { - }, "download.finishing" : { "comment" : "Download being finished information text", @@ -20420,7 +20847,7 @@ }, "Hide" : { "comment" : "Main Menu > View > Home Button > None item\n Preferences > Home Button > None item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -24791,7 +25218,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Если у вас есть закладки в %@, попробуйте импортировать их вручную." + "value" : "Если у вас есть %@-файл с закладками, попробуйте импортировать его вручную." } } } @@ -24851,7 +25278,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Если у вас есть пароли в %@, попробуйте импортировать их вручную." + "value" : "Если у вас есть %@-файл с паролями, попробуйте импортировать его вручную." } } } @@ -30734,7 +31161,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Teile deine Gedanken" + "value" : "Sign Up To Participate" } }, "en" : { @@ -30794,7 +31221,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nimm an unserer kurzen Umfrage teil und hilf uns, den besten Browser zu entwickeln." + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "en" : { @@ -30854,7 +31281,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sag uns, was dich hierher gebracht hat" + "value" : "Share Your Thoughts With Us" } }, "en" : { @@ -31094,7 +31521,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hilf uns, uns zu verbessern" + "value" : "Sign Up To Participate" } }, "en" : { @@ -31226,43 +31653,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ayúdanos a mejorar" + "value" : "Share Your Thoughts With Us" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aidez-nous à nous améliorer" + "value" : "Share Your Thoughts With Us" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aiutaci a migliorare" + "value" : "Share Your Thoughts With Us" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Help ons om te verbeteren" + "value" : "Share Your Thoughts With Us" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomóż nam we wprowadzaniu ulepszeń" + "value" : "Share Your Thoughts With Us" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Ajuda-nos a melhorar" + "value" : "Share Your Thoughts With Us" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Помогите нам стать лучше" + "value" : "Share Your Thoughts With Us" } } } @@ -46228,6 +46655,66 @@ } } }, + "quit.without.clearing" : { + "comment" : "Button to quit the application without clearing data", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beenden ohne Löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Quit Without Clearing" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir sin borrar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitter sans effacer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esci senza cancellare" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stoppen zonder wissen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyjdź bez czyszczenia" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sair sem limpar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выйти без очистки" + } + } + } + }, "Recently Closed" : { "comment" : "Main Menu History item", "localizations" : { @@ -48373,7 +48860,7 @@ }, "Show left of the back button" : { "comment" : "Preferences > Home Button > left position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48427,7 +48914,7 @@ }, "Show Left of the Back Button" : { "comment" : "Main Menu > View > Home Button > left position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48693,7 +49180,7 @@ }, "Show right of the reload button" : { "comment" : "Preferences > Home Button > right position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48747,7 +49234,7 @@ }, "Show Right of the Reload Button" : { "comment" : "Main Menu > View > Home Button > right position item", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48852,6 +49339,66 @@ } } }, + "show.data.clearing.settings" : { + "comment" : "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen zum Löschen von Daten öffnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Data Clearing Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir configuración de borrado de datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les paramètres d'effacement des données" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri Impostazioni cancellazione dati" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Instellingen voor het wissen van open gegevens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz ustawienia czyszczenia danych" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir Definições de limpeza de dados" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть настройки очистки данных" + } + } + } + }, "show.folder.contents" : { "comment" : "Menu item that shows the content of a folder ", "extractionState" : "extracted_with_value", @@ -52942,6 +53489,186 @@ } } }, + "warn.before.quit" : { + "comment" : "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warne mich, dass Tabs und Daten beim Beenden gelöscht werden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warn me that tabs and data will be cleared when quitting" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertirme de que las pestañas y los datos se borrarán al salir" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "M'avertir que les onglets et les données seront effacés à la fermeture" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avvisa prima di cancellare schede e dati all'uscita" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwen dat tabbladen en gegevens worden gewist bij het afsluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzegaj, że karty i dane zostaną wyczyszczone przy wychodzeniu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avisar-me que os separadores e os dados serão limpos ao sair" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать предупреждение о сбросе вкладок и данных при выходе" + } + } + } + }, + "warn.before.quit.dialog.checkbox.message" : { + "comment" : "A label after checkbox to configure the warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jedes Mal warnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warn me every time" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertirme cada vez" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours me prévenir" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avvisa ogni volta" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elke keer waarschuwen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzegaj za każdym razem" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avisar-me sempre" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предупреждать каждый раз" + } + } + } + }, + "warn.before.quit.dialog.header" : { + "comment" : "A header of warning before clearing data on the application termination.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs und Browserdaten löschen und DuckDuckGo beenden?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Clear tabs and browsing data and quit DuckDuckGo?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Borrar pestañas y datos de navegación y salir de DuckDuckGo?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacer les onglets et les données de navigation et quitter DuckDuckGo ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare le schede e i dati di navigazione e uscire da DuckDuckGo?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen en browsergegevens wissen en DuckDuckGo afsluiten?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyczyścić karty i dane przeglądania i wyjść z DuckDuckGo?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar separadores e dados de navegação e sair do DuckDuckGo?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить вкладки и данные и выйти из DuckDuckGo?" + } + } + } + }, "We couldn‘t find any bookmarks." : { "comment" : "Data import error message: Bookmarks weren‘t found.", "localizations" : { @@ -53608,4 +54335,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift index e336ab7cbd..26c2647981 100644 --- a/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DataClearingPreferences.swift @@ -29,6 +29,27 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { } } + @Published + var isAutoClearEnabled: Bool { + didSet { + persistor.autoClearEnabled = isAutoClearEnabled + NotificationCenter.default.post(name: .autoClearDidChange, + object: nil, + userInfo: nil) + } + } + + @Published + var isWarnBeforeClearingEnabled: Bool { + didSet { + persistor.warnBeforeClearingEnabled = isWarnBeforeClearingEnabled + } + } + + @objc func toggleWarnBeforeClearing() { + isWarnBeforeClearingEnabled.toggle() + } + @MainActor func presentManageFireproofSitesDialog() { let fireproofDomainsWindowController = FireproofDomainsViewController.create().wrappedInWindowController() @@ -46,6 +67,8 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { init(persistor: FireButtonPreferencesPersistor = FireButtonPreferencesUserDefaultsPersistor()) { self.persistor = persistor isLoginDetectionEnabled = persistor.loginDetectionEnabled + isAutoClearEnabled = persistor.autoClearEnabled + isWarnBeforeClearingEnabled = persistor.warnBeforeClearingEnabled } private var persistor: FireButtonPreferencesPersistor @@ -53,6 +76,8 @@ final class DataClearingPreferences: ObservableObject, PreferencesTabOpening { protocol FireButtonPreferencesPersistor { var loginDetectionEnabled: Bool { get set } + var autoClearEnabled: Bool { get set } + var warnBeforeClearingEnabled: Bool { get set } } struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersistor { @@ -60,4 +85,14 @@ struct FireButtonPreferencesUserDefaultsPersistor: FireButtonPreferencesPersisto @UserDefaultsWrapper(key: .loginDetectionEnabled, defaultValue: false) var loginDetectionEnabled: Bool + @UserDefaultsWrapper(key: .autoClearEnabled, defaultValue: false) + var autoClearEnabled: Bool + + @UserDefaultsWrapper(key: .warnBeforeClearingEnabled, defaultValue: false) + var warnBeforeClearingEnabled: Bool + +} + +extension Notification.Name { + static let autoClearDidChange = Notification.Name("autoClearDidChange") } diff --git a/DuckDuckGo/Preferences/Model/SearchPreferences.swift b/DuckDuckGo/Preferences/Model/SearchPreferences.swift index bbe5cfb70c..65d385d72b 100644 --- a/DuckDuckGo/Preferences/Model/SearchPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SearchPreferences.swift @@ -61,4 +61,9 @@ extension PreferencesTabOpening { WindowControllersManager.shared.show(url: url, source: .ui, newTab: true) } + @MainActor + func show(url: URL) { + WindowControllersManager.shared.show(url: url, source: .ui, newTab: false) + } + } diff --git a/DuckDuckGo/Preferences/Model/StartupPreferences.swift b/DuckDuckGo/Preferences/Model/StartupPreferences.swift index 6e46f9b729..9bdafbacf3 100644 --- a/DuckDuckGo/Preferences/Model/StartupPreferences.swift +++ b/DuckDuckGo/Preferences/Model/StartupPreferences.swift @@ -40,22 +40,28 @@ struct StartupPreferencesUserDefaultsPersistor: StartupPreferencesPersistor { } -final class StartupPreferences: ObservableObject { +final class StartupPreferences: ObservableObject, PreferencesTabOpening { static let shared = StartupPreferences() private let pinningManager: LocalPinningManager private var persistor: StartupPreferencesPersistor private var pinnedViewsNotificationCancellable: AnyCancellable? + private var dataClearingPreferences: DataClearingPreferences + private var dataClearingPreferencesNotificationCancellable: AnyCancellable? init(pinningManager: LocalPinningManager = LocalPinningManager.shared, - persistor: StartupPreferencesPersistor = StartupPreferencesUserDefaultsPersistor(appearancePrefs: AppearancePreferences.shared)) { + persistor: StartupPreferencesPersistor = StartupPreferencesUserDefaultsPersistor(appearancePrefs: AppearancePreferences.shared), + dataClearingPreferences: DataClearingPreferences = DataClearingPreferences.shared) { self.pinningManager = pinningManager self.persistor = persistor + self.dataClearingPreferences = dataClearingPreferences restorePreviousSession = persistor.restorePreviousSession launchToCustomHomePage = persistor.launchToCustomHomePage customHomePageURL = persistor.customHomePageURL updateHomeButtonState() listenToPinningManagerNotifications() + listenToDataClearingPreferencesNotifications() + checkDataClearingStatus() } @Published var restorePreviousSession: Bool { @@ -129,6 +135,21 @@ final class StartupPreferences: ObservableObject { } } + private func checkDataClearingStatus() { + if dataClearingPreferences.isAutoClearEnabled { + restorePreviousSession = false + } + } + + private func listenToDataClearingPreferencesNotifications() { + dataClearingPreferencesNotificationCancellable = NotificationCenter.default.publisher(for: .autoClearDidChange).sink { [weak self] _ in + guard let self = self else { + return + } + self.checkDataClearingStatus() + } + } + private func urlWithScheme(_ urlString: String) -> String? { guard var urlWithScheme = urlString.url else { return nil diff --git a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift index dc3f28607c..1680fb2e69 100644 --- a/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesDataClearingView.swift @@ -28,7 +28,20 @@ extension Preferences { var body: some View { PreferencePane(UserText.dataClearing) { - // SECTION 1: Fireproof Site + // SECTION 1: Automatically Clear Data + PreferencePaneSection(UserText.autoClear) { + + PreferencePaneSubSection { + ToggleMenuItem(UserText.automaticallyClearData, isOn: $model.isAutoClearEnabled) + ToggleMenuItem(UserText.warnBeforeQuit, + isOn: $model.isWarnBeforeClearingEnabled) + .disabled(!model.isAutoClearEnabled) + .padding(.leading, 16) + } + + } + + // SECTION 2: Fireproof Site PreferencePaneSection(UserText.fireproofSites) { PreferencePaneSubSection { diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index ad54e6e6ac..7d22bc8867 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -28,6 +28,7 @@ extension Preferences { @ObservedObject var startupModel: StartupPreferences @ObservedObject var downloadsModel: DownloadsPreferences @ObservedObject var searchModel: SearchPreferences + @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false var body: some View { @@ -43,9 +44,19 @@ extension Preferences { Text(UserText.reopenAllWindowsFromLastSession).tag(true) .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker.reopenAllWindowsFromLastSession") }, label: {}) - .pickerStyle(.radioGroup) - .offset(x: PreferencesViews.Const.pickerHorizontalOffset) - .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") + .pickerStyle(.radioGroup) + .disabled(dataClearingModel.isAutoClearEnabled) + .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") + if dataClearingModel.isAutoClearEnabled { + VStack(alignment: .leading, spacing: 1) { + TextMenuItemCaption(UserText.disableAutoClearToEnableSessionRestore) + TextButton(UserText.showDataClearingSettings) { + startupModel.show(url: .settingsPane(.dataClearing)) + } + } + .padding(.leading, 19) + } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 4c077395c7..5d2f842652 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -87,7 +87,8 @@ enum Preferences { case .general: GeneralView(startupModel: StartupPreferences.shared, downloadsModel: DownloadsPreferences.shared, - searchModel: SearchPreferences.shared) + searchModel: SearchPreferences.shared, + dataClearingModel: DataClearingPreferences.shared) case .sync: SyncView() case .appearance: diff --git a/DuckDuckGo/StateRestoration/StatePersistenceService.swift b/DuckDuckGo/StateRestoration/StatePersistenceService.swift index 6aed78934a..5d65a2c1d0 100644 --- a/DuckDuckGo/StateRestoration/StatePersistenceService.swift +++ b/DuckDuckGo/StateRestoration/StatePersistenceService.swift @@ -65,6 +65,7 @@ final class StatePersistenceService { func removeLastSessionState() { lastSessionStateArchive = nil + fileStore.remove(fileAtURL: URL.persistenceLocation(for: self.fileName)) } @MainActor diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json new file mode 100644 index 0000000000..bb413935ee --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/PreferencesViews/Assets.xcassets/Colors/LinkBlueColor.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEE", + "green" : "0x69", + "red" : "0x39" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF6", + "green" : "0x94", + "red" : "0x71" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.933", + "green" : "0.412", + "red" : "0.224" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.965", + "green" : "0.580", + "red" : "0.443" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index 4abefdc849..dc53430490 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -123,11 +123,59 @@ "comment" : "Title for an error alert", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Sync & Backup Error" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup Error" + } } } }, @@ -135,11 +183,59 @@ "comment" : "Button Title of an error alert", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Go to Settings" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Go to Settings" + } } } }, @@ -207,11 +303,59 @@ "comment" : "Description for unable to authenticate error", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "A device password is required to use Sync & Backup." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "A device password is required to use Sync & Backup." + } } } }, @@ -6277,4 +6421,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/UnitTests/AppDelegate/AutoClearHandlerTests.swift b/UnitTests/AppDelegate/AutoClearHandlerTests.swift new file mode 100644 index 0000000000..192de793f3 --- /dev/null +++ b/UnitTests/AppDelegate/AutoClearHandlerTests.swift @@ -0,0 +1,83 @@ +// +// AutoClearHandlerTests.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 + +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +class AutoClearHandlerTests: XCTestCase { + + var handler: AutoClearHandler! + var preferences: DataClearingPreferences! + var fireViewModel: FireViewModel! + + override func setUp() { + super.setUp() + let persistor = MockFireButtonPreferencesPersistor() + preferences = DataClearingPreferences(persistor: persistor) + fireViewModel = FireViewModel(fire: Fire(tld: ContentBlocking.shared.tld)) + let fileName = "AutoClearHandlerTests" + let fileStore = FileStoreMock() + let service = StatePersistenceService(fileStore: fileStore, fileName: fileName) + let appStateRestorationManager = AppStateRestorationManager(fileStore: fileStore, + service: service, + shouldRestorePreviousSession: false) + handler = AutoClearHandler(preferences: preferences, fireViewModel: fireViewModel, stateRestorationManager: appStateRestorationManager) + } + + override func tearDown() { + handler = nil + preferences = nil + fireViewModel = nil + super.tearDown() + } + + func testWhenBurningEnabledAndNoWarningRequiredThenTerminateLaterIsReturned() { + preferences.isAutoClearEnabled = true + preferences.isWarnBeforeClearingEnabled = false + + let response = handler.handleAppTermination() + + XCTAssertEqual(response, .terminateLater) + } + + func testWhenBurningDisabledThenNoTerminationResponse() { + preferences.isAutoClearEnabled = false + + let response = handler.handleAppTermination() + + XCTAssertNil(response) + } + + func testWhenBurningEnabledAndFlagFalseThenBurnOnStartTriggered() { + preferences.isAutoClearEnabled = true + handler.resetTheCorrectTerminationFlag() + + XCTAssertTrue(handler.burnOnStartIfNeeded()) + } + + func testWhenBurningDisabledThenBurnOnStartNotTriggered() { + preferences.isAutoClearEnabled = false + handler.resetTheCorrectTerminationFlag() + + XCTAssertFalse(handler.burnOnStartIfNeeded()) + } + +} diff --git a/UnitTests/Preferences/DataClearingPreferencesTests.swift b/UnitTests/Preferences/DataClearingPreferencesTests.swift index 5563fb6e90..1c0b6675e4 100644 --- a/UnitTests/Preferences/DataClearingPreferencesTests.swift +++ b/UnitTests/Preferences/DataClearingPreferencesTests.swift @@ -20,7 +20,11 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser class MockFireButtonPreferencesPersistor: FireButtonPreferencesPersistor { + + var autoClearEnabled: Bool = false + var warnBeforeClearingEnabled: Bool = false var loginDetectionEnabled: Bool = false + } class DataClearingPreferencesTests: XCTestCase { From da93b48a038ee708123330293ce0606bf8c3c92e Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 29 Apr 2024 11:34:37 +0200 Subject: [PATCH 043/134] macOS VPN: Remove waitlist code (#2708) Task/Issue URL: https://app.asana.com/0/0/1207169061635760/f ## Description Removes the VPN waitlist code entirely. --- DuckDuckGo.xcodeproj/project.pbxproj | 44 ----- DuckDuckGo/Application/AppDelegate.swift | 10 - .../Card-16.imageset/Card-16.pdf | Bin 3961 -> 0 bytes .../Card-16.imageset/Contents.json | 15 -- .../Rocket-16.imageset/Contents.json | 15 -- .../Rocket-16.imageset/Rocket-16.pdf | Bin 4812 -> 0 bytes .../Shield-16.imageset/Contents.json | 15 -- .../Shield-16.imageset/Shield-16.pdf | Bin 2579 -> 0 bytes .../UserText+NetworkProtection.swift | 152 --------------- .../NavigationBar/View/MoreOptionsMenu.swift | 31 ++- .../View/NavigationBarViewController.swift | 47 +---- ...NetworkProtectionInviteCodeViewModel.swift | 184 ------------------ .../NetworkProtectionInviteDialog.swift | 36 ---- .../NetworkProtectionInvitePresenter.swift | 64 ------ .../NetworkProtectionDebugMenu.swift | 99 ---------- .../NetworkProtectionDebugUtilities.swift | 4 +- .../NetworkProtectionNavBarButtonModel.swift | 8 - ...etworkProtectionNavBarPopoverManager.swift | 10 +- ...tionWaitlistFeatureFlagOverridesMenu.swift | 166 ---------------- .../NetworkProtectionRemoteMessaging.swift | 2 +- ...rkProtectionSubscriptionEventHandler.swift | 5 +- DuckDuckGo/Preferences/Model/AboutModel.swift | 10 - .../Model/VPNPreferencesModel.swift | 2 +- .../View/PreferencesAboutView.swift | 3 - .../View/PreferencesRootView.swift | 3 +- .../VPNMetadataCollector.swift | 6 - .../NetworkProtectionFeatureDisabler.swift | 13 +- .../NetworkProtectionFeatureVisibility.swift | 105 +--------- .../Waitlist/Views/WaitlistRootView.swift | 25 --- .../EnableWaitlistFeatureView.swift | 69 ------- .../WaitlistSteps/InvitedToWaitlistView.swift | 23 --- .../WaitlistSteps/JoinWaitlistView.swift | 10 - .../WaitlistSteps/JoinedWaitlistView.swift | 11 -- .../WaitlistTermsAndConditionsView.swift | 70 ------- .../WaitlistThankYouPromptPresenter.swift | 37 +--- .../WaitlistViewControllerPresenter.swift | 39 ---- DuckDuckGo/Waitlist/Waitlist.swift | 82 -------- ...tlistTermsAndConditionsActionHandler.swift | 15 -- UnitTests/Menus/MoreOptionsMenuTests.swift | 42 +--- .../VPNFeedbackFormViewModelTests.swift | 1 - .../MockNetworkProtectionCodeRedeemer.swift | 48 ----- 41 files changed, 37 insertions(+), 1484 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Rocket-16.imageset/Rocket-16.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift delete mode 100644 DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift delete mode 100644 UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1caeda173f..40365ef615 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1162,11 +1162,6 @@ 4B4D60C12A0C848E00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 4B4D60C22A0C849000BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 4B4D60C32A0C849100BCD287 /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; - 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */; }; - 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; - 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */; }; - 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; }; - 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B4D60D32A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; @@ -1209,7 +1204,6 @@ 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DEB26B0002B00E14D75 /* DataImport.swift */; }; 4B723E1326B0007A00E14D75 /* CSVLoginExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DFD26B0002B00E14D75 /* CSVLoginExporter.swift */; }; 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723E1726B000DC00E14D75 /* TemporaryFileCreator.swift */; }; - 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A57CE279A4EF300B1C70E /* ChromiumPreferences.swift */; }; 4B7A60A1273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */; }; 4B85A48028821CC500FC4C39 /* NSPasteboardItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B85A47F28821CC500FC4C39 /* NSPasteboardItemExtension.swift */; }; @@ -1272,8 +1266,6 @@ 4B9DB02A2A983B24000927DB /* WaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */; }; 4B9DB02C2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */; }; 4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */; }; - 4B9DB0322A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */; }; - 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */; }; 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; }; 4B9DB0382A983B24000927DB /* JoinedWaitlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */; }; @@ -1294,8 +1286,6 @@ 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */; }; 4B9DB0562A983B55000927DB /* MockNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0502A983B55000927DB /* MockNotificationService.swift */; }; 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0502A983B55000927DB /* MockNotificationService.swift */; }; - 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */; }; - 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */; }; 4B9DB05A2A983B55000927DB /* MockWaitlistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */; }; 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */; }; 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0532A983B55000927DB /* WaitlistViewModelTests.swift */; }; @@ -1552,12 +1542,10 @@ 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5112AD1235B00A9E72B /* NetworkProtectionIPC */; }; 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */; }; 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */; }; - 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFE95542A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95552A9DF2990081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; - 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */; }; 850E8DFB2A6FEC5E00691187 /* BookmarksBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */; }; @@ -3058,9 +3046,6 @@ 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionControllerErrorStore.swift; sourceTree = ""; }; - 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteDialog.swift; sourceTree = ""; }; - 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInvitePresenter.swift; sourceTree = ""; }; - 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; @@ -3155,7 +3140,6 @@ 4B9DB00C2A983B24000927DB /* WaitlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewModel.swift; sourceTree = ""; }; 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistStorage.swift; sourceTree = ""; }; 4B9DB00F2A983B24000927DB /* WaitlistKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistKeychainStorage.swift; sourceTree = ""; }; - 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnableWaitlistFeatureView.swift; sourceTree = ""; }; 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistTermsAndConditionsView.swift; sourceTree = ""; }; 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinedWaitlistView.swift; sourceTree = ""; }; 4B9DB0162A983B24000927DB /* InvitedToWaitlistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvitedToWaitlistView.swift; sourceTree = ""; }; @@ -3166,7 +3150,6 @@ 4B9DB01C2A983B24000927DB /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWaitlistStorage.swift; sourceTree = ""; }; 4B9DB0502A983B55000927DB /* MockNotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNotificationService.swift; sourceTree = ""; }; - 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkProtectionCodeRedeemer.swift; sourceTree = ""; }; 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWaitlistRequest.swift; sourceTree = ""; }; 4B9DB0532A983B55000927DB /* WaitlistViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewModelTests.swift; sourceTree = ""; }; 4BA1A69A258B076900F6F690 /* FileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStore.swift; sourceTree = ""; }; @@ -3329,7 +3312,6 @@ 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderPopoverView.swift; sourceTree = ""; }; 7BF1A9D72AE054D300FCA683 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift; sourceTree = ""; }; 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtectionWaitlist.swift"; sourceTree = ""; }; 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopovers.swift; sourceTree = ""; }; 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarAppearance.swift; sourceTree = ""; }; @@ -5075,7 +5057,6 @@ children = ( 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */, BDE981DB2BBD110800645880 /* Assets */, - 4B4D606B2A0B29FA00BCD287 /* Invite */, 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, B602E81C2A1E25B0006D261F /* NEOnDemandRuleExtension.swift */, @@ -5089,7 +5070,6 @@ 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */, 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */, 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */, - 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */, EEA3EEAF2B24EB5100E8333A /* VPNLocation */, 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */, B6F1B02D2BCE6B47005E863C /* TunnelControllerProvider.swift */, @@ -5097,16 +5077,6 @@ path = BothAppTargets; sourceTree = ""; }; - 4B4D606B2A0B29FA00BCD287 /* Invite */ = { - isa = PBXGroup; - children = ( - 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */, - 4B4D606F2A0B29FA00BCD287 /* NetworkProtectionInvitePresenter.swift */, - 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */, - ); - path = Invite; - sourceTree = ""; - }; 4B4D60742A0B29FA00BCD287 /* NetworkExtensionTargets */ = { isa = PBXGroup; children = ( @@ -5460,7 +5430,6 @@ 4B9DB0122A983B24000927DB /* WaitlistSteps */ = { isa = PBXGroup; children = ( - 4B9DB0132A983B24000927DB /* EnableWaitlistFeatureView.swift */, 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */, 4B9DB0152A983B24000927DB /* JoinedWaitlistView.swift */, 4B9DB0162A983B24000927DB /* InvitedToWaitlistView.swift */, @@ -5493,7 +5462,6 @@ 317295D02AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift */, 4B9DB04F2A983B55000927DB /* MockWaitlistStorage.swift */, 4B9DB0502A983B55000927DB /* MockNotificationService.swift */, - 4B9DB0512A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift */, 4B9DB0522A983B55000927DB /* MockWaitlistRequest.swift */, ); path = Mocks; @@ -9451,7 +9419,6 @@ 373D9B4929EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */, 3706FAB8293F65D500E42796 /* FaviconImageCache.swift in Sources */, 3706FAB9293F65D500E42796 /* TabBarViewController.swift in Sources */, - 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, @@ -9634,7 +9601,6 @@ 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* ContextualMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, - 4B7534CC2A1FD7EA00158A99 /* NetworkProtectionInviteDialog.swift in Sources */, 3707C71C294B5D1900682A9F /* TabExtensionsBuilder.swift in Sources */, 3706FB42293F65D500E42796 /* MainViewController.swift in Sources */, 3706FB43293F65D500E42796 /* DuckPlayer.swift in Sources */, @@ -9695,7 +9661,6 @@ 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, - 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, @@ -9784,7 +9749,6 @@ 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, - 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, @@ -10088,7 +10052,6 @@ 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */, 1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */, 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, - 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, @@ -10366,7 +10329,6 @@ 3706FE5E293F661700E42796 /* DataImportMocks.swift in Sources */, 3706FE5F293F661700E42796 /* CrashReportTests.swift in Sources */, B60C6F7F29B1B41D007BFAA8 /* TestRunHelperInitializer.m in Sources */, - 4B9DB0592A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, 9F0FFFBF2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, 3706FE61293F661700E42796 /* PinnedTabsViewModelTests.swift in Sources */, 3706FE62293F661700E42796 /* PasswordManagementListSectionTests.swift in Sources */, @@ -10834,7 +10796,6 @@ 4B9DB03B2A983B24000927DB /* InvitedToWaitlistView.swift in Sources */, 8589063C267BCDC000D23B0D /* SaveCredentialsViewController.swift in Sources */, 4BBE0AA727B9B027003B37A8 /* PopUpButton.swift in Sources */, - 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */, AABEE6A524AA0A7F0043105B /* SuggestionViewController.swift in Sources */, 1D6216B229069BBF00386B2C /* BWKeyStorage.swift in Sources */, AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */, @@ -11004,7 +10965,6 @@ 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */, 4B9DB0382A983B24000927DB /* JoinedWaitlistView.swift in Sources */, B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */, - 4B9DB0322A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, AA4FF40C2624751A004E2377 /* GrammarFeaturesManager.swift in Sources */, 4B9DB0442A983B24000927DB /* WaitlistModalViewController.swift in Sources */, B6DA06E8291401D700225DE2 /* WKMenuItemIdentifier.swift in Sources */, @@ -11186,7 +11146,6 @@ AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */, 4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */, - 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 373A1AB02842C4EA00586521 /* BookmarkHTMLImporter.swift in Sources */, B6B5F57F2B024105008DB58A /* DataImportSummaryView.swift in Sources */, 31C3CE0228EDC1E70002C24A /* CustomRoundedCornersShape.swift in Sources */, @@ -11315,7 +11274,6 @@ 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, - 4B4D60CA2A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, AA5C1DD3285A217F0089850C /* RecentlyClosedCacheItem.swift in Sources */, B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */, @@ -11394,7 +11352,6 @@ B687B7CA2947A029001DEA6F /* ContentBlockingTabExtension.swift in Sources */, 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */, 4B379C1E27BDB7FF008A968E /* DeviceAuthenticator.swift in Sources */, - 7BFE95522A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */, 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */, @@ -11619,7 +11576,6 @@ AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, B6E6BA162BA2CF5F008AA7E1 /* SandboxTestToolNotifications.swift in Sources */, B60C6F8129B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, - 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index fef5473cfe..2f93c17ac4 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -326,10 +326,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() - NetworkProtectionWaitlist().fetchNetworkProtectionInviteCodeIfAvailable { _ in - // Do nothing when code fetching fails, as the app will try again later - } - NetworkProtectionAppEvents().applicationDidBecomeActive() #if DBP @@ -600,12 +596,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - if response.notification.request.identifier == NetworkProtectionWaitlist.notificationIdentifier { - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - NetworkProtectionWaitlistViewControllerPresenter.show() - } - } - #if DBP if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Card-16.imageset/Card-16.pdf deleted file mode 100644 index ead00161852b20d05c5bb2ff0a1871277fdf914d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3961 zcmd^COK;pZ5We$Q@Dd;?U@qSeATW^FiBYscQoDx$K@S^Qah$GK?n*A4{`!7HaVg2( zBFVWr*xXMu!{Ij%j-I?beSM@vU`%nzyN_QO=g*(>)$d<#TfTb#_T!hf8Gzwg{n~D? zhR<9>@Vr20yYB9GrjUP+@3PLQcmaF6^|rrV@7itib@BfAxR|=Q`#9uHUw6Bkf}OX0 zceiWWp0=oxFJE@|`~#Ol=4VYe%Q6^8@4)a|GsY8^Rt)yDgaQ z?bWO4qq?f^>LQBdv`L0HH>`?1Ui8hDIiVf-%C(R-q+t0CGeYa6rJgV?v~}KTj$LWt z6*t01BVFQ%QA{TJ#2e;?4N|Mfk1Sp*r6b<}4Bn^+Nh`2{OpynnjZdLK@KgeF8i?Dg zDi3=~3Zs0oYPQRUDJ7KjMrKq=>t*7ECFv!$#zB))!Ek7YWD_tfWYEfuCap5sYp#Ta zi!$&eT+%lA>AX>R05MKT!-9z3B-p4-1vr;t2(T1Pk~(lNl9x7GAjSq~=?q@DC=Iy? zDHBYDlvL5{nWe{3XxL|Fh4VN(SVv@)l(0*|i6I#a>cEYLmzrB)684aEOi+jzR(lm` z5b0pF^JbEQA6mmpAkGJkaCm{ROXq+XrK}obYOGHPj0EczOhL*y9}!zH4F(*Y$U7H3 zc_5{eN#=8h0MzGzXj(!nrV!)^8LyN=*d=zPj3Hyl3Q`ReUspg4yP7gaI|?Pq!8J=s zWUP*$2Z&CfCu>?0eL%+$(rbk%2jNXD?pq(N0|Fm(%!hOkqC*Celty%*=OQ=>JPHL; zJV_N&Yf@CCVF|frDUmCd1VWNB%}y(dSrQ&m*F=tT*1&Vpixv)AI*y8PP(v(&bGQWv z)Fk)-TV`KlB>~9Q(p-*=u+}JRP-6#DWrGa*^b^~%-+s7%zyo8DEc!bdh_ZWsP=Z=U zkFeda8LEXdAx2b*5h@@(PjV=?xG765;Ik}`jkP&DJxiXQfXaHzjRL7tm?l?36w1S8dO$bnM zc@L=_inT!pMg1C!qK4ZB&5v@T!~p&~MJ~*sN&~4DIfM%JfgLuC5;#A(A?ScQLbpf> ztq4ZIJBfj++?V%AW(AG}n-V2k77MTj2Ru?Ar>!8|gb>M5$H4}fvZW*0-hs$b(x8c< z=5TZnUEIlc0@^IPzyuPh)@y^p28b$6C@$DUm3ffmkl4ANaTOR4h!8t!V=lMh1Dnv^ zI8GSvqeV7NDBVO>Q4Uc)NM~|bWX!Cg;m52_p(OcvQTfrwk#dfKEy zv!GVFSx_sch1G_tzHER~qoXWiT99Q-E12)Md^9BZs0%N_0EM2iHgAA;m6YN~wpYQ4 zzgZ(-?e?P-xvJJjWya1!^n3Jbiu5$=zPcaIE{65c=J&5u z{&(?a-T#?a zdj5gFCZ;33TVJ&Wc+*~9uYdHrge*;;IFuIq@2Zn-JG5KuNDS=3cl7x*a9lzen(;)? zcmn!%`{nu)u5voTIp2GH(%nHrHpH)V<%epBT*$ zY`g~PFULe`90y(=+JGLuEs)Ol`U41`Di5U4s~%cEm#(1qRuK#I?3y|9n>)nvq OG2Tn|5QX38SM*CHK-zKjy<0+&$W;gsVV4Ia#Dn8Zk_E3FYzHO$dcNwh$6oKE zfOy0YyW=x`sXBG4s@o4-Xc*S~~`D*)az8O}FkGI}Ge7Ilg>ix@I+w|Fb zbF;@Thwb|PW-)Aai)rMeN9zyfXCu9|&+);g19W`)g{7Fe-ED^XO*eZo{BgY)E}uU# zkFQR@Ew_avez7HYi>P+ojNyRd7f=VBydY?|_F1p-jZws3fy;5BX-W(z6lXqUSnWjo_ zLsY95-L!-B)wvX$IoffACWB9ATGv}~u42iw!wEJSa-X-0?ydS80ZOB*eoSe*6U#$2lux0yDv$c__M@U=|uh3ztz*@;RH5v|Es8!YD z74{u0ImHe(pG;TrhRBj0zL^(#VpTE6 z^=RE9_hlEc-orSUO*uMUG$wLhmyqmy)QbQBk2??X|@E*Q5>1k1VzXz^hO^F@qk?^wP+M_CJ7bER6Jw> z5ipG~;0sC|JGA%(_zlH)La$QNkOn8Z1_FUzN=%y?EYXU99)kiJU>pGaAh_z%s2X1( zMIi)IsbNfI<>V0)4rWwC=#j7Zz(~w2Y#|K_sRjtOO}|7z=fbY!Lij;T&H(}x^MUf& zvY=}5X;`4{ViiGl$VdaoGd?D*_Mi|0rL2=hRIp%)!dpeLJISVbPDUkUm5C?=OQ@tE zlM)ID@D&2}#u1Q=;wz<9xv^FVCEO5s!pqd;5^>dr6C*APm7dxdvZ#efs}fg-_A~ry zlCdj+hE^7V8d0Y_%pnNgs+bFx~6iA9%5YWBGxbQ$)XZ$`Vr8Nh9qD)84>f$f}sz?#q z4Q+s;j&rQ$G&UZ7=wc)wdFSb@PHPZV-qTmddK2+BRg6Rs1OTrf?`-`g0V;hFY9#U1 z@)E5!Bn+sbie{}x(GbD*Hjn}!G@HFaQF|=gL$IcEYso1l0*jIONw783&ezvnk&7g% z8L5K#Atg-ZIN{RRxwxP_QSpBO;4viwG1;g{X3#UA378in{~Mc;z@( zG7hRLYy$<9;aWS&w4OF9N954dz$)mDGZbAR(HymoCn*A#%D!X^G1!*0mo=h`cEh$S z{UT9P@s;A4sp)j}Ohdv~jZw;&gqn4GMj?E3*YX09sR0cnJxXf<67?LS<2aCb^m&gA zngH8DsT)?W_D6W0^qSJ?HDtW6kffpm8q6``pIu|Z|NRtCfryq9DuXgj@ z(B40_?cddR^X+f#X}tOB*IW10<8}7-)9krX$?Y%1x8L3QHsdDz)$-lj`KOmY+;;D* zp}_unK6&zY%RPKJy3JyT{GddgFtxwZ=L19k=$br(vp3sr2DMMt++yxYPF-+}Qb^O* zoFq@S{%W;a@3!V=y`UdmdNF@9?1ArwSJ(4L|MIG@D;<9|P+1&)s(8F!?S>UQDgy`b zh5jP3Gj$K0@TAb_f&Luw&GjoxHR{2oIavI7{hqn@j{RGZla(m9r%S1a+iAz6=a1*R z`Evc{_S_Gf;dRG(z;MyPI!zjUCps-Gc{Dzkl`@f>htx diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json deleted file mode 100644 index 510acba745..0000000000 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Shield-16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/Shield-16.imageset/Shield-16.pdf deleted file mode 100644 index 677edb434b04e64e0c5890edd3905cb8c1120d2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2579 zcmZveUvFDC48`B?r_f7*c0grO)L)<|u%_!UY{R;AZ^a%w=eEs|*jwxlT|fPP)U}he znI97QSft3qLrRZczkKyne4Qq7!R`L|b8_z4Gxz-YG;c5L>lE+ftDolW`@;vPfakVr zf4G^~*VF3d{LkfPKL7rOJ9~5g+h#xgJ&6zFkq^3@TEJ+f2=8%_ZQ^z|@ks z?nB9?_2I4bk`qu$8fzY>HVz=vNrD%xE#NTuT5C+Kg0ESMV4S@6mI^EJwyld{F{CKT ztRvKrN;Ncu-ZIt^>lgx2tM8?XrKI@oV&Sl_*QVX9@g907T2@lB)?_Q3z{%LDqJr=2 zW5W@V;|Gh_%sTrjghbZrwUtn?&d{UX;*B0Jp$zNZTS)`cgu`(*OfB9aPl%i#DMpnZ zy0hP6u4F*21Pf;@QmrN1!RmuF$<;-#MWYgzcu5ixr}-o#UtybcyP!{>qQ+4tic&RE zyntbfvA8R0*AvwVV6pce^1w7VX=x}=SW@Ys1Cj`+`31Ub3W{F5bq<&T_LBZ_97wTC`nuAuLDlvBSV6~^FqXdYy=QQLJG{7mb zl~y|jLrbY&C-_=H5&ns58XLa!9sj7JB{;^z<(-DFoeTZOt*!=Nd8~1W7Fs zB2DS7n9|83#uk)NJ7tB7(NNL)KN3WWMRasgr8V$IF|0R8oJNn0QDc79^n0|eXuZIF zWKeJj9RUrco9w`H%YyE};IA)H6|_rpEUnCZD%62uhEfn5I~gM3hymq=bnVF+oqb9d znT8gdszVC+3=wNFYI_(S8A~Gy8@r>su%Nl5-KV`)y3bU@lAgHZ znhcf#SY0Y}EnBO|C;=X??_nd^2BFq8Bopf5vb55(Ri9cfa*dRfZpaB|v5)LDSGg%E00@pwiV=%+gFLPt*kZjO2`^F~;DlHuK)uQ{(*T?rgU`%v(;hcg6{Py}ki+`3T*6jTm(MN?HAQc|oYoGB|gq zbIx`jv6Tn$??LW)Vn*KcWC`wX54=nKY<*Z??cRTB`srqVH}Qg)mR#+xE18#r13rEj z%wy%Yw-AE-5{iLo_%(#izk(v-`Wj+9o}k0c`toYNnRvFXes~ETkN3Ol`H6dcdGTZ+ lWp%#W9h@aR2wuIp{5%u=c)qP~4kI0gSDhX``tFxk{{i7b5.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionWaitlistSignUpPromptDismissed.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } catch NetworkProtectionClientError.invalidInviteCode { - errorText = UserText.inviteDialogUnrecognizedCodeMessage - showProgressView = false - return - } catch { - errorText = UserText.unknownErrorTryAgainMessage - showProgressView = false - return - } - showProgressView = false - delegate?.networkProtectionInviteCodeViewModelDidReedemSuccessfully(self) - } - - func onCancel() { - delegate?.networkProtectionInviteCodeViewModelDidCancel(self) - } - -} - -protocol NetworkProtectionInviteSuccessViewModelDelegate: AnyObject { - func networkProtectionInviteSuccessViewModelDidConfirm(_ viewModel: NetworkProtectionInviteSuccessViewModel) -} - -final class NetworkProtectionInviteSuccessViewModel: InviteCodeSuccessViewModel { - - weak var delegate: NetworkProtectionInviteSuccessViewModelDelegate? - - var titleText: String { - UserText.networkProtectionInviteSuccessTitle - } - - var messageText: String { - UserText.networkProtectionInviteSuccessMessage - } - - var confirmButtonText: String { - UserText.inviteDialogGetStartedButton - } - - func onConfirm() { - delegate?.networkProtectionInviteSuccessViewModelDidConfirm(self) - } - -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift deleted file mode 100644 index 2d6aa8476e..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// NetworkProtectionInviteDialog.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 SwiftUI -import NetworkProtection -import SwiftUIExtensions - -struct NetworkProtectionInviteDialog: View { - @ObservedObject var model: NetworkProtectionInviteViewModel - - var body: some View { - switch model.currentDialog { - case .codeEntry: - InviteCodeView(viewModel: model.inviteCodeViewModel) - case .success: - InviteCodeSuccessView(viewModel: model.successCodeViewModel) - case .none: - EmptyView() - } - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift deleted file mode 100644 index d845eedc0c..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// NetworkProtectionInvitePresenter.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 SwiftUI -import NetworkProtection - -protocol NetworkProtectionInvitePresenting { - func present() -} - -final class NetworkProtectionInvitePresenter: NetworkProtectionInvitePresenting, NetworkProtectionInviteViewModelDelegate { - - private var presentedViewController: NSViewController? - - // MARK: NetworkProtectionInvitePresenting - - @MainActor func present() { - let viewModel = NetworkProtectionInviteViewModel(delegate: self, redemptionCoordinator: NetworkProtectionCodeRedemptionCoordinator()) - - let view = NetworkProtectionInviteDialog(model: viewModel) - let hostingVC = NSHostingController(rootView: view) - presentedViewController = hostingVC - let newWindowController = hostingVC.wrappedInWindowController() - - guard let newWindow = newWindowController.window, - let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController - else { - assertionFailure("Failed to present \(hostingVC)") - return - } - parentWindowController.window?.beginSheet(newWindow) - } - - // MARK: NetworkProtectionInviteViewModelDelegate - - func didCancelInviteFlow() { - presentedViewController?.dismiss() - presentedViewController = nil - } - - func didCompleteInviteFlow() { - presentedViewController?.dismiss() - presentedViewController = nil - Task { - await WindowControllersManager.shared.showNetworkProtectionStatus() - } - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 2833f1a23d..472b1f5275 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -53,13 +53,6 @@ final class NetworkProtectionDebugMenu: NSMenu { private let excludeLocalNetworksMenuItem = NSMenuItem(title: "excludeLocalNetworks", action: #selector(NetworkProtectionDebugMenu.toggleShouldExcludeLocalRoutes)) - private let enterWaitlistInviteCodeItem = NSMenuItem(title: "Enter Waitlist Invite Code", action: #selector(NetworkProtectionDebugMenu.showNetworkProtectionInviteCodePrompt)) - - private let waitlistTokenItem = NSMenuItem(title: "Waitlist Token:") - private let waitlistTimestampItem = NSMenuItem(title: "Waitlist Timestamp:") - private let waitlistInviteCodeItem = NSMenuItem(title: "Waitlist Invite Code:") - private let waitlistTermsAndConditionsAcceptedItem = NSMenuItem(title: "T&C Accepted:") - // swiftlint:disable:next function_body_length init() { preferredServerMenu = NSMenu { [preferredServerAutomaticItem] in @@ -144,28 +137,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) } - NSMenuItem(title: "NetP Waitlist") { - NSMenuItem(title: "Reset Waitlist State", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionWaitlistState)) - .targetting(self) - NSMenuItem(title: "Reset T&C Acceptance", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionTermsAndConditionsAcceptance)) - .targetting(self) - - enterWaitlistInviteCodeItem - .targetting(self) - - NSMenuItem(title: "Send Waitlist Notification", action: #selector(NetworkProtectionDebugMenu.sendNetworkProtectionWaitlistAvailableNotification)) - .targetting(self) - NSMenuItem.separator() - - waitlistTokenItem - waitlistTimestampItem - waitlistInviteCodeItem - waitlistTermsAndConditionsAcceptedItem - } - - NSMenuItem(title: "NetP Waitlist Feature Flag Overrides") - .submenu(NetworkProtectionWaitlistFeatureFlagOverridesMenu()) - NSMenuItem.separator() NSMenuItem(title: "Kill Switch (alternative approach)") { @@ -423,10 +394,6 @@ final class NetworkProtectionDebugMenu: NSMenu { excludedRoutesMenu.addItem(menuItem) } - // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow - let waitlist = NetworkProtectionWaitlist() - enterWaitlistInviteCodeItem.isEnabled = waitlist.waitlistStorage.isOnWaitlist || waitlist.waitlistStorage.isInvited - } // MARK: - Menu State Update @@ -437,7 +404,6 @@ final class NetworkProtectionDebugMenu: NSMenu { updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() - updateNetworkProtectionItems() } private func updateEnvironmentMenu() { @@ -504,27 +470,8 @@ final class NetworkProtectionDebugMenu: NSMenu { disableRekeyingMenuItem.state = settings.disableRekeying ? .on : .off } - private func updateNetworkProtectionItems() { - let waitlistStorage = WaitlistKeychainStore(waitlistIdentifier: NetworkProtectionWaitlist.identifier, keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup) - waitlistTokenItem.title = "Waitlist Token: \(waitlistStorage.getWaitlistToken() ?? "N/A")" - waitlistInviteCodeItem.title = "Waitlist Invite Code: \(waitlistStorage.getWaitlistInviteCode() ?? "N/A")" - - if let timestamp = waitlistStorage.getWaitlistTimestamp() { - waitlistTimestampItem.title = "Waitlist Timestamp: \(String(describing: timestamp))" - } else { - waitlistTimestampItem.title = "Waitlist Timestamp: N/A" - } - - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - waitlistTermsAndConditionsAcceptedItem.title = "T&C Accepted: \(accepted ? "Yes" : "No")" - } - // MARK: Waitlist - @objc func sendNetworkProtectionWaitlistAvailableNotification(_ sender: Any?) { - NetworkProtectionWaitlist().sendInviteCodeAvailableNotification(completion: nil) - } - @objc func resetNetworkProtectionActivationDate(_ sender: Any?) { overrideNetworkProtectionActivationDate(to: nil) } @@ -556,52 +503,6 @@ final class NetworkProtectionDebugMenu: NSMenu { } } - @objc func resetNetworkProtectionWaitlistState(_ sender: Any?) { - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionWaitlistSignUpPromptDismissed.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - @objc func resetNetworkProtectionTermsAndConditionsAcceptance(_ sender: Any?) { - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - @objc func showNetworkProtectionInviteCodePrompt(_ sender: Any?) { - let code = getInviteCode() - - Task { - do { - let redeemer = NetworkProtectionCodeRedemptionCoordinator() - try await redeemer.redeem(code) - NetworkProtectionWaitlist().waitlistStorage.store(inviteCode: code) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } catch { - // Do nothing here, this is just a debug menu - } - } - } - - private func getInviteCode() -> String { - let alert = NSAlert() - alert.addButton(withTitle: "Use Invite Code") - alert.addButton(withTitle: "Cancel") - alert.messageText = "Enter Invite Code" - alert.informativeText = "Please grab a VPN invite code from Asana and enter it here." - - let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) - alert.accessoryView = textField - - let response = alert.runModal() - - if response == .alertFirstButtonReturn { - return textField.stringValue - } else { - return "" - } - } - // MARK: Environment @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index 8c141f7061..721f194c0d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -55,7 +55,7 @@ final class NetworkProtectionDebugUtilities { // MARK: - Debug commands for the extension func resetAllState(keepAuthToken: Bool) async { - let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(keepAuthToken: keepAuthToken, uninstallSystemExtension: true) + let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) guard uninstalledSuccessfully else { return @@ -63,8 +63,6 @@ final class NetworkProtectionDebugUtilities { settings.resetToDefaults() - NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() - DefaultWaitlistActivationDateStore(source: .netP).removeDates() DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index de9f3ee692..e4742d3521 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -169,14 +169,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { @MainActor func updateVisibility() { - // The button is visible in the case where NetP has not been activated, but the user has been invited and they haven't accepted T&Cs. - if vpnVisibility.isNetworkProtectionBetaVisible() { - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - showButton = true - return - } - } - guard !isPinned, !popoverManager.isShown, !isHavingConnectivityIssues else { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 1cd0015748..14e266a11c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -116,7 +116,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { userDefaults: .netP, locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in - _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) + _ = await self?.networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) }) popover.delegate = delegate @@ -138,13 +138,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { networkProtectionPopover.close() self.networkProtectionPopover = nil } else { - let featureVisibility = DefaultNetworkProtectionVisibility() - - if featureVisibility.isNetworkProtectionBetaVisible() { - show(positionedBelow: view, withDelegate: delegate) - } else { - featureVisibility.disableForWaitlistUsers() - } + show(positionedBelow: view, withDelegate: delegate) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift deleted file mode 100644 index aa9fce2460..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// NetworkProtectionWaitlistFeatureFlagOverridesMenu.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 AppKit -import Foundation -import NetworkProtection -import NetworkProtectionUI -import SwiftUI - -/// Implements the logic for the VPN's simulate failures menu. -/// -@MainActor -final class NetworkProtectionWaitlistFeatureFlagOverridesMenu: NSMenu { - - // MARK: - Waitlist Active Properties - - private let waitlistActiveUseRemoteValueMenuItem: NSMenuItem - private let waitlistActiveOverrideONMenuItem: NSMenuItem - private let waitlistActiveOverrideOFFMenuItem: NSMenuItem - - @UserDefaultsWrapper(key: .networkProtectionWaitlistActiveOverrideRawValue, - defaultValue: WaitlistOverride.default.rawValue, - defaults: .netP) - private var waitlistActiveOverrideValue: Int - - // MARK: - Waitlist Enabled Properties - - private let waitlistEnabledUseRemoteValueMenuItem: NSMenuItem - private let waitlistEnabledOverrideONMenuItem: NSMenuItem - private let waitlistEnabledOverrideOFFMenuItem: NSMenuItem - - @UserDefaultsWrapper(key: .networkProtectionWaitlistEnabledOverrideRawValue, - defaultValue: WaitlistOverride.default.rawValue, - defaults: .netP) - private var waitlistEnabledOverrideValue: Int - - init() { - waitlistActiveUseRemoteValueMenuItem = NSMenuItem(title: "Remote Value", action: #selector(Self.waitlistEnabledUseRemoteValue)) - waitlistActiveOverrideONMenuItem = NSMenuItem(title: "ON", action: #selector(Self.waitlistEnabledOverrideON)) - waitlistActiveOverrideOFFMenuItem = NSMenuItem(title: "OFF", action: #selector(Self.waitlistEnabledOverrideOFF)) - - waitlistEnabledUseRemoteValueMenuItem = NSMenuItem(title: "Remote Value", action: #selector(Self.waitlistActiveUseRemoteValue)) - waitlistEnabledOverrideONMenuItem = NSMenuItem(title: "ON", action: #selector(Self.waitlistActiveOverrideON)) - waitlistEnabledOverrideOFFMenuItem = NSMenuItem(title: "OFF", action: #selector(Self.waitlistActiveOverrideOFF)) - - super.init(title: "") - buildItems { - NSMenuItem(title: "Reset Waitlist Overrides", action: #selector(Self.waitlistResetFeatureOverrides)).targetting(self) - NSMenuItem.separator() - - NSMenuItem(title: "Waitlist Enabled") { - waitlistActiveUseRemoteValueMenuItem.targetting(self) - waitlistActiveOverrideONMenuItem.targetting(self) - waitlistActiveOverrideOFFMenuItem.targetting(self) - } - - NSMenuItem(title: "Waitlist Active") { - waitlistEnabledUseRemoteValueMenuItem.targetting(self) - waitlistEnabledOverrideONMenuItem.targetting(self) - waitlistEnabledOverrideOFFMenuItem.targetting(self) - } - } - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Misc IBActions - - @objc func waitlistResetFeatureOverrides(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.default.rawValue - waitlistEnabledOverrideValue = WaitlistOverride.default.rawValue - } - - // MARK: - Waitlist Active IBActions - - @objc func waitlistActiveUseRemoteValue(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.useRemoteValue.rawValue - } - - @objc func waitlistActiveOverrideON(sender: NSMenuItem) { - waitlistActiveOverrideValue = WaitlistOverride.on.rawValue - } - - @objc func waitlistActiveOverrideOFF(sender: NSMenuItem) { - Task { @MainActor in - guard case .alertFirstButtonReturn = await waitlistOFFAlert().runModal() else { - return - } - - waitlistActiveOverrideValue = WaitlistOverride.off.rawValue - } - } - - // MARK: - Waitlist Enabled IBActions - - @objc func waitlistEnabledUseRemoteValue(sender: NSMenuItem) { - waitlistEnabledOverrideValue = WaitlistOverride.useRemoteValue.rawValue - } - - @objc func waitlistEnabledOverrideON(sender: NSMenuItem) { - waitlistEnabledOverrideValue = WaitlistOverride.on.rawValue - } - - @objc func waitlistEnabledOverrideOFF(sender: NSMenuItem) { - Task { @MainActor in - guard case .alertFirstButtonReturn = await waitlistOFFAlert().runModal() else { - return - } - - waitlistEnabledOverrideValue = WaitlistOverride.off.rawValue - } - } - - // MARK: - Updating the menu state - - override func update() { - waitlistActiveUseRemoteValueMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.useRemoteValue.rawValue ? .on : .off - waitlistActiveOverrideONMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.on.rawValue ? .on : .off - waitlistActiveOverrideOFFMenuItem.state = waitlistActiveOverrideValue == WaitlistOverride.off.rawValue ? .on : .off - - waitlistEnabledUseRemoteValueMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.useRemoteValue.rawValue ? .on : .off - waitlistEnabledOverrideONMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.on.rawValue ? .on : .off - waitlistEnabledOverrideOFFMenuItem.state = waitlistEnabledOverrideValue == WaitlistOverride.off.rawValue ? .on : .off - } - - // MARK: - UI Additions - - private func waitlistOFFAlert() -> NSAlert { - let alert = NSAlert() - alert.messageText = "Override to OFF value?" - alert.informativeText = """ - This will potentially disable DuckDuckGo VPN and erase your invitation. - - You can re-enable DuckDuckGo VPN after reverting this change. - - Please click 'Cancel' if you're unsure. - """ - alert.alertStyle = .warning - alert.addButton(withTitle: "Override") - alert.addButton(withTitle: UserText.cancel) - return alert - } -} - -#if DEBUG -#Preview { - return MenuPreview(menu: NetworkProtectionWaitlistFeatureFlagOverridesMenu()) -} -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift index 739c7501ed..6daeb90569 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift @@ -132,7 +132,7 @@ final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMess } // Next, check if the message requires access to NetP but it's not visible: - if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isNetworkProtectionBetaVisible() { + if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isVPNVisible() { return false } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index ecdc324254..31795df3a3 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -26,19 +26,16 @@ import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { private let accountManager: AccountManager - private let networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling private let userDefaults: UserDefaults private var cancellables = Set() init(accountManager: AccountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), - networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming = NetworkProtectionCodeRedemptionCoordinator(), networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), userDefaults: UserDefaults = .netP) { self.accountManager = accountManager - self.networkProtectionRedemptionCoordinator = networkProtectionRedemptionCoordinator self.networkProtectionTokenStorage = networkProtectionTokenStorage self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler self.userDefaults = userDefaults @@ -109,7 +106,7 @@ final class NetworkProtectionSubscriptionEventHandler { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") Task { - await networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: false) } } diff --git a/DuckDuckGo/Preferences/Model/AboutModel.swift b/DuckDuckGo/Preferences/Model/AboutModel.swift index 1504240116..ed2e0a914d 100644 --- a/DuckDuckGo/Preferences/Model/AboutModel.swift +++ b/DuckDuckGo/Preferences/Model/AboutModel.swift @@ -22,12 +22,6 @@ import Common final class AboutModel: ObservableObject, PreferencesTabOpening { let appVersion = AppVersion() - private let netPInvitePresenter: NetworkProtectionInvitePresenting - - init(netPInvitePresenter: NetworkProtectionInvitePresenting) { - self.netPInvitePresenter = netPInvitePresenter - } - let displayableAboutURL: String = URL.aboutDuckDuckGo .toString(decodePunycode: false, dropScheme: true, dropTrailingSlash: false) @@ -39,8 +33,4 @@ final class AboutModel: ObservableObject, PreferencesTabOpening { func copy(_ value: String) { NSPasteboard.general.copy(value) } - - func displayNetPInvite() { - netPInvitePresenter.present() - } } diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 320d0d2cad..c79ef7a8e2 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -109,7 +109,7 @@ final class VPNPreferencesModel: ObservableObject { switch response { case .OK: - await NetworkProtectionFeatureDisabler().disable(keepAuthToken: true, uninstallSystemExtension: true) + await NetworkProtectionFeatureDisabler().disable(uninstallSystemExtension: true) default: // intentional no-op break diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index 63c0601ea8..e4e8900df9 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -55,9 +55,6 @@ extension Preferences { .multilineTextAlignment(.leading) Text(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) - .onTapGesture(count: 12) { - model.displayNetPInvite() - } .contextMenu(ContextMenu(menuItems: { Button(UserText.copy, action: { model.copy(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 5d2f842652..8cecf62db6 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -109,8 +109,7 @@ enum Preferences { // Opens a new tab Spacer() case .about: - let netPInvitePresenter = NetworkProtectionInvitePresenter() - AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) + AboutView(model: AboutModel()) } } .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 1e9dd5d333..700149aad7 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -75,7 +75,6 @@ struct VPNMetadata: Encodable { } struct PrivacyProInfo: Encodable { - let betaParticipant: Bool let hasPrivacyProAccount: Bool let hasVPNEntitlement: Bool } @@ -304,15 +303,10 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let waitlistStore = WaitlistKeychainStore( - waitlistIdentifier: NetworkProtectionWaitlist.identifier, - keychainAppGroup: NetworkProtectionWaitlist.keychainAppGroup - ) let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false return .init( - betaParticipant: waitlistStore.isInvited, hasPrivacyProAccount: accountManager.isUserAuthenticated, hasVPNEntitlement: hasVPNEntitlement ) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index e6d085bc91..cddc3b1a9c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -29,7 +29,7 @@ protocol NetworkProtectionFeatureDisabling { /// - Returns: `true` if the uninstallation was completed. `false` if it was cancelled by the user or an error. /// @discardableResult - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool + func disable(uninstallSystemExtension: Bool) async -> Bool func stop() } @@ -68,12 +68,11 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling /// This method disables the VPN and clear all of its state. /// /// - Parameters: - /// - keepAuthToken: If `true`, the auth token will not be removed. /// - includeSystemExtension: Whether this method should uninstall the system extension. /// @MainActor @discardableResult - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool { + func disable(uninstallSystemExtension: Bool) async -> Bool { // We can do this optimistically as it has little if any impact. unpinNetworkProtection() @@ -118,10 +117,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling try? await Task.sleep(interval: 0.5) disableLoginItems() - if !keepAuthToken { - try? removeAppAuthToken() - } - notifyVPNUninstalled() isDisabling = false return true @@ -151,10 +146,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling pinningManager.unpin(.networkProtection) } - private func removeAppAuthToken() throws { - try NetworkProtectionKeychainTokenStore().deleteToken() - } - private func removeVPNConfiguration() async throws { // Remove the agent VPN configuration try await ipcClient.debugCommand(.removeVPNConfiguration) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 11bf4a988e..6956ba22ae 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -27,15 +27,12 @@ import PixelKit import Subscription protocol NetworkProtectionFeatureVisibility { - var isEligibleForThankYouMessage: Bool { get } var isInstalled: Bool { get } func canStartVPN() async throws -> Bool func isVPNVisible() -> Bool - func isNetworkProtectionBetaVisible() -> Bool func shouldUninstallAutomatically() -> Bool func disableForAllUsers() async - func disableForWaitlistUsers() @discardableResult func disableIfUserHasNoAccess() async -> Bool @@ -47,16 +44,11 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let featureDisabler: NetworkProtectionFeatureDisabling private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation - private let networkProtectionWaitlist = NetworkProtectionWaitlist() private let privacyConfigurationManager: PrivacyConfigurationManaging private let defaults: UserDefaults let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) let accountManager: AccountManager - var waitlistIsOngoing: Bool { - isWaitlistEnabled && isWaitlistBetaActive - } - init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), @@ -72,17 +64,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) } - /// Calculates whether the VPN is visible. - /// The following criteria are used: - /// - /// 1. If the user has a valid auth token, the feature is visible - /// 2. If no auth token is found, the feature is visible if the waitlist feature flag is enabled - /// - /// Once the waitlist beta has ended, we can trigger a remote change that removes the user's auth token and turn off the waitlist flag, hiding the VPN from the user. - func isNetworkProtectionBetaVisible() -> Bool { - return isEasterEggUser || waitlistIsOngoing - } - var isInstalled: Bool { LoginItem.vpnMenu.status.isInstalled } @@ -94,7 +75,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// func canStartVPN() async throws -> Bool { guard subscriptionFeatureAvailability.isFeatureAvailable else { - return isNetworkProtectionBetaVisible() + return false } switch await accountManager.hasEntitlement(for: .networkProtection) { @@ -112,7 +93,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// func isVPNVisible() -> Bool { guard subscriptionFeatureAvailability.isFeatureAvailable else { - return isNetworkProtectionBetaVisible() + return false } return accountManager.isUserAuthenticated @@ -142,93 +123,19 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { defaults.networkProtectionOnboardingStatusPublisher } - /// Easter egg users can be identified by them being internal users and having an auth token (NetP being activated). - /// - private var isEasterEggUser: Bool { - !isWaitlistUser && networkProtectionFeatureActivation.isFeatureActivated - } - - /// Whether it's a user with feature access - private var isEnabledWaitlistUser: Bool { - isWaitlistUser && waitlistIsOngoing - } - - /// Waitlist users are users that have the waitlist enabled and active - /// - private var isWaitlistUser: Bool { - networkProtectionWaitlist.waitlistStorage.isWaitlistUser - } - - /// Waitlist users are users that have the waitlist enabled and active and are invited - /// - private var isInvitedWaitlistUser: Bool { - networkProtectionWaitlist.waitlistStorage.isWaitlistUser && networkProtectionWaitlist.waitlistStorage.isInvited - } - - private var isWaitlistBetaActive: Bool { - true - } - - private var isWaitlistEnabled: Bool { - true - } - func disableForAllUsers() async { - await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) - } - - /// Disables the VPN for legacy users, if necessary. - /// - /// This method does not seek to remove tokens or uninstall anything. - /// - private func disableVPNForLegacyUsersIfSubscriptionAvailable() async -> Bool { - guard isEligibleForThankYouMessage && !defaults.vpnLegacyUserAccessDisabledOnce else { - return false - } - - PixelKit.fire(VPNPrivacyProPixel.vpnBetaStoppedWhenPrivacyProEnabled, frequency: .dailyAndCount) - defaults.vpnLegacyUserAccessDisabledOnce = true - await featureDisabler.disable(keepAuthToken: true, uninstallSystemExtension: false) - return true - } - - func disableForWaitlistUsers() { - guard isWaitlistUser else { - return - } - - Task { - await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) - } + await featureDisabler.disable(uninstallSystemExtension: false) } /// A method meant to be called safely from different places to disable the VPN if the user isn't meant to have access to it. /// @discardableResult func disableIfUserHasNoAccess() async -> Bool { - if shouldUninstallAutomatically() { - await disableForAllUsers() - return true - } - - return await disableVPNForLegacyUsersIfSubscriptionAvailable() - } - - // MARK: - Subscription Start Support - - /// To query whether we're a legacy (waitlist or easter egg) user. - /// - private func isPreSubscriptionUser() -> Bool { - guard let token = try? NetworkProtectionKeychainTokenStore(isSubscriptionEnabled: false).fetchToken() else { + guard shouldUninstallAutomatically() else { return false } - return !token.hasPrefix(Self.subscriptionAuthTokenPrefix) - } - - /// Checks whether the VPN needs to be disabled. - /// - var isEligibleForThankYouMessage: Bool { - isPreSubscriptionUser() && subscriptionFeatureAvailability.isFeatureAvailable + await disableForAllUsers() + return true } } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift index 776e44acdf..b9a029d4c6 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift @@ -18,31 +18,6 @@ import SwiftUI -struct NetworkProtectionWaitlistRootView: View { - @EnvironmentObject var model: WaitlistViewModel - - var body: some View { - Group { - switch model.viewState { - case .notOnWaitlist, .joiningWaitlist: - JoinWaitlistView(viewData: NetworkProtectionJoinWaitlistViewData()) - case .joinedWaitlist(let state): - JoinedWaitlistView(viewData: NetworkProtectionJoinedWaitlistViewData(), - notificationsAllowed: state == .notificationAllowed) - case .invited: - InvitedToWaitlistView(viewData: NetworkProtectionInvitedToWaitlistViewData()) - case .termsAndConditions: - WaitlistTermsAndConditionsView(viewData: NetworkProtectionWaitlistTermsAndConditionsViewData()) { - NetworkProtectionTermsAndConditionsContentView() - } - case .readyToEnable: - EnableWaitlistFeatureView(viewData: EnableNetworkProtectionViewData()) - } - } - .environmentObject(model) - } -} - #if DBP import SwiftUI diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift deleted file mode 100644 index 1f4ee5d72c..0000000000 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// EnableWaitlistFeatureView.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 SwiftUI -import SwiftUIExtensions - -protocol EnableWaitlistFeatureViewData { - var headerImageName: String { get } - var title: String { get } - var subtitle: String { get } - var availabilityDisclaimer: String { get } - var buttonConfirmLabel: String { get } -} - -struct EnableWaitlistFeatureView: View { - var viewData: EnableWaitlistFeatureViewData - @EnvironmentObject var model: WaitlistViewModel - - var body: some View { - WaitlistDialogView { - VStack(spacing: 16.0) { - Image(viewData.headerImageName) - - Text(viewData.title) - .font(.system(size: 17, weight: .bold)) - - Text(viewData.subtitle) - .multilineTextAlignment(.center) - .foregroundColor(Color(.blackWhite80)) - - Text(viewData.availabilityDisclaimer) - .multilineTextAlignment(.center) - .font(.system(size: 12)) - .foregroundColor(Color(.blackWhite60)) - } - } buttons: { - Button(viewData.buttonConfirmLabel) { - Task { - await model.perform(action: .closeAndConfirmFeature) - } - } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - } - .environmentObject(model) - } -} - -struct EnableNetworkProtectionViewData: EnableWaitlistFeatureViewData { - var headerImageName: String = "Network-Protection-256" - var title: String = UserText.networkProtectionWaitlistEnableTitle - var subtitle: String = UserText.networkProtectionWaitlistEnableSubtitle - var availabilityDisclaimer: String = UserText.networkProtectionWaitlistAvailabilityDisclaimer - var buttonConfirmLabel: String = UserText.networkProtectionWaitlistButtonGotIt -} diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift index 52b5ec44cf..ca30a44deb 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift @@ -117,29 +117,6 @@ struct WaitlistEntryViewItemViewData: Identifiable { let subtitle: String } -struct NetworkProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { - let headerImageName = "Gift-96" - let title = UserText.networkProtectionWaitlistInvitedTitle - let subtitle = UserText.networkProtectionWaitlistInvitedSubtitle - let buttonDismissLabel = UserText.networkProtectionWaitlistButtonDismiss - let buttonGetStartedLabel = UserText.networkProtectionWaitlistButtonGetStarted - let availabilityDisclaimer = UserText.networkProtectionWaitlistAvailabilityDisclaimer - let entryViewViewDataList: [WaitlistEntryViewItemViewData] = - [ - .init(imageName: "Shield-16", - title: UserText.networkProtectionWaitlistInvitedSection1Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection1Subtitle), - - .init(imageName: "Rocket-16", - title: UserText.networkProtectionWaitlistInvitedSection2Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection2Subtitle), - - .init(imageName: "Card-16", - title: UserText.networkProtectionWaitlistInvitedSection3Title, - subtitle: UserText.networkProtectionWaitlistInvitedSection3Subtitle) - ] -} - #if DBP struct DataBrokerProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift index b47869f981..6c0e4deb2a 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift @@ -73,16 +73,6 @@ struct JoinWaitlistView: View { } } -struct NetworkProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { - let headerImageName = "JoinWaitlistHeader" - let title = UserText.networkProtectionWaitlistJoinTitle - let subtitle1 = UserText.networkProtectionWaitlistJoinSubtitle1 - let subtitle2 = UserText.networkProtectionWaitlistJoinSubtitle2 - let availabilityDisclaimer = UserText.networkProtectionWaitlistAvailabilityDisclaimer - let buttonCloseLabel = UserText.networkProtectionWaitlistButtonClose - let buttonJoinWaitlistLabel = UserText.networkProtectionWaitlistButtonJoinWaitlist -} - #if DBP struct DataBrokerProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift index bda0183f51..553d3e6562 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift @@ -85,17 +85,6 @@ struct JoinedWaitlistView: View { } } -struct NetworkProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { - let headerImageName = "JoinedWaitlistHeader" - var title = UserText.networkProtectionWaitlistJoinedTitle - var joinedWithNoNotificationSubtitle1 = UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle1 - var joinedWithNoNotificationSubtitle2 = UserText.networkProtectionWaitlistJoinedWithNotificationsSubtitle2 - var enableNotificationSubtitle = UserText.networkProtectionWaitlistEnableNotifications - var buttonConfirmLabel = UserText.networkProtectionWaitlistButtonDone - var buttonCancelLabel = UserText.networkProtectionWaitlistButtonNoThanks - var buttonEnableNotificationLabel = UserText.networkProtectionWaitlistButtonEnableNotifications -} - #if DBP struct DataBrokerProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift index 0137b172e8..7495ae9e0b 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift @@ -82,76 +82,6 @@ private extension Text { } -struct NetworkProtectionTermsAndConditionsContentView: View { - var body: some View { - VStack(alignment: .leading, spacing: 0) { - Text(verbatim: UserText.networkProtectionPrivacyPolicyTitle) - .font(.system(size: 15, weight: .bold)) - .multilineTextAlignment(.leading) - - Group { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1Title).titleStyle() - - if #available(macOS 12.0, *) { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListMarkdown).bodyStyle() - } else { - Text(verbatim: UserText.networkProtectionPrivacyPolicySection1ListNonMarkdown).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionPrivacyPolicySection2Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection2List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection3Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection3List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection4Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection4List).bodyStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection5Title).titleStyle() - Text(verbatim: UserText.networkProtectionPrivacyPolicySection5List).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionTermsOfServiceTitle) - .font(.system(size: 15, weight: .bold)) - .multilineTextAlignment(.leading) - .padding(.top, 28) - .padding(.bottom, 14) - - Group { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection1Title).titleStyle(topPadding: 0) - Text(verbatim: UserText.networkProtectionTermsOfServiceSection1List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection2Title).titleStyle() - - if #available(macOS 12.0, *) { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection2ListMarkdown).bodyStyle() - } else { - Text(UserText.networkProtectionTermsOfServiceSection2ListNonMarkdown).bodyStyle() - } - - Text(verbatim: UserText.networkProtectionTermsOfServiceSection3Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection3List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection4Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection4List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection5Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection5List).bodyStyle() - } - - Group { - Text(verbatim: UserText.networkProtectionTermsOfServiceSection6Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection6List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection7Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection7List).bodyStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection8Title).titleStyle() - Text(verbatim: UserText.networkProtectionTermsOfServiceSection8List).bodyStyle() - } - } - .padding(.all, 20) - } -} - -struct NetworkProtectionWaitlistTermsAndConditionsViewData: WaitlistTermsAndConditionsViewData { - let title = "VPN Beta\nService Terms and Privacy Policy" - let buttonCancelLabel = UserText.networkProtectionWaitlistButtonCancel - let buttonAgreeAndContinueLabel = UserText.networkProtectionWaitlistButtonAgreeAndContinue -} - #if DBP struct DataBrokerProtectionTermsAndConditionsContentView: View { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 4c895fcf36..4db6e135b5 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -25,31 +25,26 @@ final class WaitlistThankYouPromptPresenter { private enum Constants { static let didShowThankYouPromptKey = "duckduckgo.macos.browser.did-show-thank-you-prompt" - static let didDismissVPNCardKey = "duckduckgo.macos.browser.did-dismiss-vpn-card" static let didDismissPIRCardKey = "duckduckgo.macos.browser.did-dismiss-pir-card" } - private let isVPNBetaTester: () -> Bool private let isPIRBetaTester: () -> Bool private let userDefaults: UserDefaults convenience init() { - self.init(isVPNBetaTester: { - return DefaultNetworkProtectionVisibility().isEligibleForThankYouMessage - }, isPIRBetaTester: { + self.init(isPIRBetaTester: { return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() }) } - init(isVPNBetaTester: @escaping () -> Bool, isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { - self.isVPNBetaTester = isVPNBetaTester + init(isPIRBetaTester: @escaping () -> Bool, userDefaults: UserDefaults = .standard) { self.isPIRBetaTester = isPIRBetaTester self.userDefaults = userDefaults } // MARK: - Presentation - // Presents a Thank You prompt to testers of the VPN or PIR. + // Presents a Thank You prompt to testers of PIR. // If the user tested both, the PIR prompt will be displayed. @MainActor func presentThankYouPromptIfNecessary(in window: NSWindow) { @@ -67,19 +62,6 @@ final class WaitlistThankYouPromptPresenter { saveDidShowPromptCheck() PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouDBP, frequency: .dailyAndCount) presentPIRThankYouPrompt(in: window) - } else if isVPNBetaTester() { - saveDidShowPromptCheck() - PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouVPN, frequency: .dailyAndCount) - presentVPNThankYouPrompt(in: window) - } - } - - @MainActor - func presentVPNThankYouPrompt(in window: NSWindow) { - let thankYouModalView = WaitlistBetaThankYouDialogViewController(copy: .vpn) - let thankYouWindowController = thankYouModalView.wrappedInWindowController() - if let thankYouWindow = thankYouWindowController.window { - window.beginSheet(thankYouWindow) } } @@ -94,14 +76,6 @@ final class WaitlistThankYouPromptPresenter { // MARK: - Eligibility - var canShowVPNCard: Bool { - guard !self.userDefaults.bool(forKey: Constants.didDismissVPNCardKey) else { - return false - } - - return isVPNBetaTester() - } - var canShowPIRCard: Bool { guard !self.userDefaults.bool(forKey: Constants.didDismissPIRCardKey) else { return false @@ -116,10 +90,6 @@ final class WaitlistThankYouPromptPresenter { // MARK: - Dismissal - func didDismissVPNThankYouCard() { - self.userDefaults.setValue(true, forKey: Constants.didDismissVPNCardKey) - } - func didDismissPIRThankYouCard() { self.userDefaults.setValue(true, forKey: Constants.didDismissPIRCardKey) } @@ -132,7 +102,6 @@ final class WaitlistThankYouPromptPresenter { func resetPromptCheck() { self.userDefaults.removeObject(forKey: Constants.didShowThankYouPromptKey) - self.userDefaults.removeObject(forKey: Constants.didDismissVPNCardKey) self.userDefaults.removeObject(forKey: Constants.didDismissPIRCardKey) } diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index 85c050b790..78e0d51794 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -30,45 +30,6 @@ extension WaitlistViewControllerPresenter { } } -struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { - - @MainActor - static func show(completion: (() -> Void)? = nil) { - guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, - windowController.window?.isKeyWindow == true else { - return - } - - // This is a hack to get around an issue with the waitlist notification screen showing the wrong state while it animates in, and then - // jumping to the correct state as soon as the animation is complete. This works around that problem by providing the correct state up front, - // preventing any state changing from occurring. - UNUserNotificationCenter.current().getNotificationSettings { settings in - let status = settings.authorizationStatus - let state = WaitlistViewModel.NotificationPermissionState.from(status) - - DispatchQueue.main.async { - let viewModel = WaitlistViewModel(waitlist: NetworkProtectionWaitlist(), - notificationPermissionState: state, - showNotificationSuccessState: true, - termsAndConditionActionHandler: NetworkProtectionWaitlistTermsAndConditionsActionHandler(), - featureSetupHandler: NetworkProtectionWaitlistFeatureSetupHandler()) - - let viewController = WaitlistModalViewController(viewModel: viewModel, contentView: NetworkProtectionWaitlistRootView()) - windowController.mainViewController.beginSheet(viewController) { _ in - // If the user dismissed the waitlist flow without signing up, hide the button. - let waitlist = NetworkProtectionWaitlist() - if !waitlist.waitlistStorage.isOnWaitlist { - waitlist.waitlistSignUpPromptDismissed = true - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - } - - completion?() - } - } - } - } -} - #if DBP struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 56c9f8cdd7..b8bc8e1ba7 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -154,88 +154,6 @@ extension ProductWaitlistRequest { } } -// MARK: - VPN Waitlist - -struct NetworkProtectionWaitlist: Waitlist { - - static let identifier: String = "networkprotection" - static let apiProductName: String = "networkprotection_macos" - static let keychainAppGroup: String = Bundle.main.appGroup(bundle: .netP) - - static let notificationIdentifier = "com.duckduckgo.macos.browser.network-protection.invite-code-available" - static let inviteAvailableNotificationTitle = UserText.networkProtectionWaitlistNotificationTitle - static let inviteAvailableNotificationBody = UserText.networkProtectionWaitlistNotificationText - - let waitlistStorage: WaitlistStorage - let waitlistRequest: WaitlistRequest - private let networkProtectionCodeRedemption: NetworkProtectionCodeRedeeming - - @UserDefaultsWrapper(key: .networkProtectionWaitlistSignUpPromptDismissed, defaultValue: false) - var waitlistSignUpPromptDismissed: Bool - - var shouldShowWaitlistViewController: Bool { - return isOnWaitlist || readyToAcceptTermsAndConditions - } - - var isOnWaitlist: Bool { - return waitlistStorage.isOnWaitlist - } - - var isInvited: Bool { - return waitlistStorage.isInvited - } - - var readyToAcceptTermsAndConditions: Bool { - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) - return waitlistStorage.isInvited && !accepted - } - - init() { - self.init( - store: WaitlistKeychainStore(waitlistIdentifier: Self.identifier, keychainAppGroup: Self.keychainAppGroup), - request: ProductWaitlistRequest(productName: Self.apiProductName), - networkProtectionCodeRedemption: NetworkProtectionCodeRedemptionCoordinator() - ) - } - - init(store: WaitlistStorage, request: WaitlistRequest, networkProtectionCodeRedemption: NetworkProtectionCodeRedeeming) { - self.waitlistStorage = store - self.waitlistRequest = request - self.networkProtectionCodeRedemption = networkProtectionCodeRedemption - } - - func fetchNetworkProtectionInviteCodeIfAvailable(completion: @escaping (WaitlistInviteCodeFetchError?) -> Void) { - // Never fetch the invite code if the Privacy Pro flag is enabled: - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - completion(nil) - return - } - - self.fetchInviteCodeIfAvailable { error in - if let error { - // Do nothing if the app fails to fetch, as the waitlist is being phased out - completion(error) - } else if let inviteCode = waitlistStorage.getWaitlistInviteCode() { - Task { @MainActor in - do { - try await networkProtectionCodeRedemption.redeem(inviteCode) - NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - sendInviteCodeAvailableNotification(completion: nil) - completion(nil) - } catch { - assertionFailure("Failed to redeem invite code") - completion(.failure(error)) - } - } - } else { - completion(nil) - assertionFailure("Didn't get error or invite code") - } - } - } - -} - #if DBP // MARK: - DataBroker Protection Waitlist diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index ccde7fd9a7..3f6f456ea0 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -26,21 +26,6 @@ protocol WaitlistTermsAndConditionsActionHandler { mutating func didAccept() } -struct NetworkProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { - @UserDefaultsWrapper(key: .networkProtectionTermsAndConditionsAccepted, defaultValue: false) - var acceptedTermsAndConditions: Bool - - func didShow() { - // Intentional no-op - } - - mutating func didAccept() { - acceptedTermsAndConditions = true - // Remove delivered NetP notifications in case the user didn't click them. - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [NetworkProtectionWaitlist.notificationIdentifier]) - } -} - #if DBP struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 209701e87a..6bda1607b7 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -65,7 +65,7 @@ final class MoreOptionsMenuTests: XCTestCase { } @MainActor - func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsEnabled() { + func testThatMoreOptionMenuHasTheExpectedItems() { moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), @@ -100,38 +100,6 @@ final class MoreOptionsMenuTests: XCTestCase { } } - @MainActor - func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsDisabled() { - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: false), - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - - XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) - XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[2].title, UserText.plusButtonNewTabMenuItem) - XCTAssertEqual(moreOptionMenu.items[3].title, UserText.newWindowMenuItem) - XCTAssertEqual(moreOptionMenu.items[4].title, UserText.newBurnerWindowMenuItem) - XCTAssertTrue(moreOptionMenu.items[5].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[6].title, UserText.zoom) - XCTAssertTrue(moreOptionMenu.items[7].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[8].title, UserText.bookmarks) - XCTAssertEqual(moreOptionMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionMenu.items[10].title, UserText.passwordManagement) - XCTAssertTrue(moreOptionMenu.items[11].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[12].title, UserText.emailOptionsMenuItem) - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) - XCTAssertTrue(moreOptionMenu.items[15].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[16].title, UserText.settings) - } else { - XCTAssertEqual(moreOptionMenu.items[14].title, UserText.settings) - } - } - // MARK: Zoom @MainActor @@ -195,10 +163,6 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility return !visible } - func isNetworkProtectionBetaVisible() -> Bool { - return visible - } - func canStartVPN() async throws -> Bool { return false } @@ -207,10 +171,6 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility // intentional no-op } - func disableForWaitlistUsers() { - // intentional no-op - } - var isEligibleForThankYouMessage: Bool { false } diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index 17b489a705..d9a45fda0c 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -128,7 +128,6 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { ) let privacyProInfo = VPNMetadata.PrivacyProInfo( - betaParticipant: false, hasPrivacyProAccount: true, hasVPNEntitlement: true ) diff --git a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift b/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift deleted file mode 100644 index b998d135e0..0000000000 --- a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// MockNetworkProtectionCodeRedeemer.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import NetworkProtection - -final class MockNetworkProtectionCodeRedeemer: NetworkProtectionCodeRedeeming { - - enum MockNetworkProtectionCodeRedeemerError: Error { - case error - } - - var throwError: Bool = false - - var redeemedCode: String? - func redeem(_ code: String) async throws { - if throwError { - throw MockNetworkProtectionCodeRedeemerError.error - } else { - redeemedCode = code - } - } - - var redeemedAccessToken: String? - func exchange(accessToken: String) async throws { - if throwError { - throw MockNetworkProtectionCodeRedeemerError.error - } else { - redeemedAccessToken = accessToken - } - } - -} From 956e5265f1337a55301fb22725c866d8cce17258 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 29 Apr 2024 13:13:26 +0200 Subject: [PATCH 044/134] Fix reporting success in create_variants.yml (#2714) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207192089896666/f Description: Always fetch Mattermost message template from main branch. --- .github/workflows/create_variants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml index c48424a947..eb3acf62ff 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -143,7 +143,7 @@ jobs: GH_TOKEN: ${{ github.token }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json?ref=${{ github.ref }} --jq .download_url) \ + curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json --jq .download_url) \ --output message-template.json export MM_USER_HANDLE=$(base64 -d <<< ${{ secrets.MM_HANDLES_BASE64 }} | jq ".${{ github.actor }}" | tr -d '"') From 8762c1392093339f263d2d124970532ac0be8403 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 29 Apr 2024 18:52:31 +0600 Subject: [PATCH 045/134] Fix quick download button animation (#2711) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207169957308547/f --- .../View/AppKit/CircularProgressView.swift | 116 ++++++++++++++++-- .../View/NavigationBarViewController.swift | 20 +-- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift index 204d950546..3e1190a9e9 100644 --- a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift @@ -164,6 +164,14 @@ final class CircularProgressView: NSView { guard !isBackgroundAnimating || !animated else { // will call `updateProgressState` on animation completion completion(false) + // if background animation is in progress but 1.0 was received before + // the `progress = nil` update – complete the progress animation + // before hiding + if progress == nil && oldValue == 1.0, animated, + // shouldn‘t be already animating to 100% + progressLayer.strokeStart != 0.0 { + updateProgress(from: 0, to: 1, animated: animated) { _ in } + } return } @@ -177,7 +185,7 @@ final class CircularProgressView: NSView { completion(true) } case (true, true): - updateProgress(oldValue: oldValue, animated: animated, completion: completion) + updateProgress(from: oldValue, to: progress, animated: animated, completion: completion) case (false, false): backgroundLayer.removeAllAnimations() progressLayer.removeAllAnimations() @@ -216,17 +224,16 @@ final class CircularProgressView: NSView { } } - private func updateProgress(oldValue: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { + private func updateProgress(from oldValue: Double?, to progress: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { guard let progress else { assertionFailure("Unexpected flow") completion(false) return } - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart let newStrokeStart = 1.0 - (progress >= 0.0 ? CGFloat(progress) : max(Constants.indeterminateProgressValue, min(0.9, 1.0 - currentStrokeStart))) - guard animated else { progressLayer.strokeStart = newStrokeStart @@ -274,7 +281,7 @@ final class CircularProgressView: NSView { guard let progress, progress == value else { return } if let oldValue, oldValue < 0, value != progress, animated { - updateProgress(oldValue: value, animated: animated) { _ in } + updateProgress(from: value, to: progress, animated: animated) { _ in } return } @@ -356,7 +363,7 @@ final class CircularProgressView: NSView { progressLayer.add(progressEndAnimation, forKey: #keyPath(CAShapeLayer.strokeEnd)) let progressAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart)) - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart progressLayer.removeAnimation(forKey: #keyPath(CAShapeLayer.strokeStart)) progressLayer.strokeStart = 0.0 @@ -375,6 +382,14 @@ final class CircularProgressView: NSView { private extension CAShapeLayer { + var currentStrokeStart: CGFloat { + if animation(forKey: #keyPath(CAShapeLayer.strokeStart)) != nil, + let presentation = self.presentation() { + return presentation.strokeStart + } + return strokeStart + } + func configureCircle(radius: CGFloat, lineWidth: CGFloat) { self.bounds = CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2) @@ -530,14 +545,97 @@ struct CircularProgress: NSViewRepresentable { perform { progress = 1 } - perform { - progress = nil + Task { + perform { + progress = nil + } } } } label: { Text(verbatim: "0->1->nil").frame(width: 120) } + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } label: { + Text(verbatim: "nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = nil + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 0 + } + try await Task.sleep(interval: 0.2) + for p in [0.26, 0.64, 0.95, 1, nil] { + perform { + progress = p + } + try await Task.sleep(interval: 0.001) + } + } + } label: { + Text(verbatim: "nil->0.2…1->nil").frame(width: 120) + } + Button { Task { perform { @@ -581,7 +679,7 @@ struct CircularProgress: NSViewRepresentable { .background(Color.white) Spacer() } - }.frame(width: 600, height: 400) + }.frame(width: 600, height: 500) } } return ProgressPreview() diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 7f916c8cd2..6b81061e5e 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -579,7 +579,7 @@ final class NavigationBarViewController: NSViewController { } let heightChange: () -> Void - if animated && view.window != nil { + if animated, let window = view.window, window.isVisible == true { heightChange = { NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.1 @@ -601,13 +601,13 @@ final class NavigationBarViewController: NSViewController { performResize() } } - if view.window == nil { - // update synchronously for off-screen view - heightChange() - } else { + if let window = view.window, window.isVisible { let dispatchItem = DispatchWorkItem(block: heightChange) DispatchQueue.main.async(execute: dispatchItem) self.heightChangeAnimation = dispatchItem + } else { + // update synchronously for off-screen view + heightChange() } } @@ -642,13 +642,19 @@ final class NavigationBarViewController: NSViewController { downloadListCoordinator.progress.publisher(for: \.totalUnitCount) .combineLatest(downloadListCoordinator.progress.publisher(for: \.completedUnitCount)) - .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .map { (total, completed) -> Double? in guard total > 0, completed < total else { return nil } return Double(completed) / Double(total) } + .dropFirst() + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { [weak downloadsProgressView] progress in - downloadsProgressView?.setProgress(progress, animated: true) + guard let downloadsProgressView else { return } + if progress == nil, downloadsProgressView.progress != 1 { + // show download completed animation before hiding + downloadsProgressView.setProgress(1, animated: true) + } + downloadsProgressView.setProgress(progress, animated: true) } .store(in: &downloadsCancellables) } From ab9b1a3c3cd521a5cfdbe050e4871bf1e42204ba Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 29 Apr 2024 08:43:04 -0700 Subject: [PATCH 046/134] Send pixels when showing notifications (#2681) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207142220574385/f Tech Design URL: CC: Description: This PR sends pixels when we display notifications to the user. We're doing this temporarily, so that we can verify that notifications are not showing in massive rates for some users. This PR has a larger diff than expected since the notifications target was never set up to send pixels until now. --- DuckDuckGo.xcodeproj/project.pbxproj | 24 ++++++++++ .../NetworkProtectionPixelEvent.swift | 36 +++++++++++++- ...rkProtectionUNNotificationsPresenter.swift | 15 ++++++ .../DuckDuckGoNotificationsAppDelegate.swift | 48 +++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 40365ef615..df5499bc80 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1083,6 +1083,12 @@ 4B1E6EEE27AB5E5100F51793 /* PasswordManagementListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EEC27AB5E5100F51793 /* PasswordManagementListSection.swift */; }; 4B1E6EF127AB5E5D00F51793 /* NSPopUpButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EEF27AB5E5D00F51793 /* NSPopUpButtonView.swift */; }; 4B1E6EF227AB5E5D00F51793 /* PasswordManagementItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EF027AB5E5D00F51793 /* PasswordManagementItemList.swift */; }; + 4B1EFF1C2BD71EEF007CC84F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B1EFF1B2BD71EEF007CC84F /* PixelKit */; }; + 4B1EFF1D2BD71FCA007CC84F /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 4B1EFF1E2BD72034007CC84F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 4B1EFF1F2BD72170007CC84F /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 4B1EFF212BD72189007CC84F /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B1EFF202BD72189007CC84F /* Networking */; }; + 4B1EFF222BD7223D007CC84F /* NetworkProtectionPixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */; }; 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; @@ -4153,7 +4159,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B1EFF1C2BD71EEF007CC84F /* PixelKit in Frameworks */, 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */, + 4B1EFF212BD72189007CC84F /* Networking in Frameworks */, 37269F052B3332C2005E8E46 /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8363,6 +8371,8 @@ packageProductDependencies = ( 37269F042B3332C2005E8E46 /* Common */, 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */, + 4B1EFF1B2BD71EEF007CC84F /* PixelKit */, + 4B1EFF202BD72189007CC84F /* Networking */, ); productName = DuckDuckGoNotifications; productReference = 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */; @@ -10580,9 +10590,13 @@ 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, + 4B1EFF1D2BD71FCA007CC84F /* UserDefaultsWrapper.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, + 4B1EFF222BD7223D007CC84F /* NetworkProtectionPixelEvent.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, + 4B1EFF1F2BD72170007CC84F /* OptionalExtension.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, + 4B1EFF1E2BD72034007CC84F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -12946,6 +12960,16 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Navigation; }; + 4B1EFF1B2BD71EEF007CC84F /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + 4B1EFF202BD72189007CC84F /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 4B2D062B2A11C0E100DE1F49 /* Networking */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 0a29c23807..50a2b96444 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -97,6 +97,13 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionUnhandledError(function: String, line: Int, error: Error) + // Temporary pixels added to verify notification delivery rates: + case networkProtectionConnectedNotificationDisplayed + case networkProtectionDisconnectedNotificationDisplayed + case networkProtectionReconnectingNotificationDisplayed + case networkProtectionSupersededNotificationDisplayed + case networkProtectionExpiredEntitlementNotificationDisplayed + /// Name of the pixel event /// - Unique pixels must end with `_u` /// - Daily pixels will automatically have `_d` or `_c` appended to their names @@ -273,6 +280,21 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionUnhandledError: return "netp_unhandled_error" + + case .networkProtectionConnectedNotificationDisplayed: + return "netp_connected_notification_displayed" + + case .networkProtectionDisconnectedNotificationDisplayed: + return "netp_disconnected_notification_displayed" + + case .networkProtectionReconnectingNotificationDisplayed: + return "netp_reconnecting_notification_displayed" + + case .networkProtectionSupersededNotificationDisplayed: + return "netp_superseded_notification_displayed" + + case .networkProtectionExpiredEntitlementNotificationDisplayed: + return "netp_expired_entitlement_notification_displayed" } } @@ -360,7 +382,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionRekeyAttempt, .networkProtectionRekeyCompleted, .networkProtectionRekeyFailure, - .networkProtectionSystemExtensionActivationFailure: + .networkProtectionSystemExtensionActivationFailure, + .networkProtectionConnectedNotificationDisplayed, + .networkProtectionDisconnectedNotificationDisplayed, + .networkProtectionReconnectingNotificationDisplayed, + .networkProtectionSupersededNotificationDisplayed, + .networkProtectionExpiredEntitlementNotificationDisplayed: return nil } } @@ -425,7 +452,12 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionWireguardErrorCannotStartWireguardBackend, .networkProtectionNoAuthTokenFoundError, .networkProtectionRekeyAttempt, - .networkProtectionRekeyCompleted: + .networkProtectionRekeyCompleted, + .networkProtectionConnectedNotificationDisplayed, + .networkProtectionDisconnectedNotificationDisplayed, + .networkProtectionReconnectingNotificationDisplayed, + .networkProtectionSupersededNotificationDisplayed, + .networkProtectionExpiredEntitlementNotificationDisplayed: return nil } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index 3977fccef5..80ac5013fc 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -20,6 +20,7 @@ import Foundation import UserNotifications import NetworkProtection import NetworkProtectionUI +import PixelKit extension UNNotificationAction { @@ -159,6 +160,20 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti _=self.registerNotificationCategoriesOnce self.userNotificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue]) self.userNotificationCenter.add(request) + + switch identifier { + case .disconnected: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionDisconnectedNotificationDisplayed, frequency: .dailyAndCount) + case .reconnecting: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionReconnectingNotificationDisplayed, frequency: .dailyAndCount) + case .connected: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionConnectedNotificationDisplayed, frequency: .dailyAndCount) + case .superseded: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionSupersededNotificationDisplayed, frequency: .dailyAndCount) + case .expiredEntitlement: + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionExpiredEntitlementNotificationDisplayed, frequency: .dailyAndCount) + case .test: break + } } } diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index 41677dc8f1..75b3471644 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -19,6 +19,8 @@ import Cocoa import Combine import Common +import Networking +import PixelKit import NetworkExtension import NetworkProtection @@ -69,6 +71,38 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("Login item finished launching", log: .networkProtectionLoginItemLog, type: .info) + let dryRun: Bool + +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + let pixelSource: String + +#if NETP_SYSTEM_EXTENSION + pixelSource = "vpnNotificationAgent" +#else + pixelSource = "vpnNotificationAgentAppStore" // Should never get used, but just in case +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: pixelSource, + defaultHeaders: [:], + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } + startObservingVPNStatusChanges() os_log("Login item listening") } @@ -157,3 +191,17 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate } } + +extension NSApplication { + + enum RunType: Int, CustomStringConvertible { + case normal + var description: String { + switch self { + case .normal: return "normal" + } + } + } + static var runType: RunType { .normal } + +} From f14a2d444b7f0bacfa91d80a844a8fb470bae6ac Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Mon, 29 Apr 2024 18:06:17 +0200 Subject: [PATCH 047/134] Fix bookmarks and fav rearrangement (#2686) Task/Issue URL: https://app.asana.com/0/1201493110486074/1207136222342904/f Tech Design URL: CC: **Description**: Expand test suite to cover stub objects when rearranging Bookmarks. **Steps to test this PR**: Check the tests that've been added. Go to Debug -> Sync -> Populate Stub Objects. This will create a mix of stub and non stub objects in both root folder and favorite folders. Test rearranging of both regular bookmarks and favorites. --- ###### 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) --- .../Services/LocalBookmarkStore.swift | 52 ++++- DuckDuckGo/Sync/SyncDebugMenu.swift | 26 ++- .../Services/LocalBookmarkStoreTests.swift | 210 ++++++++++++++++++ 3 files changed, 276 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index 2a480e8333..d8cdf7773f 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -655,21 +655,37 @@ final class LocalBookmarkStore: BookmarkStore { currentInsertionIndex > objectIndex { adjustedInsertionIndex -= 1 } + let nextInsertionIndex = adjustedInsertionIndex + 1 bookmarkManagedObject.parent = nil // Removing the bookmark from its current parent may have removed it from the collection it is about to be added to, so re-check // the bounds before adding it back. if adjustedInsertionIndex < newParentFolder.childrenArray.count { - newParentFolder.insertIntoChildren(bookmarkManagedObject, at: adjustedInsertionIndex) + // Handle stubs + let allChildren = (newParentFolder.children?.array as? [BookmarkEntity]) ?? [] + if newParentFolder.childrenArray.count != allChildren.count { + var correctedIndex = 0 + + while adjustedInsertionIndex > 0 && correctedIndex < allChildren.count { + if allChildren[correctedIndex].isStub == false { + adjustedInsertionIndex -= 1 + } + correctedIndex += 1 + } + newParentFolder.insertIntoChildren(bookmarkManagedObject, at: correctedIndex) + } else { + newParentFolder.insertIntoChildren(bookmarkManagedObject, at: adjustedInsertionIndex) + } } else { newParentFolder.addToChildren(bookmarkManagedObject) } - currentInsertionIndex = adjustedInsertionIndex + 1 + currentInsertionIndex = nextInsertionIndex } } + // swiftlint:disable:next function_body_length func moveFavorites(with objectUUIDs: [String], toIndex index: Int?, completion: @escaping (Error?) -> Void) { applyChangesAndSave(changes: { [weak self] context in @@ -692,27 +708,45 @@ final class LocalBookmarkStore: BookmarkStore { return (try? context.fetch(entityFetchRequest))?.first } - if let index = index, index < (displayedFavoritesFolder.favorites?.count ?? 0) { + if let index = index, index < displayedFavoritesFolder.favoritesArray.count { var currentInsertionIndex = max(index, 0) for bookmarkManagedObject in bookmarkManagedObjects { var adjustedInsertionIndex = currentInsertionIndex - if let currentIndex = displayedFavoritesFolder.favorites?.index(of: bookmarkManagedObject), + if let currentIndex = displayedFavoritesFolder.favoritesArray.firstIndex(of: bookmarkManagedObject), currentInsertionIndex > currentIndex { adjustedInsertionIndex -= 1 } + let nextInsertionIndex = adjustedInsertionIndex + 1 bookmarkManagedObject.removeFromFavorites(with: favoritesDisplayMode) - if adjustedInsertionIndex < (displayedFavoritesFolder.favorites?.count ?? 0) { - bookmarkManagedObject.addToFavorites(insertAt: adjustedInsertionIndex, - favoritesRoot: displayedFavoritesFolder) - bookmarkManagedObject.addToFavorites(folders: favoritesFoldersWithoutDisplayed) + + if adjustedInsertionIndex < displayedFavoritesFolder.favoritesArray.count { + // Handle stubs + let allChildren = (displayedFavoritesFolder.favorites?.array as? [BookmarkEntity]) ?? [] + if displayedFavoritesFolder.favoritesArray.count != allChildren.count { + var correctedIndex = 0 + + while adjustedInsertionIndex > 0 && correctedIndex < allChildren.count { + if allChildren[correctedIndex].isStub == false { + adjustedInsertionIndex -= 1 + } + correctedIndex += 1 + } + bookmarkManagedObject.addToFavorites(insertAt: correctedIndex, + favoritesRoot: displayedFavoritesFolder) + bookmarkManagedObject.addToFavorites(folders: favoritesFoldersWithoutDisplayed) + } else { + bookmarkManagedObject.addToFavorites(insertAt: adjustedInsertionIndex, + favoritesRoot: displayedFavoritesFolder) + bookmarkManagedObject.addToFavorites(folders: favoritesFoldersWithoutDisplayed) + } } else { bookmarkManagedObject.addToFavorites(folders: favoritesFolders) } - currentInsertionIndex = adjustedInsertionIndex + 1 + currentInsertionIndex = nextInsertionIndex } } else { for bookmarkManagedObject in bookmarkManagedObjects { diff --git a/DuckDuckGo/Sync/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift index da1fe3e0be..c5ff802e84 100644 --- a/DuckDuckGo/Sync/SyncDebugMenu.swift +++ b/DuckDuckGo/Sync/SyncDebugMenu.swift @@ -89,14 +89,34 @@ final class SyncDebugMenu: NSMenu { context.performAndWait { let root = BookmarkUtils.fetchRootFolder(context)! + let favorites = BookmarkUtils.fetchFavoritesFolders(for: .displayNative(.desktop), in: context) + + let nonStub1 = BookmarkEntity.makeBookmark(title: "Non stub", url: "url", parent: root, context: context) + nonStub1.addToFavorites(folders: favorites) + + let stub1 = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub1.isStub = true + stub1.addToFavorites(folders: favorites) - _ = BookmarkEntity.makeBookmark(title: "Non stub", url: "url", parent: root, context: context) - let stub = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) - stub.isStub = true let emptyStub = BookmarkEntity.makeBookmark(title: "", url: "", parent: root, context: context) emptyStub.isStub = true emptyStub.title = nil emptyStub.url = nil + emptyStub.addToFavorites(folders: favorites) + + let nonStub2 = BookmarkEntity.makeBookmark(title: "Non stub 2", url: "url", parent: root, context: context) + nonStub2.addToFavorites(folders: favorites) + + let stub2 = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub2.isStub = true + stub2.addToFavorites(folders: favorites) + + let stub3 = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context) + stub3.isStub = true + stub3.addToFavorites(folders: favorites) + + let nonStub3 = BookmarkEntity.makeBookmark(title: "Non stub 3", url: "url", parent: root, context: context) + nonStub3.addToFavorites(folders: favorites) try? context.save() } diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 73d2f96e8d..1619b751fa 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -30,6 +30,7 @@ extension LocalBookmarkStore { } } +@MainActor final class LocalBookmarkStoreTests: XCTestCase { // MARK: Save/Delete @@ -441,6 +442,122 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } + func testWhenMovingBookmarkWithinParentCollection_AndThereAreStubs_ThenIndexIsCalculatedAndBookmarkIsMoved() async { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + guard let rootMO = BookmarkUtils.fetchRootFolder(context) else { + XCTFail("Missing root folder") + return + } + + let folderMO = BookmarkEntity.makeFolder(title: "Parent", parent: rootMO, context: context) + + let bookmarkStub1MO = BookmarkEntity.makeBookmark(title: "Stub 1", url: "", parent: folderMO, + context: context) + bookmarkStub1MO.isStub = true + + let bookmark1MO = BookmarkEntity.makeBookmark(title: "Example 1", url: "https://example1.com", parent: folderMO, + context: context) + + let bookmarkStub2MO = BookmarkEntity.makeBookmark(title: "Stub 2", url: "", parent: folderMO, + context: context) + bookmarkStub2MO.isStub = true + + let bookmark2MO = BookmarkEntity.makeBookmark(title: "Example 2", url: "https://example2.com", parent: folderMO, + context: context) + + let bookmarkStub3MO = BookmarkEntity.makeBookmark(title: "Stub 3", url: "", parent: folderMO, + context: context) + bookmarkStub3MO.isStub = true + let bookmarkStub4MO = BookmarkEntity.makeBookmark(title: "Stub 4", url: "", parent: folderMO, + context: context) + bookmarkStub4MO.isStub = true + + let bookmark3MO = BookmarkEntity.makeBookmark(title: "Example 3", url: "https://example3.com", parent: folderMO, + context: context) + + let bookmarkStub5MO = BookmarkEntity.makeBookmark(title: "Stub 5", url: "", parent: folderMO, + context: context) + bookmarkStub5MO.isStub = true + + let bookmark4MO = BookmarkEntity.makeBookmark(title: "Example 4", url: "https://example3.com", parent: folderMO, + context: context) + + let bookmarkStub6MO = BookmarkEntity.makeBookmark(title: "Stub 6", url: "", parent: folderMO, + context: context) + bookmarkStub6MO.isStub = true + + // Save the initial bookmarks state: + + do { + try context.save() + } catch { + XCTFail("Failed to save context") + } + + // Fetch persisted bookmarks back from the store: + + guard case let .success(initialTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), + let initialParentFolder = initialTopLevelEntities.first as? BookmarkFolder else { + XCTFail("Couldn't load top level entities") + return + } + + XCTAssertEqual(initialParentFolder.children.count, 4) + + // Verify initial order of saved bookmarks: + + let initialBookmarkUUIDs = [bookmark1MO.uuid, bookmark2MO.uuid, bookmark3MO.uuid, bookmark4MO.uuid] + let initialFetchedBookmarkUUIDs = initialParentFolder.children.map(\.id) + XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) + + func testMoving(bookmarkUUIDs: [String], toIndex: Int) async -> [String] { + let moveBookmarksError = await bookmarkStore.move(objectUUIDs: bookmarkUUIDs, toIndex: toIndex, withinParentFolder: .parent(uuid: folderMO.uuid!)) + XCTAssertNil(moveBookmarksError) + + guard case let .success(updatedTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), + let updatedParentFolder = updatedTopLevelEntities.first as? BookmarkFolder else { + XCTFail("Couldn't load top level entities") + return [] + } + + return updatedParentFolder.children.map(\.title) + } + + // Update the order of the bookmarks: + // More than one bookmark + // To the end + var result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 4) + XCTAssertEqual(result, [bookmark3MO.title, bookmark4MO.title, bookmark1MO.title, bookmark2MO.title]) + // To the beginning + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark2MO.title, bookmark3MO.title, bookmark4MO.title]) + // To middle + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 3) + XCTAssertEqual(result, [bookmark3MO.title, bookmark1MO.title, bookmark2MO.title, bookmark4MO.title]) + // To the beginning + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!, bookmark2MO.uuid!], toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark2MO.title, bookmark3MO.title, bookmark4MO.title]) + + // Single bookmark + // Middle to end + result = await testMoving(bookmarkUUIDs: [bookmark2MO.uuid!], toIndex: 4) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to Beginning + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!], toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to First + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!], toIndex: 1) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to Second + result = await testMoving(bookmarkUUIDs: [bookmark1MO.uuid!], toIndex: 2) + XCTAssertEqual(result, [bookmark3MO.title, bookmark1MO.title, bookmark4MO.title, bookmark2MO.title]) + // First to End + result = await testMoving(bookmarkUUIDs: [bookmark3MO.uuid!], toIndex: 4) + XCTAssertEqual(result, [bookmark1MO.title, bookmark4MO.title, bookmark2MO.title, bookmark3MO.title]) + } + func testWhenMovingBookmarkWithinParentCollection_AndIndexIsOutOfBounds_ThenBookmarkIsAppended() async { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) @@ -871,6 +988,99 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } + func testWhenMovingFavorite_AndThereAreStubs_ThenIndexIsCalculatedAndBookmarkIsMoved() async { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + bookmarkStore.applyFavoritesDisplayMode(.displayUnified(native: .desktop)) + + guard let rootMO = BookmarkUtils.fetchRootFolder(context) else { + XCTFail("Missing root folder") + return + } + + let folderMO = BookmarkEntity.makeFolder(title: "Parent", parent: rootMO, context: context) + + let bookmark1MO = BookmarkEntity.makeBookmark(title: "Example 1", url: "https://example1.com", parent: folderMO, + context: context) + let bookmark2MO = BookmarkEntity.makeBookmark(title: "Example 2", url: "https://example2.com", parent: folderMO, + context: context) + let bookmarkStub1MO = BookmarkEntity.makeBookmark(title: "Stub 1", url: "", parent: folderMO, + context: context) + bookmarkStub1MO.isStub = true + let bookmarkStub2MO = BookmarkEntity.makeBookmark(title: "Stub 2", url: "", parent: folderMO, + context: context) + bookmarkStub2MO.isStub = true + let bookmark3MO = BookmarkEntity.makeBookmark(title: "Example 3", url: "https://example3.com", parent: folderMO, + context: context) + let bookmarkStub3MO = BookmarkEntity.makeBookmark(title: "Stub 3", url: "", parent: folderMO, + context: context) + bookmarkStub3MO.isStub = true + + let favoriteRoots = BookmarkUtils.fetchFavoritesFolders(for: .displayUnified(native: .desktop), in: context) + guard !favoriteRoots.isEmpty else { + XCTFail("No favorite root") + return + } + bookmark1MO.addToFavorites(folders: favoriteRoots) + bookmark2MO.addToFavorites(folders: favoriteRoots) + bookmarkStub1MO.addToFavorites(folders: favoriteRoots) + bookmarkStub2MO.addToFavorites(folders: favoriteRoots) + bookmark3MO.addToFavorites(folders: favoriteRoots) + bookmarkStub3MO.addToFavorites(folders: favoriteRoots) + + // Save the initial state: + + do { + try context.save() + } catch { + XCTFail("Failed to save context") + } + + // Fetch persisted bookmarks back from the store: + + guard case let .success(favorites) = await bookmarkStore.loadAll(type: .favorites) else { + XCTFail("Couldn't load top level entities") + return + } + + XCTAssertEqual(favorites.count, 3) + + // Verify initial order of saved bookmarks: + + let initialBookmarkUUIDs = [bookmark1MO.uuid, bookmark2MO.uuid, bookmark3MO.uuid] + let initialFetchedBookmarkUUIDs = favorites.map(\.id) + XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) + + func testMoving(bookmarkUUID: String, toIndex: Int) async -> [String] { + let moveBookmarksError = await bookmarkStore.moveFavorites(with: [bookmarkUUID], toIndex: toIndex) + XCTAssertNil(moveBookmarksError) + + guard case let .success(updatedFavorites) = await bookmarkStore.loadAll(type: .favorites) else { + XCTFail("Couldn't load top level entities") + return [] + } + + return updatedFavorites.map(\.title) + } + + // Update the order of the bookmarks: + // Middle to end + var result = await testMoving(bookmarkUUID: bookmark2MO.uuid!, toIndex: 3) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark2MO.title]) + // First to Beginning + result = await testMoving(bookmarkUUID: bookmark1MO.uuid!, toIndex: 0) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark2MO.title]) + // First to First + result = await testMoving(bookmarkUUID: bookmark1MO.uuid!, toIndex: 1) + XCTAssertEqual(result, [bookmark1MO.title, bookmark3MO.title, bookmark2MO.title]) + // First to Second + result = await testMoving(bookmarkUUID: bookmark1MO.uuid!, toIndex: 2) + XCTAssertEqual(result, [bookmark3MO.title, bookmark1MO.title, bookmark2MO.title]) + // First to End + result = await testMoving(bookmarkUUID: bookmark3MO.uuid!, toIndex: 3) + XCTAssertEqual(result, [bookmark1MO.title, bookmark2MO.title, bookmark3MO.title]) + } + func testWhenMovingFavorite_AndIndexIsOutOfBounds_ThenFavoriteIsAppended() async { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) From 79d648da2723bdca68e5bef763e5cd357e5e253a Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 29 Apr 2024 13:22:46 -0300 Subject: [PATCH 048/134] DBP: Implement pixels to observe initial scan loading times (#2706) --- .../DBP/DataBrokerProtectionAppEvents.swift | 4 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 2 +- ...taBrokerProtectionLoginItemScheduler.swift | 3 +- .../DataBrokerProtectionPixelsHandler.swift | 6 ++- .../IPCServiceManager.swift | 3 +- .../DataBrokerProtectionDataManager.swift | 21 +++++--- .../DataBrokerRunCustomJSONViewModel.swift | 8 ++- .../DebugUI/DebugScanOperation.swift | 5 ++ .../IPC/DataBrokerProtectionIPCClient.swift | 3 +- .../DataBrokerProtectionIPCScheduler.swift | 3 +- .../IPC/DataBrokerProtectionIPCServer.swift | 5 +- .../Model/DBPUIViewModel.swift | 6 +-- .../Operations/DataBrokerOperation.swift | 25 ++++++++++ .../DataBrokerOperationRunner.swift | 10 ++++ .../DataBrokerOperationsCollection.swift | 1 + ...taBrokerProfileQueryOperationManager.swift | 13 ++++- .../Operations/OptOutOperation.swift | 4 ++ .../Operations/ScanOperation.swift | 4 ++ .../Pixels/DataBrokerProtectionPixels.swift | 50 +++++++++++++++---- ...kerProtectionStageDurationCalculator.swift | 13 +++-- .../DataBrokerProtectionNoOpScheduler.swift | 2 +- .../DataBrokerProtectionProcessor.swift | 1 + .../DataBrokerProtectionScheduler.swift | 21 ++++++-- .../DataBrokerOperationActionTests.swift | 20 ++++++++ ...kerProfileQueryOperationManagerTests.swift | 7 +-- ...otectionStageDurationCalculatorTests.swift | 14 +++--- .../DataBrokerProtectionTests/Mocks.swift | 1 + 27 files changed, 204 insertions(+), 51 deletions(-) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 05f1584b80..07a579367f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -49,8 +49,8 @@ struct DataBrokerProtectionAppEvents { // If we don't have profileQueries it means there's no user profile saved in our DB // In this case, let's disable the agent and delete any left-over data because there's nothing for it to do - if let profileQueries = try? DataBrokerProtectionManager.shared.dataManager.fetchBrokerProfileQueryData(ignoresCache: true), - profileQueries.count > 0 { + if let profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), + profileQueriesCount > 0 { restartBackgroundAgent(loginItemsManager: loginItemsManager) } else { featureVisibility.disableAndDeleteForWaitlistUsers() diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 48a458f3cf..b22ee8eac2 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -227,7 +227,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView) { errors in + DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView, startTime: Date()) { errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift index cc0d841ee8..50b75e9fac 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift @@ -56,9 +56,10 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler } func startManualScan(showWebView: Bool, + startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { enableLoginItem() - ipcScheduler.startManualScan(showWebView: showWebView, completion: completion) + ipcScheduler.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) } func startScheduler(showWebView: Bool) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 08c5b74f18..2f91fe97a0 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -99,7 +99,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Void)) { pixelHandler.fire(.ipcServerScanAllBrokersReceivedByAgent) - scheduler.startManualScan(showWebView: showWebView) { errors in + scheduler.startManualScan(showWebView: showWebView, startTime: startTime) { errors in if let error = errors?.oneTimeError { switch error { case DataBrokerProtectionSchedulerError.operationsInterrupted: diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index ac8544c81e..b006af7433 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -30,12 +30,7 @@ public protocol DataBrokerProtectionDataManaging { func fetchBrokerProfileQueryData(ignoresCache: Bool) throws -> [BrokerProfileQueryData] func prepareBrokerProfileQueryDataCache() throws func hasMatches() throws -> Bool -} - -extension DataBrokerProtectionDataManaging { - func fetchBrokerProfileQueryData() throws -> [BrokerProfileQueryData] { - try fetchBrokerProfileQueryData(ignoresCache: false) - } + func profileQueriesCount() throws -> Int } public protocol DataBrokerProtectionDataManagerDelegate: AnyObject { @@ -74,6 +69,18 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { return cache.profile } + return try fetchProfileFromDB() + } + + public func profileQueriesCount() throws -> Int { + guard let profile = try fetchProfileFromDB() else { + throw DataBrokerProtectionError.dataNotInDatabase + } + + return profile.profileQueries.count + } + + private func fetchProfileFromDB() throws -> DataBrokerProtectionProfile? { if let profile = try database.fetchProfile() { cache.profile = profile return profile @@ -287,7 +294,7 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { } func startScanAndOptOut() -> Bool { - return scanDelegate?.startScan() ?? false + return scanDelegate?.startScan(startDate: Date()) ?? false } func getInitialScanState() async -> DBPUIInitialScanState { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 9b6a7e00ac..6b5894d5e1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -145,6 +145,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { private let runnerProvider: OperationRunnerProvider private let privacyConfigManager: PrivacyConfigurationManaging + private let fakePixelHandler: EventMapping = EventMapping { event, _, _, _ in + print(event) + } private let contentScopeProperties: ContentScopeProperties private let csvColumns = ["name_input", "age_input", "city_input", "state_input", "name_scraped", "age_scraped", "address_scraped", "relatives_scraped", "url", "broker name", "screenshot_id", "error", "matched_fields", "result_match", "expected_match"] @@ -347,7 +350,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { Task { do { - let extractedProfiles = try await runner.scan(query, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true } + let extractedProfiles = try await runner.scan(query, stageCalculator: FakeStageDurationCalculator(), pixelHandler: fakePixelHandler, showWebView: true) { true } DispatchQueue.main.async { for extractedProfile in extractedProfiles { @@ -383,7 +386,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { ) Task { do { - try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: scanResult.extractedProfile, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { + try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: scanResult.extractedProfile, stageCalculator: FakeStageDurationCalculator(), pixelHandler: fakePixelHandler, showWebView: true) { true } @@ -473,6 +476,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { final class FakeStageDurationCalculator: StageDurationCalculator { var attemptId: UUID = UUID() + var isManualScan: Bool = false func durationSinceLastStage() -> Double { 0.0 diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift index fb632105a3..3d4e4b779c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift @@ -68,6 +68,8 @@ final class DebugScanOperation: DataBrokerOperation { var scanURL: String? let clickAwaitTime: TimeInterval let cookieHandler: CookieHandler + let pixelHandler: EventMapping + var postLoadingSiteStartTime: Date? private let fileManager = FileManager.default private let debugScanContentPath: String? @@ -96,6 +98,9 @@ final class DebugScanOperation: DataBrokerOperation { } self.cookieHandler = EmptyCookieHandler() stageCalculator = FakeStageDurationCalculator() + pixelHandler = EventMapping(mapping: { _, _, _, _ in + // We do not need the pixel handler for the debug + }) } func run(inputValue: Void, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index f7b809ac9f..1f1d2451b2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -137,6 +137,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { } public func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { self.pixelHandler.fire(.ipcServerScanAllBrokersCalledByApp) @@ -155,7 +156,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { } xpc.execute(call: { server in - server.startManualScan(showWebView: showWebView) { errors in + server.startManualScan(showWebView: showWebView, startTime: startTime) { errors in if let error = errors?.oneTimeError { let nsError = error as NSError let interruptedError = DataBrokerProtectionSchedulerError.operationsInterrupted as NSError diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift index 0763404514..ea73c3e1a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift @@ -52,9 +52,10 @@ public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionSchedul } public func startManualScan(showWebView: Bool, + startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } - ipcClient.startManualScan(showWebView: showWebView, completion: completion) + ipcClient.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) } public func runQueuedOperations(showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index dce168b8fe..7f4ae3e840 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -95,6 +95,7 @@ public protocol IPCServerInterface: AnyObject { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) @@ -136,6 +137,7 @@ protocol XPCServerInterface { func optOutAllBrokers(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) @@ -214,8 +216,9 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { } func startManualScan(showWebView: Bool, + startTime: Date, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.startManualScan(showWebView: showWebView, completion: completion) + serverDelegate?.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) } func runQueuedOperations(showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index df3cf6fd82..a43f6d9ae0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -23,7 +23,7 @@ import BrowserServicesKit import Common protocol DBPUIScanOps: AnyObject { - func startScan() -> Bool + func startScan(startDate: Date) -> Bool func updateCacheWithCurrentScans() async func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? } @@ -74,8 +74,8 @@ final class DBPUIViewModel { } extension DBPUIViewModel: DBPUIScanOps { - func startScan() -> Bool { - scheduler.startManualScan() + func startScan(startDate: Date) -> Bool { + scheduler.startManualScan(startTime: startDate) return true } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index fb88d48e61..893ed9e350 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -33,6 +33,7 @@ protocol DataBrokerOperation: CCFCommunicationDelegate { var captchaService: CaptchaServiceProtocol { get } var cookieHandler: CookieHandler { get } var stageCalculator: StageDurationCalculator { get } + var pixelHandler: EventMapping { get } var webViewHandler: WebViewHandler? { get set } var actionsHandler: ActionsHandler? { get } @@ -41,6 +42,7 @@ protocol DataBrokerOperation: CCFCommunicationDelegate { var shouldRunNextStep: () -> Bool { get } var retriesCountOnError: Int { get set } var clickAwaitTime: TimeInterval { get } + var postLoadingSiteStartTime: Date? { get set } func run(inputValue: InputValue, webViewHandler: WebViewHandler?, @@ -151,11 +153,13 @@ extension DataBrokerOperation { } func complete(_ value: ReturnValue) { + self.firePostLoadingDurationPixel(hasError: false) self.continuation?.resume(returning: value) self.continuation = nil } func failed(with error: Error) { + self.firePostLoadingDurationPixel(hasError: true) self.continuation?.resume(throwing: error) self.continuation = nil } @@ -175,6 +179,8 @@ extension DataBrokerOperation { // MARK: - CSSCommunicationDelegate func loadURL(url: URL) async { + let webSiteStartLoadingTime = Date() + do { // https://app.asana.com/0/1204167627774280/1206912494469284/f if query.dataBroker.url == "spokeo.com" { @@ -183,12 +189,31 @@ extension DataBrokerOperation { } } try await webViewHandler?.load(url: url) + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: false) + postLoadingSiteStartTime = Date() await executeNextStep() } catch { + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: true) await onError(error: error) } } + private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { + if stageCalculator.isManualScan { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + } + } + + func firePostLoadingDurationPixel(hasError: Bool) { + if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + } + } + func success(actionId: String, actionType: ActionType) async { switch actionType { case .click: diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift index 15bfb11e9f..52a64c3fac 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift @@ -24,12 +24,14 @@ protocol WebOperationRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -38,10 +40,12 @@ extension WebOperationRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { try await scan(profileQuery, stageCalculator: stageCalculator, + pixelHandler: pixelHandler, showWebView: false, shouldRunNextStep: shouldRunNextStep) } @@ -49,11 +53,13 @@ extension WebOperationRunner { func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool) async throws { try await optOut(profileQuery: profileQuery, extractedProfile: extractedProfile, stageCalculator: stageCalculator, + pixelHandler: pixelHandler, showWebView: false, shouldRunNextStep: shouldRunNextStep) } @@ -78,6 +84,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { let scan = ScanOperation( @@ -87,6 +94,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { emailService: emailService, captchaService: captchaService, stageDurationCalculator: stageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: shouldRunNextStep ) return try await scan.run(inputValue: (), showWebView: showWebView) @@ -95,6 +103,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { let optOut = OptOutOperation( @@ -104,6 +113,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { emailService: emailService, captchaService: captchaService, stageCalculator: stageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: shouldRunNextStep ) try await optOut.run(inputValue: extractedProfile, showWebView: showWebView) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index 81e3859c17..cbe22fe8fe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -181,6 +181,7 @@ final class DataBrokerOperationsCollection: Operation { runner: runner, pixelHandler: pixelHandler, showWebView: showWebView, + isManualScan: operationType == .scan, userNotificationService: userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index f1e9a7b377..f7eeb24d2b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -35,6 +35,7 @@ protocol OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, showWebView: Bool, + isManualScan: Bool, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -47,6 +48,7 @@ extension OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, userNotificationService: DataBrokerProtectionUserNotificationService, + isManual: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { try await runOperation(operationData: operationData, @@ -56,6 +58,7 @@ extension OperationsManager { runner: runner, pixelHandler: pixelHandler, showWebView: false, + isManualScan: isManual, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } @@ -70,6 +73,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, showWebView: Bool = false, + isManualScan: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { @@ -80,6 +84,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, + isManual: isManualScan, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } else if let optOutOperationData = operationData as? OptOutOperationData { @@ -102,6 +107,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: NotificationCenter, pixelHandler: EventMapping, showWebView: Bool = false, + isManual: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { os_log("Running scan operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) @@ -118,13 +124,15 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) - let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler) + let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, + handler: pixelHandler, + isManualScan: isManual) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) try database.add(event) - let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: stageCalculator, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) + let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: stageCalculator, pixelHandler: pixelHandler, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) os_log("Extracted profiles: %@", log: .dataBrokerProtection, extractedProfiles) if !extractedProfiles.isEmpty { @@ -322,6 +330,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: extractedProfile, stageCalculator: stageDurationCalculator, + pixelHandler: pixelHandler, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift index bf5be40ecb..0a957a62f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift @@ -40,6 +40,8 @@ final class OptOutOperation: DataBrokerOperation { private let operationAwaitTime: TimeInterval let shouldRunNextStep: () -> Bool let clickAwaitTime: TimeInterval + let pixelHandler: EventMapping + var postLoadingSiteStartTime: Date? // Captcha is a third-party resource that sometimes takes more time to load // if we are not able to get the captcha information. We will try to run the action again @@ -57,6 +59,7 @@ final class OptOutOperation: DataBrokerOperation { operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 40, stageCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool ) { self.privacyConfig = privacyConfig @@ -69,6 +72,7 @@ final class OptOutOperation: DataBrokerOperation { self.shouldRunNextStep = shouldRunNextStep self.clickAwaitTime = clickAwaitTime self.cookieHandler = cookieHandler + self.pixelHandler = pixelHandler } func run(inputValue: ExtractedProfile, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index 10bf5bb9e9..f7bb9de995 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -41,6 +41,8 @@ final class ScanOperation: DataBrokerOperation { let shouldRunNextStep: () -> Bool var retriesCountOnError: Int = 0 let clickAwaitTime: TimeInterval + let pixelHandler: EventMapping + var postLoadingSiteStartTime: Date? init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, @@ -51,6 +53,7 @@ final class ScanOperation: DataBrokerOperation { operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 0, stageDurationCalculator: StageDurationCalculator, + pixelHandler: EventMapping, shouldRunNextStep: @escaping () -> Bool ) { self.privacyConfig = privacyConfig @@ -63,6 +66,7 @@ final class ScanOperation: DataBrokerOperation { self.shouldRunNextStep = shouldRunNextStep self.clickAwaitTime = clickAwaitTime self.cookieHandler = cookieHandler + self.pixelHandler = pixelHandler } func run(inputValue: InputValue, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index d9de1179a7..ad7f3cd61d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -64,6 +64,11 @@ public enum DataBrokerProtectionPixels { static let wasOnWaitlist = "was_on_waitlist" static let httpCode = "http_code" static let backendServiceCallSite = "backend_service_callsite" + static let isManualScan = "is_manual_scan" + static let durationInMs = "duration_in_ms" + static let profileQueries = "profile_queries" + static let hasError = "has_error" + static let brokerURL = "broker_url" } case error(error: DataBrokerProtectionError, dataBroker: String) @@ -138,9 +143,9 @@ public enum DataBrokerProtectionPixels { case dataBrokerProtectionNotificationOpenedAllRecordsRemoved // Scan/Search pixels - case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int) - case scanFailed(dataBroker: String, duration: Double, tries: Int) - case scanError(dataBroker: String, duration: Double, category: String, details: String) + case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isManualScan: Bool) + case scanFailed(dataBroker: String, duration: Double, tries: Int, isManualScan: Bool) + case scanError(dataBroker: String, duration: Double, category: String, details: String, isManualScan: Bool) // KPIs - engagement case dailyActiveUser @@ -168,6 +173,13 @@ public enum DataBrokerProtectionPixels { case homeViewShowBadPathError case homeViewCTAMoveApplicationClicked case homeViewCTAGrantPermissionClicked + + // Initial scans pixels + // https://app.asana.com/0/1204006570077678/1206981742767458/f + case initialScanTotalDuration(duration: Double, profileQueries: Int) + case initialScanSiteLoadDuration(duration: Double, hasError: Bool, brokerURL: String) + case initialScanPostLoadingDuration(duration: Double, hasError: Bool, brokerURL: String) + case initialScanPreStartDuration(duration: Double) } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -279,6 +291,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .homeViewShowBadPathError: return "m_mac_dbp_home_view_show-bad-path-error" case .homeViewCTAMoveApplicationClicked: return "m_mac_dbp_home_view-cta-move-application-clicked" case .homeViewCTAGrantPermissionClicked: return "m_mac_dbp_home_view-cta-grant-permission-clicked" + + // Initial scans pixels + case .initialScanTotalDuration: return "m_mac_dbp_initial_scan_duration" + case .initialScanSiteLoadDuration: return "m_mac_dbp_scan_broker_site_loaded" + case .initialScanPostLoadingDuration: return "m_mac_dbp_initial_scan_broker_post_loading" + case .initialScanPreStartDuration: return "m_mac_dbp_initial_scan_pre_start_duration" } } @@ -403,12 +421,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .ipcServerRunQueuedOperationsCompletion, .ipcServerRunAllOperations: return [Consts.bundleIDParamKey: Bundle.main.bundleIdentifier ?? "nil"] - case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries): - return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries)] - case .scanFailed(let dataBroker, let duration, let tries): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries)] - case .scanError(let dataBroker, let duration, let category, let details): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details] + case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isManualScan): + return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] + case .scanFailed(let dataBroker, let duration, let tries, let isManualScan): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] + case .scanError(let dataBroker, let duration, let category, let details, let isManualScan): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isManualScan: isManualScan.description] case .generateEmailHTTPErrorDaily(let statusCode, let environment, let wasOnWaitlist): return [Consts.environmentKey: environment, Consts.httpCode: String(statusCode), @@ -417,6 +435,14 @@ extension DataBrokerProtectionPixels: PixelKitEvent { return [Consts.environmentKey: environment, Consts.wasOnWaitlist: String(wasOnWaitlist), Consts.backendServiceCallSite: backendServiceCallSite.rawValue] + case .initialScanTotalDuration(let duration, let profileQueries): + return [Consts.durationInMs: String(duration), Consts.profileQueries: String(profileQueries)] + case .initialScanSiteLoadDuration(let duration, let hasError, let brokerURL): + return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL] + case .initialScanPostLoadingDuration(let duration, let hasError, let brokerURL): + return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL] + case .initialScanPreStartDuration(let duration): + return [Consts.durationInMs: String(duration)] } } } @@ -502,7 +528,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double func durationSinceStartTime() -> Double @@ -62,6 +63,7 @@ protocol StageDurationCalculator { } final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { + let isManualScan: Bool let handler: EventMapping let attemptId: UUID let dataBroker: String @@ -74,12 +76,14 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator init(attemptId: UUID = UUID(), startTime: Date = Date(), dataBroker: String, - handler: EventMapping) { + handler: EventMapping, + isManualScan: Bool = false) { self.attemptId = attemptId self.startTime = startTime self.lastStateTime = startTime self.dataBroker = dataBroker self.handler = handler + self.isManualScan = isManualScan } /// Returned in milliseconds @@ -159,11 +163,11 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator } func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1)) + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) } func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1)) + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) } func fireScanError(error: Error) { @@ -200,7 +204,8 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator dataBroker: dataBroker, duration: durationSinceStartTime(), category: errorCategory.toString, - details: error.localizedDescription + details: error.localizedDescription, + isManualScan: isManualScan ) ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift index aaac238d1c..41e02edaca 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift @@ -37,7 +37,7 @@ final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runAllOperations(showWebView: Bool) { } func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 7465df1771..f02d472666 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -57,6 +57,7 @@ final class DataBrokerProtectionProcessor { // MARK: - Public functions func startManualScans(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + operationQueue.cancelAllOperations() runOperations(operationType: .scan, priorityDate: nil, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index e7a15bd1bb..bd9e88641c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -80,7 +80,7 @@ public protocol DataBrokerProtectionScheduler { func stopScheduler() func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func startManualScan(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runAllOperations(showWebView: Bool) @@ -98,8 +98,8 @@ extension DataBrokerProtectionScheduler { runAllOperations(showWebView: false) } - public func startManualScan() { - startManualScan(showWebView: false, completion: nil) + public func startManualScan(startTime: Date) { + startManualScan(showWebView: false, startTime: startTime, completion: nil) } } @@ -250,7 +250,10 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } public func startManualScan(showWebView: Bool = false, + startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) + let backgroundAgentManualScanStartTime = Date() stopScheduler() userNotificationService.requestNotificationPermission() @@ -286,10 +289,22 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } } + fireManualScanCompletionPixel(startTime: backgroundAgentManualScanStartTime) completion?(errors) } } + private func fireManualScanCompletionPixel(startTime: Date) { + do { + let profileQueries = try dataManager.profileQueriesCount() + let durationSinceStart = Date().timeIntervalSince(startTime) * 1000 + self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), + profileQueries: profileQueries)) + } catch { + os_log("Manual Scan Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) + } + } + public func optOutAllBrokers(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { os_log("Opting out all brokers...", log: .dataBrokerProtection) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index fdfc8469bf..cdbb776170 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -27,6 +27,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let webViewHandler = WebViewHandlerMock() let emailService = EmailServiceMock() let captchaService = CaptchaServiceMock() + let pixelHandler = MockDataBrokerProtectionPixelsHandler() let stageCalulator = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: MockDataBrokerProtectionPixelsHandler()) override func tearDown() async throws { @@ -47,6 +48,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -71,6 +73,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -102,6 +105,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -131,6 +135,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -153,6 +158,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) emailService.shouldThrow = true @@ -181,6 +187,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, clickAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -200,6 +207,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -221,6 +229,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -243,6 +252,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) let actionsHandler = ActionsHandler(step: step) @@ -272,6 +282,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -294,6 +305,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.retriesCountOnError = 0 @@ -317,6 +329,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -335,6 +348,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -355,6 +369,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -374,6 +389,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -393,6 +409,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -412,6 +429,7 @@ final class DataBrokerOperationActionTests: XCTestCase { captchaService: captchaService, operationAwaitTime: 0, stageCalculator: mockStageCalculator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -431,6 +449,7 @@ final class DataBrokerOperationActionTests: XCTestCase { cookieHandler: mockCookieHandler, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) @@ -452,6 +471,7 @@ final class DataBrokerOperationActionTests: XCTestCase { cookieHandler: mockCookieHandler, operationAwaitTime: 0, stageCalculator: stageCalulator, + pixelHandler: pixelHandler, shouldRunNextStep: { true } ) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 70619e8736..19d92e74d6 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -20,6 +20,8 @@ import XCTest import BrowserServicesKit +import Common +import PixelKit @testable import DataBrokerProtection final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { @@ -931,14 +933,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { } final class MockWebOperationRunner: WebOperationRunner { - var shouldScanThrow = false var shouldOptOutThrow = false var scanResults = [ExtractedProfile]() var wasScanCalled = false var wasOptOutCalled = false - func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { + func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { wasScanCalled = true if shouldScanThrow { @@ -948,7 +949,7 @@ final class MockWebOperationRunner: WebOperationRunner { } } - func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { + func optOut(profileQuery: BrokerProfileQueryData, extractedProfile: ExtractedProfile, stageCalculator: StageDurationCalculator, pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { wasOptOutCalled = true if shouldOptOutThrow { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift index 7ba868976f..c7aeee1db7 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift @@ -39,7 +39,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanFailed(let broker, _, _): + case .scanFailed(let broker, _, _, _): XCTAssertEqual(broker, "broker") default: XCTFail("The scan failed pixel should be fired") } @@ -57,7 +57,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.clientError(httpCode: 403).toString) default: XCTFail("The scan error pixel should be fired") } @@ -75,7 +75,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.serverError(httpCode: 500).toString) default: XCTFail("The scan error pixel should be fired") } @@ -93,7 +93,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.validationError.toString) default: XCTFail("The scan error pixel should be fired") } @@ -112,7 +112,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.networkError.toString) default: XCTFail("The scan error pixel should be fired") } @@ -131,7 +131,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, "database-error-SecureVaultError-13") default: XCTFail("The scan error pixel should be fired") } @@ -150,7 +150,7 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ switch failurePixel { - case .scanError(_, _, let category, _): + case .scanError(_, _, let category, _, _): XCTAssertEqual(category, ErrorCategory.unclassified.toString) default: XCTFail("The scan error pixel should be fired") } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 4cdc54e06e..9b60fe4812 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -868,6 +868,7 @@ final class MockAppVersion: AppVersionNumberProvider { } final class MockStageDurationCalculator: StageDurationCalculator { + var isManualScan: Bool = false var attemptId: UUID = UUID() var stage: Stage? From 38f8cc835de32ba09725c28931ca84d7f7790f86 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 29 Apr 2024 17:21:07 +0000 Subject: [PATCH 049/134] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 89 +++++++++++++++++-- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index e4add5a17e..8650176744 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"7cf7b71adb62c3cbcbf8b84c61a0004d\"" - public static let embeddedDataSHA = "20e9b59e7e60ccc9ae52853935ebe3d74227234fcf8b46da5a66cff3adc7e6c7" + public static let embeddedDataETag = "\"0cbf39995c17cfe7455c0ceff1b5d8cc\"" + public static let embeddedDataSHA = "4e5495e24a27eba09553c335fc566bc694111e957faac6512093b76d3f585f71" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 39163097c1..361f28807b 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1713542334045, + "version": 1714411110624, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -135,6 +135,9 @@ { "domain": "ksta.de" }, + { + "domain": "larazon.es" + }, { "domain": "motherdenim.com" }, @@ -288,6 +291,9 @@ { "domain": "sporthoj.com" }, + { + "domain": "www.michelinman.com" + }, { "domain": "marvel.com" }, @@ -299,12 +305,11 @@ "disabledCMPs": [ "generic-cosmetic", "termsfeed3", - "healthline-media", "tarteaucitron.js" ] }, "state": "enabled", - "hash": "f35e24cf85485b441cb9a76146e77e17" + "hash": "bca76e26434e161140776de92c03cb5f" }, "autofill": { "exceptions": [ @@ -1092,6 +1097,10 @@ { "domain": "web.de", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1931" + }, + { + "domain": "id.seb.se", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/2025" } ], "webViewDefault": [ @@ -1119,7 +1128,7 @@ }, "exceptions": [], "state": "enabled", - "hash": "76976e1ac417949aae8cb1c7c7ca0a60" + "hash": "c52448ca8da413a93de9822cac039920" }, "dbp": { "state": "enabled", @@ -4256,11 +4265,20 @@ "type": "override" } ] + }, + { + "domain": "wideopencountry.com", + "rules": [ + { + "selector": ".entry-ad", + "type": "hide-empty" + } + ] } ] }, "state": "enabled", - "hash": "765e789c939c6e3307f576bc698fbb9e" + "hash": "cebfdc8b9e9f34d7e407f430850e7d70" }, "exceptionHandler": { "exceptions": [ @@ -4274,6 +4292,11 @@ "state": "disabled", "hash": "dc1b4fa301193a03ddcd4bdf7ee3e610" }, + "extendedOnboarding": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "fingerprintingAudio": { "state": "disabled", "exceptions": [ @@ -5981,6 +6004,12 @@ "sbs.com.au" ] }, + { + "rule": "https://googleads.g.doubleclick.net/ads/preferences/naioptout", + "domains": [ + "zojirushi.com" + ] + }, { "rule": "www3.doubleclick.net", "domains": [ @@ -6683,6 +6712,16 @@ } ] }, + "grow.me": { + "rules": [ + { + "rule": "grow.me/main.js", + "domains": [ + "budgetbytes.com" + ] + } + ] + }, "gstatic.com": { "rules": [ { @@ -6909,6 +6948,7 @@ { "rule": "a.klaviyo.com/media/js/onsite/onsite.js", "domains": [ + "bonescoffee.com", "tanglefree.com" ] }, @@ -7121,7 +7161,8 @@ { "rule": "connect.nosto.com/script/shopify/nosto.js", "domains": [ - "oneill.com" + "oneill.com", + "thefryecompany.com" ] } ] @@ -7217,6 +7258,7 @@ "abc.net.au", "emol.com", "oufc.co.uk", + "the-afc.com", "theposh.com" ] } @@ -7235,6 +7277,12 @@ "domains": [ "hgtv.com" ] + }, + { + "rule": "https://cdn.optimizely.com/js/271989291.js", + "domains": [ + "my.zipcar.com" + ] } ] }, @@ -7319,10 +7367,23 @@ "primis.tech": { "rules": [ { - "rule": "live.primis.tech/live/liveView.php", + "rule": "video.primis.tech/", + "domains": [ + "wideopencountry.com" + ] + }, + { + "rule": "live.primis.tech/content/omid/static/", + "domains": [ + "wideopencountry.com" + ] + }, + { + "rule": "live.primis.tech/live/", "domains": [ "belfastlive.co.uk", - "cornwalllive.com" + "cornwalllive.com", + "wideopencountry.com" ] } ] @@ -7698,6 +7759,16 @@ } ] }, + "skimresources.com": { + "rules": [ + { + "rule": "go.skimresources.com/", + "domains": [ + "www.lotustalk.com" + ] + } + ] + }, "slickstream.com": { "rules": [ { @@ -8220,7 +8291,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "2d5ce26ddae089bcb61e4f4a0b1ae487" + "hash": "e21e28c6597b4ac6ab6c03bdf359912e" }, "trackingCookies1p": { "settings": { From 905896530b84565f367df7e84a01b1744897e14c Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 29 Apr 2024 17:21:07 +0000 Subject: [PATCH 050/134] Set marketing version to 1.86.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index b517e1e1fb..c23c40bdc0 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.85.0 +MARKETING_VERSION = 1.86.0 From a951d2ff727fba02798605f013f519b8b1197a2d Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 29 Apr 2024 17:31:51 +0000 Subject: [PATCH 051/134] Bump version to 1.86.0 (177) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index fe80d801b7..d0563940bd 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 176 +CURRENT_PROJECT_VERSION = 177 From e0f5c47446f56b6bb13cca3b1909d180681cc418 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 30 Apr 2024 11:23:59 +0200 Subject: [PATCH 052/134] Egress Server Failure Detection & Recovery (#2657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1206669340827392/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/786 **Description**: - Use the WireGuard handshakes to determine when a connection fails by checking that the last handshake time is less than 300 seconds (just over two handshakes). - When a failure is detected the client will call /register with server set to the name of the current (failed) server and mode set to failureRecovery. If the server is not in the "active" status on the controller a new registration will be returned otherwise the previous data will be returned (this data should be checked against the current tunnel data including allowedIPs). **Steps to test this PR**: 1. Launch the VPN and connect 2. Block UDP traffic by following these instructions: https://app.asana.com/0/0/1207029295107667/f 3. Open the Console.app and filter by Category: `Network Protection`. The failure recovery logs are indicated with a 🟢 so you can also look for them. 4. After around 5 minutes you will see logs indicating that a register request has been made. 5. It will error with no recovery required as there is not a real problem. --- ###### 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 | 16 ++++- .../xcshareddata/swiftpm/Package.resolved | 8 +-- .../MacPacketTunnelProvider.swift | 30 +++++++++ .../Pixels/VPNFailureRecoveryPixel.swift | 67 +++++++++++++++++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 7 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index df5499bc80..975b9f1315 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2510,6 +2510,8 @@ EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; + EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */; }; + EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */; }; EEBCE6822BA444FA00B9DF00 /* XCUIElementExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBCE6812BA444FA00B9DF00 /* XCUIElementExtension.swift */; }; EEBCE6832BA463DD00B9DF00 /* NSImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */; }; EEBCE6842BA4643200B9DF00 /* NSSizeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */; }; @@ -4006,6 +4008,7 @@ EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLauncher.swift; sourceTree = ""; }; + EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNFailureRecoveryPixel.swift; sourceTree = ""; }; EEBCE6812BA444FA00B9DF00 /* XCUIElementExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCUIElementExtension.swift; sourceTree = ""; }; EEC111E3294D06020086524F /* JSAlert.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = JSAlert.storyboard; sourceTree = ""; }; EEC111E5294D06290086524F /* JSAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModel.swift; sourceTree = ""; }; @@ -5123,6 +5126,7 @@ 4B4D607D2A0B29FA00BCD287 /* NetworkExtensionTargets */ = { isa = PBXGroup; children = ( + EEBCA0C12BD7CDDA004DF19C /* Pixels */, 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, @@ -8073,6 +8077,14 @@ path = JSAlert; sourceTree = ""; }; + EEBCA0C12BD7CDDA004DF19C /* Pixels */ = { + isa = PBXGroup; + children = ( + EEBCA0C52BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift */, + ); + path = Pixels; + sourceTree = ""; + }; EEBCE6802BA444FA00B9DF00 /* Common */ = { isa = PBXGroup; children = ( @@ -10490,6 +10502,7 @@ 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, + EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, @@ -10609,6 +10622,7 @@ 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, + EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, @@ -12724,7 +12738,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.1.2; + version = 142.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 64aca2b97a..72e9ff06b4 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" : "f8c73292d4d6724ec81f98bd29dbb2061f1a8cf6", - "version" : "141.1.2" + "revision" : "2681b5271a4e0582f175771737617adb8a4d6e78", + "version" : "142.0.0" } }, { @@ -120,7 +120,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "46989693916f56d1186bd59ac15124caef896560", "version" : "1.3.1" @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 3c71dc41a7..5623af1f0c 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -287,6 +287,33 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { frequency: .dailyAndCount, includeAppVersionParameter: true) } + case .failureRecoveryAttempt(let step): + switch step { + case .started: + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryStarted, + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + case .completed(.healthy): + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryCompletedHealthy, + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + case .completed(.unhealthy): + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryCompletedUnhealthy, + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + case .failed(let error): + PixelKit.fire( + VPNFailureRecoveryPixel.vpnFailureRecoveryFailed(error), + frequency: .dailyAndCount, + includeAppVersionParameter: true + ) + } } } @@ -300,6 +327,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization + @MainActor @objc public init() { let isSubscriptionEnabled = false @@ -360,6 +388,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { /// Observe server changes to broadcast those changes through distributed notifications. /// + @MainActor private func observeServerChanges() { lastSelectedServerInfoPublisher.sink { [weak self] server in self?.lastStatusChangeDate = Date() @@ -404,6 +433,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { /// Broadcasts the current server information. /// + @MainActor private func broadcastLastSelectedServerInfo() { broadcast(lastSelectedServerInfo) } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift new file mode 100644 index 0000000000..3fdae85a54 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/Pixels/VPNFailureRecoveryPixel.swift @@ -0,0 +1,67 @@ +// +// VPNFailureRecoveryPixel.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 + +/// PrivacyPro pixels. +/// +/// Ref: https://app.asana.com/0/0/1206939413299475/f +/// +public enum VPNFailureRecoveryPixel: PixelKitEventV2 { + + /// This pixel is emitted when the last handshake diff is greater than n minutes and an attempt to recover is made (/register is called with failureRecovery) + /// + case vpnFailureRecoveryStarted + + /// This pixel is emitted when the recovery attempt failed due to any reason. + /// + case vpnFailureRecoveryFailed(Error) + + /// This pixel is emitted when the recovery attempt completed and the server was healthy and no further action needs to be taken. + /// + case vpnFailureRecoveryCompletedHealthy + + /// This pixel is emitted when the recovery attempt completed and the server was unhealthy resulting to reconnecting to a different server. + /// + case vpnFailureRecoveryCompletedUnhealthy + + public var name: String { + switch self { + case .vpnFailureRecoveryStarted: + return "m_mac_netp_ev_failure_recovery_started" + case .vpnFailureRecoveryFailed: + return "m_mac_netp_ev_failure_recovery_failed" + case .vpnFailureRecoveryCompletedHealthy: + return "m_mac_netp_ev_failure_recovery_completed_server_healthy" + case .vpnFailureRecoveryCompletedUnhealthy: + return "m_mac_netp_ev_failure_recovery_completed_server_unhealthy" + } + } + + public var error: Error? { + switch self { + case .vpnFailureRecoveryStarted, .vpnFailureRecoveryCompletedHealthy, .vpnFailureRecoveryCompletedUnhealthy: return nil + case .vpnFailureRecoveryFailed(let error): return error + } + } + + public var parameters: [String: String]? { + nil + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 7657755407..e541454331 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: "141.1.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "142.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 45900c0243..bc9b47340f 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "142.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 7c57479333..68611f927c 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: "141.1.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "142.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 632cb2e386d27a7693f3e81a5fbafd0414287a04 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 30 Apr 2024 19:58:14 +0600 Subject: [PATCH 053/134] Fix Tab not closed for redrected download (#2715) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207179864616081/f --- .../TabExtensions/DownloadsTabExtension.swift | 5 +- .../Common/TestsURLExtension.swift | 2 +- .../Downloads/DownloadsIntegrationTests.swift | 127 ++++++++++++++++++ IntegrationTests/Tab/AddressBarTests.swift | 35 ++++- IntegrationTests/Tab/ErrorPageTests.swift | 15 ++- IntegrationTests/Tab/TabContentTests.swift | 1 - 6 files changed, 177 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 1aae34f4c6..339aca0a9a 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -224,11 +224,12 @@ extension DownloadsTabExtension: NavigationResponder { let task = downloadManager.add(download, fromBurnerWindow: self.isBurner, delegate: self, destination: .auto) var isMainFrameNavigationActionWithNoHistory: Bool { - guard let navigationAction, + // get the first navigation action in the redirect series + guard let navigationAction = navigationAction?.redirectHistory?.first ?? navigationAction, navigationAction.isForMainFrame, navigationAction.isTargetingNewWindow, // webView has no navigation history (downloaded navigationAction has started from an empty state) - (navigationAction.redirectHistory?.first ?? navigationAction).fromHistoryItemIdentity == nil + navigationAction.fromHistoryItemIdentity == nil else { return false } return true } diff --git a/IntegrationTests/Common/TestsURLExtension.swift b/IntegrationTests/Common/TestsURLExtension.swift index 95da2cd8ca..b1bd1f7a8f 100644 --- a/IntegrationTests/Common/TestsURLExtension.swift +++ b/IntegrationTests/Common/TestsURLExtension.swift @@ -37,7 +37,7 @@ extension URL { let url = URL.testsServer .appendingPathComponent("filename") // "http://localhost:8085/filename" .appendingTestParameters(status: 301, - reason: "Moved" + reason: "Moved", data: Data(), headers: ["Location": "/redirect-location.html"]) Tab.setUrl(url) diff --git a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift index fbe3f5f557..f74e6e42e5 100644 --- a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift +++ b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift @@ -185,6 +185,133 @@ class DownloadsIntegrationTests: XCTestCase { } } + @MainActor + func testWhenDownloadIsStartedInNewTab_tabIsClosed() async throws { + let preferences = DownloadsPreferences.shared + preferences.alwaysRequestDownloadLocation = false + preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory + let dirURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) + + let downloadUrl = URL.testsServer + .appendingPathComponent("fname.dat") + .appendingTestParameters(data: data.html, + headers: ["Content-Disposition": "attachment; filename=\"fname.dat\"", + "Content-Type": "text/html"]) + + let pageUrl = URL.testsServer + .appendingTestParameters(data: """ + + + + + Clickable Body + + +

      Click anywhere on the page to open the link

      + + + """.utf8data) + let tab = tabViewModel.tab + _=await tab.setUrl(pageUrl, source: .link)?.result + + NSApp.activate(ignoringOtherApps: true) + let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise() + + let e1 = expectation(description: "new tab opened") + var e2: XCTestExpectation! + let c = tabCollectionViewModel.$selectedTabViewModel.dropFirst() + .receive(on: DispatchQueue.main) + .sink { [unowned self] tabViewModel in + guard let tabViewModel else { return } + print("tabViewModel", tabViewModel.tab, tab) + if tabViewModel.tab !== tab { + e1.fulfill() + e2 = expectation(description: "new tab closed") + } else { + e2.fulfill() + } + } + + // click to open a new (download) tab and instantly deactivate it + click(tab.webView) + + // download should start in the background tab + _=try await downloadTaskFuture.get() + + // expect for the download tab to close + await fulfillment(of: [e1, e2], timeout: 10) + withExtendedLifetime(c, {}) + } + + @MainActor + func testWhenDownloadIsStartedInNewTabAfterRedirect_tabIsClosed() async throws { + let preferences = DownloadsPreferences.shared + preferences.alwaysRequestDownloadLocation = false + preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory + let dirURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true) + + let downloadUrl = URL.testsServer + .appendingPathComponent("fname.dat") + .appendingTestParameters(data: data.html, + headers: ["Content-Disposition": "attachment; filename=\"fname.dat\"", + "Content-Type": "text/html"]) + + let redirectUrl = URL.testsServer + .appendingTestParameters(data: """ + + + + """.utf8data) + + let pageUrl = URL.testsServer + .appendingTestParameters(data: """ + + + + + Clickable Body + + +

      Click anywhere on the page to open the link

      + + + """.utf8data) + let tab = tabViewModel.tab + _=await tab.setUrl(pageUrl, source: .link)?.result + + NSApp.activate(ignoringOtherApps: true) + let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise() + + let e1 = expectation(description: "new tab opened") + var e2: XCTestExpectation! + let c = tabCollectionViewModel.$selectedTabViewModel.dropFirst() + .receive(on: DispatchQueue.main) + .sink { [unowned self] tabViewModel in + guard let tabViewModel else { return } + print("tabViewModel", tabViewModel.tab, tab) + if tabViewModel.tab !== tab { + e1.fulfill() + e2 = expectation(description: "new tab closed") + } else { + e2.fulfill() + } + } + + // click to open a new (download) tab and instantly deactivate it + click(tab.webView) + + // download should start in the background tab + _=try await downloadTaskFuture.get() + + // expect for the download tab to close + await fulfillment(of: [e1, e2], timeout: 10) + withExtendedLifetime(c, {}) + } + @MainActor func testWhenSaveDialogOpenInBackgroundTabAndTabIsClosed_downloadIsCancelled() async throws { let persistor = DownloadsPreferencesUserDefaultsPersistor() diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index e5297ed96b..9323afb45c 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -23,7 +23,6 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser @available(macOS 12.0, *) -@MainActor class AddressBarTests: XCTestCase { var window: MainWindow! @@ -141,6 +140,7 @@ class AddressBarTests: XCTestCase { // MARK: - Tests + @MainActor func testWhenUserStartsTypingOnNewTabPageLoad_userInputIsNotReset() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, privacyFeatures: privacyFeaturesMock) @@ -190,6 +190,7 @@ class AddressBarTests: XCTestCase { } + @MainActor func testWhenSwitchingBetweenTabs_addressBarFocusStateIsCorrect() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [ Tab(content: .newtab, privacyFeatures: privacyFeaturesMock), @@ -220,24 +221,28 @@ class AddressBarTests: XCTestCase { } } + @MainActor func testWhenRestoringToOnboarding_addressBarIsNotActive() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .onboarding, privacyFeatures: privacyFeaturesMock)])) window = WindowsManager.openNewWindow(with: viewModel)! XCTAssertEqual(window.firstResponder, mainViewController.browserTabViewController.transientTabContentViewController!.view) } + @MainActor func testWhenRestoringToSettings_addressBarIsNotActive() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .settings(pane: .appearance), privacyFeatures: privacyFeaturesMock)])) window = WindowsManager.openNewWindow(with: viewModel)! XCTAssertEqual(window.firstResponder, mainViewController.browserTabViewController.preferencesViewController!.view) } + @MainActor func testWhenRestoringToBookmarks_addressBarIsNotActive() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .bookmarks, privacyFeatures: privacyFeaturesMock)])) window = WindowsManager.openNewWindow(with: viewModel)! XCTAssertEqual(window.firstResponder, mainViewController.browserTabViewController.bookmarksViewController!.view) } + @MainActor func testWhenRestoringToURL_addressBarIsNotActive() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .loadedByStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -245,12 +250,14 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenRestoringToNewTab_addressBarIsActive() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .newtab, privacyFeatures: privacyFeaturesMock)])) window = WindowsManager.openNewWindow(with: viewModel)! XCTAssertTrue(isAddressBarFirstResponder) } + @MainActor func testWhenOpeningNewTab_addressBarIsActivated() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .loadedByStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -274,6 +281,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenSwitchingBetweenTabsWithTypedValue_typedValueIsPreserved() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [ Tab(content: .newtab, privacyFeatures: privacyFeaturesMock), @@ -312,6 +320,7 @@ class AddressBarTests: XCTestCase { } } + @MainActor func testWhenSwitchingBetweenURLTabs_addressBarIsDeactivated() async throws { let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [ Tab(content: .url(.duckDuckGo, credential: nil, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock), @@ -332,6 +341,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, viewModel.tabs[0].webView) } + @MainActor func testWhenDeactivatingAddressBar_webViewShouldBecomeFirstResponder() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -346,6 +356,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenGoingBack_addressBarIsDeactivated() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .loadedByStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -372,6 +383,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenGoingBackToNewtabPage_addressBarIsActivated() async throws { let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -409,6 +421,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenGoingBackToNewtabPageFromSettings_addressBarIsActivated() async throws { let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -434,6 +447,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, mainViewController.browserTabViewController.preferencesViewController!.view) } + @MainActor func testWhenGoingBackToNewtabPageFromBookmarks_addressBarIsActivated() async throws { let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -459,6 +473,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, mainViewController.browserTabViewController.bookmarksViewController!.view) } + @MainActor func testWhenTabReloaded_addressBarIsDeactivated() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .webViewUpdated), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -476,6 +491,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenReloadingFailingPage_addressBarIsDeactivated() async throws { // first navigation should fail schemeHandler.middleware = [{ _ in @@ -499,6 +515,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenTabReloadedBySubmittingSameAddressAndAddressIsActivated_addressBarIsKeptActiveOnPageLoad() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -520,6 +537,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "some-text") } + @MainActor func testWhenEditingSerpURL_serpIconIsDisplayed() async throws { let tab = Tab(content: .url(.makeSearchUrl(from: "catz")!, credential: nil, source: .userEntered("catz")), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -527,10 +545,9 @@ class AddressBarTests: XCTestCase { _=try await tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise().value _=window.makeFirstResponder(addressBarTextField) - -// try await Task.sleep(interval: 60.01) } + @MainActor func testWhenOpeningBookmark_addressBarIsDeactivated() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .webViewUpdated), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -545,6 +562,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenOpeningHistoryEntry_addressBarIsDeactivated() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .webViewUpdated), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -559,6 +577,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenOpeningURLfromUI_addressBarIsDeactivated() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .webViewUpdated), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -573,6 +592,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenHomePageIsOpened_addressBarIsDeactivated() async throws { StartupPreferences.shared.launchToCustomHomePage = true @@ -592,6 +612,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenAddressSubmitted_addressBarIsDeactivated() async throws { let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -605,6 +626,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window.firstResponder, tab.webView) } + @MainActor func testWhenAddressSubmittedAndAddressBarIsReactivated_addressBarIsKeptActiveOnPageLoad() async throws { let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) @@ -624,6 +646,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "some-text") } + @MainActor func testWhenPageRedirected_addressBarStaysActivePreservingUserInput() async throws { let expectation = expectation(description: "request sent") schemeHandler.middleware = [{ request in @@ -669,6 +692,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "replacement-url") } + @MainActor func testWhenPageRedirectedWhenAddressBarIsInactive_addressBarShouldReset() async throws { AppearancePreferences.shared.showFullURL = true @@ -717,6 +741,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "https://redirected.com/") } + @MainActor func testWhenActivatingWindowWithPinnedTabOpen_webViewBecomesFirstResponder() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) WindowControllersManager.shared.pinnedTabsManager.setUp(with: TabCollection(tabs: [tab])) @@ -748,6 +773,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window2.firstResponder, window2) } + @MainActor func testWhenActivatingWindowWithPinnedTabWhenAddressBarIsActive_addressBarIsKeptActive() async throws { let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) WindowControllersManager.shared.pinnedTabsManager.setUp(with: TabCollection(tabs: [tab])) @@ -780,6 +806,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window2.firstResponder, window2) } + @MainActor func test_WhenSiteCertificateNil_ThenAddressBarShowsStandardShieldIcon() async throws { // GIVEN let expectedImage = NSImage(named: "Shield")! @@ -797,6 +824,7 @@ class AddressBarTests: XCTestCase { XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) } + @MainActor func test_WhenSiteCertificateValid_ThenAddressBarShowsStandardShieldIcon() async throws { // GIVEN let expectedImage = NSImage(named: "Shield")! @@ -815,6 +843,7 @@ class AddressBarTests: XCTestCase { XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) } + @MainActor func test_WhenSiteCertificateInvalid_ThenAddressBarShowsDottedShieldIcon() async throws { // GIVEN let expectedImage = NSImage(named: "ShieldDot")! diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift index b4dc46832c..a6dab00d9d 100644 --- a/IntegrationTests/Tab/ErrorPageTests.swift +++ b/IntegrationTests/Tab/ErrorPageTests.swift @@ -23,7 +23,6 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser @available(macOS 12.0, *) -@MainActor class ErrorPageTests: XCTestCase { var window: NSWindow! @@ -146,6 +145,7 @@ class ErrorPageTests: XCTestCase { // MARK: - Tests + @MainActor func testWhenPageFailsToLoad_errorPageShown() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -186,6 +186,7 @@ class ErrorPageTests: XCTestCase { XCTAssertEqual(tab.content.userEditableUrl, .test) } + @MainActor func testWhenTabWithNoConnectionErrorActivated_reloadTriggered() async throws { // open 2 Tabs with newtab page let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -231,6 +232,7 @@ class ErrorPageTests: XCTestCase { XCTAssertNil(tab1.error) } + @MainActor func testWhenTabWithConnectionLostErrorActivatedAndReloadFailsAgain_errorPageIsShownOnce() async throws { // open 2 Tabs with newtab page // navigate to a failing url right away @@ -284,6 +286,7 @@ class ErrorPageTests: XCTestCase { withExtendedLifetime(c) {} } + @MainActor func testWhenTabWithOtherErrorActivated_reloadNotTriggered() async throws { // open 2 Tabs with newtab page // navigate to a failing url right away @@ -314,6 +317,7 @@ class ErrorPageTests: XCTestCase { withExtendedLifetime(c) {} } + @MainActor func testWhenGoingBackToFailingPage_reloadIsTriggered() async throws { // open Tab with newtab page // navigate to a failing url right away @@ -366,6 +370,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenGoingBackToFailingPageAndItFailsAgain_errorPageIsUpdated() async throws { // open Tab with newtab page // navigate to a failing url right away @@ -424,6 +429,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenPageLoadedAndFailsOnRefreshAndOnConsequentRefresh_errorPageIsUpdatedKeepingForwardHistory() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -500,6 +506,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenPageLoadedAndFailsOnRefreshAndSucceedsOnConsequentRefresh_forwardHistoryIsPreserved() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -568,6 +575,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenReloadingBySubmittingSameURL_errorPageRemainsSame() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -643,6 +651,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenGoingToAnotherUrlFails_newBackForwardHistoryItemIsAdded() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -718,6 +727,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenGoingToAnotherUrlSucceeds_newBackForwardHistoryItemIsAdded() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) @@ -787,6 +797,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testWhenLoadingFailsAfterSessionRestoration_navigationHistoryIsPreserved() async throws { schemeHandler.middleware = [{ _ in .failure(NSError.noConnection) @@ -827,6 +838,7 @@ class ErrorPageTests: XCTestCase { XCTAssertTrue(tab.canReload) } + @MainActor func testPinnedTabDoesNotNavigateAway() async throws { schemeHandler.middleware = [{ _ in return .ok(.html(Self.testHtml)) @@ -874,6 +886,7 @@ class ErrorPageTests: XCTestCase { XCTAssertEqual(viewModel.tabs.count, 1) } + @MainActor func testWhenPageFailsToLoadAfterRedirect_errorPageShown() async throws { // open Tab with newtab page let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock) diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift index 16434f7a3b..bf851f09c8 100644 --- a/IntegrationTests/Tab/TabContentTests.swift +++ b/IntegrationTests/Tab/TabContentTests.swift @@ -24,7 +24,6 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser @available(macOS 12.0, *) -@MainActor class TabContentTests: XCTestCase { var window: NSWindow! From 8354f7910f60363c029084d9747896e115e4ba1c Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 30 Apr 2024 21:17:56 +0100 Subject: [PATCH 054/134] Prevent queued operations from cancelling manual scans (#2719) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207169472324664/f Tech Design URL: CC: **Description**: Prevent multiple scans from running at the same time --- .../DataBrokerProtectionScheduler.swift | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index bd9e88641c..4e7d9a9846 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -112,6 +112,14 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch static let tolerance: TimeInterval = 20 * 60 // 20 minutes } + private enum DataBrokerProtectionCurrentOperation { + case idle + case queued + case manualScan + case optOutAll + case all + } + private let privacyConfigManager: PrivacyConfigurationManaging private let contentScopeProperties: ContentScopeProperties private let dataManager: DataBrokerProtectionDataManager @@ -122,6 +130,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch private let emailService: EmailServiceProtocol private let captchaService: CaptchaServiceProtocol private let userNotificationService: DataBrokerProtectionUserNotificationService + private var currentOperation: DataBrokerProtectionCurrentOperation = .idle /// Ensures that only one scheduler operation is executed at the same time. /// @@ -186,9 +195,16 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch completion(.finished) return } + + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + completion(.finished) + return + } self.lastSchedulerSessionStartTimestamp = Date() self.status = .running os_log("Scheduler running...", log: .dataBrokerProtection) + self.currentOperation = .queued self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { @@ -201,6 +217,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } } self?.status = .idle + self?.currentOperation = .idle completion(.finished) } } @@ -214,7 +231,13 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } public func runAllOperations(showWebView: Bool = false) { + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + return + } + os_log("Running all operations...", log: .dataBrokerProtection) + self.currentOperation = .all self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { @@ -226,12 +249,19 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } + self?.currentOperation = .idle } } public func runQueuedOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + return + } + os_log("Running queued operations...", log: .dataBrokerProtection) + self.currentOperation = .queued dataBrokerProcessor.runQueuedOperations(showWebView: showWebView, completion: { [weak self] errors in if let errors = errors { @@ -245,6 +275,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } } completion?(errors) + self?.currentOperation = .idle }) } @@ -257,7 +288,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch stopScheduler() userNotificationService.requestNotificationPermission() - + self.currentOperation = .manualScan os_log("Scanning all brokers...", log: .dataBrokerProtection) dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in guard let self = self else { return } @@ -288,7 +319,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } - + self.currentOperation = .idle fireManualScanCompletionPixel(startTime: backgroundAgentManualScanStartTime) completion?(errors) } @@ -307,7 +338,14 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public func optOutAllBrokers(showWebView: Bool = false, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + + guard self.currentOperation != .manualScan else { + os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) + return + } + os_log("Opting out all brokers...", log: .dataBrokerProtection) + self.currentOperation = .optOutAll self.dataBrokerProcessor.runAllOptOutOperations(showWebView: showWebView, completion: { [weak self] errors in if let errors = errors { @@ -320,7 +358,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } - + self?.currentOperation = .idle completion?(errors) }) } From 04a7f6eb0ba4acafd8c9326a4b06967a5431ab27 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 1 May 2024 11:28:43 +1000 Subject: [PATCH 055/134] Alessandro/create subtask on pr requested (#2692) Task/Issue URL: https://app.asana.com/0/0/1207193414517319/f **Description**: This PR automates the creation of the Asana PR ticket when a GitHub reviewer is added to the PR. --- .../asana-create-action-item/action.yml | 2 +- .github/actions/asana-log-message/action.yml | 2 +- .github/workflows/code_freeze.yml | 2 +- .github/workflows/pr_task_url.yml | 20 ++++++++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml index 7f4ee3f3c2..b83f2dd1ad 100644 --- a/.github/actions/asana-create-action-item/action.yml +++ b/.github/actions/asana-create-action-item/action.yml @@ -45,7 +45,7 @@ runs: - id: get-asana-user-id if: github.event_name != 'schedule' - uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ inputs.access-token }} github-handle: ${{ github.actor }} diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index 288fd832ba..e00cbd2a6d 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -31,7 +31,7 @@ runs: - id: get-asana-user-id if: github.event_name != 'schedule' - uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ inputs.access-token }} github-handle: ${{ github.actor }} diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 755282af5e..5b7bc475b5 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -47,7 +47,7 @@ jobs: - name: Get Asana user ID id: get-asana-user-id - uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main + uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main with: access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} github-handle: ${{ github.actor }} diff --git a/.github/workflows/pr_task_url.yml b/.github/workflows/pr_task_url.yml index 820b61ba14..b5e247e02d 100644 --- a/.github/workflows/pr_task_url.yml +++ b/.github/workflows/pr_task_url.yml @@ -2,7 +2,7 @@ name: Asana PR Task URL on: pull_request: - types: [opened, edited, closed, unlabeled, synchronize] + types: [opened, edited, closed, unlabeled, synchronize, review_requested] jobs: @@ -112,6 +112,24 @@ jobs: if: ${{ needs.assert-project-membership.outputs.task_id }} run: exit ${{ needs.assert-project-membership.outputs.failure }} + # When reviewer is assigned create a subtask in Asana if not existing already + create-asana-pr-subtask-if-needed: + + name: "Create the PR subtask in Asana" + + runs-on: ubuntu-latest + if: github.event.action == 'review_requested' + + needs: [assert-project-membership] + + steps: + - name: Create or Update PR Subtask + uses: duckduckgo/apple-toolbox/actions/asana-create-pr-subtask@main + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + asana-task-id: ${{ needs.assert-project-membership.outputs.task_id }} + github-reviewer-user: ${{ github.event.requested_reviewer.login }} + # When a PR is merged, move the task to the Waiting for Release section of the App Board. mark-waiting-for-release: From 40fec1f0dbc6f5393f65219bd9234c0835f5a84c Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 1 May 2024 05:12:57 +0000 Subject: [PATCH 056/134] Bump version to 1.86.0 (178) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index d0563940bd..3b1dd4f073 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 177 +CURRENT_PROJECT_VERSION = 178 From 2dd71d8087ee2696c00147e2d268d632a17caf6d Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Wed, 1 May 2024 08:34:07 +0100 Subject: [PATCH 057/134] macOS: Set Error Reporter on All SecureVault Initializations (#2717) Task/Issue URL: https://app.asana.com/0/1203581873609357/1207182308262542/f **Description**: Passes the static `DataBrokerProtectionSecureVaultErrorReporter.shared` at all points where we initialize the DBP Secure Vault using `makeVault` --- .../Database/DataBrokerProtectionDatabase.swift | 4 ++-- .../Operations/DataBrokerProtectionBrokerUpdater.swift | 3 ++- .../Scheduler/DataBrokerProtectionProcessor.swift | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index bab6bce44f..c1cbafd4a8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -323,7 +323,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let scan = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { return [HistoryEvent]() } @@ -338,7 +338,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> [HistoryEvent] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let optOut = try vault.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) else { return [HistoryEvent]() } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 9128f87178..cc0df841f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -18,6 +18,7 @@ import Foundation import Common +import SecureStorage protocol ResourcesRepository { func fetchBrokerFromResourceFiles() throws -> [DataBroker]? @@ -118,7 +119,7 @@ public struct DataBrokerProtectionBrokerUpdater { } public static func provide() -> DataBrokerProtectionBrokerUpdater? { - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { return DataBrokerProtectionBrokerUpdater(vault: vault) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index f02d472666..74b381eb3e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -115,7 +115,7 @@ final class DataBrokerProtectionProcessor { completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { // Before running new operations we check if there is any updates to the broker files. - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) brokerUpdater.checkForUpdatesInBrokerJSONFiles() } From ba5345ae2326ea1a5094c460f5017e5c38d86b0c Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 1 May 2024 11:49:58 +0200 Subject: [PATCH 058/134] Tentative fix for tab crashes (#2727) Task/Issue URL: https://app.asana.com/0/0/1207210775441763/f ## Description Tentative fix for `Tab.cleanUpBeforeClosing` crashes. Please see the video in the referenced task for more details. --- DuckDuckGo/Tab/Model/Tab.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 2ec1087956..031cbbe851 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -328,15 +328,15 @@ protocol NewWindowPolicyDecisionMaker { } deinit { - cleanUpBeforeClosing(onDeinit: true) + cleanUpBeforeClosing(onDeinit: true, webView: webView, userContentController: userContentController) } func cleanUpBeforeClosing() { - cleanUpBeforeClosing(onDeinit: false) + cleanUpBeforeClosing(onDeinit: false, webView: webView, userContentController: userContentController) } @MainActor(unsafe) - private func cleanUpBeforeClosing(onDeinit: Bool) { + private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView, userContentController: UserContentController?) { let job = { [webView, userContentController] in webView.stopAllMedia(shouldStopLoading: true) From 68fca97b29cee1e4fc3449ea3c41d3384827df87 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 1 May 2024 12:16:07 +0200 Subject: [PATCH 059/134] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 ++-- DuckDuckGo/ContentBlocker/macos-config.json | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 8650176744..935d103d42 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"0cbf39995c17cfe7455c0ceff1b5d8cc\"" - public static let embeddedDataSHA = "4e5495e24a27eba09553c335fc566bc694111e957faac6512093b76d3f585f71" + public static let embeddedDataETag = "\"3471e126687b3688c3512627a8fde0a1\"" + public static let embeddedDataSHA = "a49fcdf77320604568abb7c69704d1fd93069b952cf1e2f6f3ffcf13020172ec" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 361f28807b..af01839109 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1714411110624, + "version": 1714486769720, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -3528,6 +3528,15 @@ } ] }, + { + "domain": "realtor.com", + "rules": [ + { + "selector": ".ads_container", + "type": "hide" + } + ] + }, { "domain": "reddit.com", "rules": [ @@ -4278,7 +4287,7 @@ ] }, "state": "enabled", - "hash": "cebfdc8b9e9f34d7e407f430850e7d70" + "hash": "893bd7422971b3a7b4c6e02cdfc6332d" }, "exceptionHandler": { "exceptions": [ From c5c3a7fcd187888cac5c61548e8b4ff4d835d168 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 1 May 2024 12:16:07 +0200 Subject: [PATCH 060/134] Bump version to 1.86.0 (179) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 3b1dd4f073..2410e9736d 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 178 +CURRENT_PROJECT_VERSION = 179 From 924d880c81b08f2d35aa4a9443422b052adb51d1 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 1 May 2024 10:44:36 +0000 Subject: [PATCH 061/134] Bump version to 1.86.0 (180) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 2410e9736d..ca1afd458a 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 179 +CURRENT_PROJECT_VERSION = 180 From 89a38be9d286ed05c2257ee82edeb38e2a085430 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 1 May 2024 14:56:13 +0200 Subject: [PATCH 062/134] Count VPN controller cancellations (#2720) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207193126056882/f ## Description Stop counting when the user disables the VPN configuration creation as a controller start failure. --- .../NetworkProtectionPixelEvent.swift | 6 +++ .../NetworkProtectionTunnelController.swift | 54 +++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 50a2b96444..e9b9f4c847 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -28,6 +28,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionControllerStartAttempt case networkProtectionControllerStartSuccess + case networkProtectionControllerStartCancelled case networkProtectionControllerStartFailure(_ error: Error) case networkProtectionTunnelStartAttempt @@ -122,6 +123,9 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case .networkProtectionControllerStartSuccess: return "netp_controller_start_success" + case .networkProtectionControllerStartCancelled: + return "netp_controller_start_cancelled" + case .networkProtectionControllerStartFailure: return "netp_controller_start_failure" @@ -344,6 +348,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionNewUser, .networkProtectionControllerStartAttempt, .networkProtectionControllerStartSuccess, + .networkProtectionControllerStartCancelled, .networkProtectionControllerStartFailure, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, @@ -415,6 +420,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionNewUser, .networkProtectionControllerStartAttempt, .networkProtectionControllerStartSuccess, + .networkProtectionControllerStartCancelled, .networkProtectionTunnelStartAttempt, .networkProtectionTunnelStartSuccess, .networkProtectionTunnelStopAttempt, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index b1bebcadbb..a9051ab6a2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -448,14 +448,18 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Starting & Stopping the VPN - enum StartError: LocalizedError { + enum StartError: LocalizedError, CustomNSError { + case cancelled case noAuthToken case connectionStatusInvalid case connectionAlreadyStarted case simulateControllerFailureError + case startTunnelFailure(_ error: Error) var errorDescription: String? { switch self { + case .cancelled: + return nil case .noAuthToken: return "You need a subscription to start the VPN" case .connectionAlreadyStarted: @@ -473,6 +477,34 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr #endif case .simulateControllerFailureError: return "Simulated a controller error as requested" + case .startTunnelFailure(let error): + return error.localizedDescription + } + } + + var errorCode: Int { + switch self { + case .cancelled: return 0 + // MARK: Setup errors + case .noAuthToken: return 1 + case .connectionStatusInvalid: return 2 + case .connectionAlreadyStarted: return 3 + case .simulateControllerFailureError: return 4 + // MARK: Actual connection attempt issues + case .startTunnelFailure: return 100 + } + } + + var errorUserInfo: [String: Any] { + switch self { + case .cancelled, + .noAuthToken, + .connectionStatusInvalid, + .connectionAlreadyStarted, + .simulateControllerFailureError: + return [:] + case .startTunnelFailure(let error): + return [NSUnderlyingErrorKey: error] } } } @@ -502,6 +534,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } catch { if case NEVPNError.configurationReadWriteFailed = error { onboardingStatusRawValue = OnboardingStatus.isOnboarding(step: .userNeedsToAllowVPNConfiguration).rawValue + + throw StartError.cancelled } throw error @@ -528,9 +562,15 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } catch { VPNOperationErrorRecorder().recordControllerStartFailure(error) - PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true - ) + if case StartError.cancelled = error { + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionControllerStartCancelled, frequency: .dailyAndCount, includeAppVersionParameter: true + ) + } else { + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndCount, includeAppVersionParameter: true + ) + } await stop() @@ -577,7 +617,11 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr throw StartError.simulateControllerFailureError } - try tunnelManager.connection.startVPNTunnel(options: options) + do { + try tunnelManager.connection.startVPNTunnel(options: options) + } catch { + throw StartError.startTunnelFailure(error) + } PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionNewUser, From e2d1428530c4f695081252d52ac1041768e36e62 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 1 May 2024 15:58:50 +0100 Subject: [PATCH 063/134] Use NotLocalizedString on DBP error screen copy https://app.asana.com/0/1204167627774280/1207141364289924/f --- DuckDuckGo.xcodeproj/project.pbxproj | 10 +++ .../Common/Localizables/UserText+DBP.swift | 86 +++++++++++++++++++ .../UserText+NetworkProtection.swift | 54 ------------ DuckDuckGo/Common/Localizables/UserText.swift | 9 -- 4 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 DuckDuckGo/Common/Localizables/UserText+DBP.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 975b9f1315..a373ea7b73 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -185,6 +185,10 @@ 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 31A3A4E22B0C115F0021063C /* DataBrokerProtection */; }; + 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; + 31A83FB62BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; + 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; + 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31B4AF532901A4F20013585E /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B4AF522901A4F20013585E /* NSEventExtension.swift */; }; @@ -2850,6 +2854,7 @@ 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureDisabler.swift; sourceTree = ""; }; 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAppEvents.swift; sourceTree = ""; }; 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionVisibilityTests.swift; sourceTree = ""; }; + 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+DBP.swift"; sourceTree = ""; }; 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemPixels.swift; sourceTree = ""; }; 31B4AF522901A4F20013585E /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 31C3CE0128EDC1E70002C24A /* CustomRoundedCornersShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRoundedCornersShape.swift; sourceTree = ""; }; @@ -6798,6 +6803,7 @@ children = ( AA80EC53256BE3BC007083E7 /* UserText.swift */, 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */, + 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */, ); path = Localizables; sourceTree = ""; @@ -9981,6 +9987,7 @@ 3706FC2F293F65D500E42796 /* MouseOverButton.swift in Sources */, 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 */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, @@ -10682,6 +10689,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, @@ -10695,6 +10703,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, @@ -11457,6 +11466,7 @@ B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, + 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText+DBP.swift b/DuckDuckGo/Common/Localizables/UserText+DBP.swift new file mode 100644 index 0000000000..65590075e0 --- /dev/null +++ b/DuckDuckGo/Common/Localizables/UserText+DBP.swift @@ -0,0 +1,86 @@ +// +// UserText+DBP.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 Common + +#if DBP +// MARK: - Data Broker Protection Waitlist +extension UserText { + // "data-broker-protection.privacy-policy.title" - Privacy Policy title for Personal Information Removal + static let dataBrokerProtectionPrivacyPolicyTitle = "Privacy Policy" + // "data-broker-protection.waitlist.notification.title" - Title for Personal Information Removal waitlist notification + static let dataBrokerProtectionWaitlistNotificationTitle = "Personal Information Removal beta is ready!" + // "data-broker-protection.waitlist.notification.text" - Title for Personal Information Removal waitlist notification + static let dataBrokerProtectionWaitlistNotificationText = "Open your invite" + // "data-broker-protection.waitlist.join.title" - Title for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistJoinTitle = "Personal Information Removal Beta" + // "data-broker-protection.waitlist.join.subtitle.1" - First subtitle for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistJoinSubtitle1 = "Automatically scan and remove your data from 17+ sites that sell personal information with DuckDuckGo’s Personal Information Removal." + // "data-broker-protection.waitlist.joined.title" - Title for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistJoinedTitle = "You’re on the list!" + // "data-broker-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for Personal Information Removal joined waitlist screen when notifications are enabled + static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." + // "data-broker-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for Personal Information Removal joined waitlist screen when notifications are enabled + static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." + // "data-broker-protection.waitlist.enable-notifications" - Enable notifications prompt for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistEnableNotifications = "Want to get a notification when your Personal Information Removal invite is ready?" + // "data-broker-protection.waitlist.invited.title" - Title for Personal Information Removal invited screen + static let dataBrokerProtectionWaitlistInvitedTitle = "You’re invited to try\nPersonal Information Removal beta!" + // "data-broker-protection.waitlist.invited.subtitle" - Subtitle for Personal Information Removal invited screen + static let dataBrokerProtectionWaitlistInvitedSubtitle = "Automatically find and remove your personal information – such as your name and address – from 17+ sites that store and sell it, reducing the risk of identity theft and spam." + // "data-broker-protection.waitlist.enable.title" - Title for Personal Information Removal enable screen + static let dataBrokerProtectionWaitlistEnableTitle = "Let’s get started" + // "data-broker-protection.waitlist.enable.subtitle" - Subtitle for Personal Information Removal enable screen + static let dataBrokerProtectionWaitlistEnableSubtitle = "We’ll need your name, address and the year you were born in order to find your personal information on data broker sites\n\nThis info is stored securely on your device, and is never sent to DuckDuckGo." + // "data-broker-protection.waitlist.availability-disclaimer" - Availability disclaimer for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistAvailabilityDisclaimer = "Personal Information Removal is free during the beta.\nJoin the waitlist and we'll notify you when ready." + // "data-broker-protection.waitlist.button.close" - Close button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonClose = "Close" + // "data-broker-protection.waitlist.button.done" - Close button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonDone = "Done" + // "data-broker-protection.waitlist.button.dismiss" - Dismiss button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonDismiss = "Dismiss" + // "data-broker-protection.waitlist.button.cancel" - Cancel button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonCancel = "Cancel" + // "data-broker-protection.waitlist.button.no-thanks" - No Thanks button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonNoThanks = "No Thanks" + // "data-broker-protection.waitlist.button.get-started" - Get Started button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonGetStarted = "Get Started" + // "data-broker-protection.waitlist.button.got-it" - Get started button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonGotIt = "Get started" + // "data-broker-protection.waitlist.button.enable-notifications" - Enable Notifications button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonEnableNotifications = "Enable Notifications" + // "data-broker-protection.waitlist.button.join-waitlist" - Join Waitlist button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" + // "data-broker-protection.waitlist.button.agree-and-continue" - Agree and Continue button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" +} + +// MARK: - DBP Error pages +extension UserText { + static let dbpErrorPageBadPathTitle = NotLocalizedString("dbp.errorpage.bad.path.title", value: "Move DuckDuckGo App to Applications", comment: "Title for Personal Information Removal bad path error screen") + static let dbpErrorPageBadPathMessage = NotLocalizedString("dbp.errorpage.bad.path.message", value: "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you.", comment: "Message for Personal Information Removal bad path error screen") + static let dbpErrorPageBadPathCTA = NotLocalizedString("dbp.errorpage.bad.path.cta", value: "Move App for Me...", comment: "Call to action for moving the app to the Applications folder") + + static let dbpErrorPageNoPermissionTitle = NotLocalizedString("dbp.errorpage.no.permission.title", value: "Change System Setting", comment: "Title for error screen when there is no permission") + static let dbpErrorPageNoPermissionMessage = NotLocalizedString("dbp.errorpage.no.permission.message", value: "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background.", comment: "Message for error screen when there is no permission") + static let dbpErrorPageNoPermissionCTA = NotLocalizedString("dbp.errorpage.no.permission.cta", value: "Open System Settings...", comment: "Call to action for opening system settings") +} + +#endif diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 99770904df..d908735e23 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -189,60 +189,6 @@ extension UserText { } } -#if DBP -// MARK: - Data Broker Protection Waitlist -extension UserText { - // "data-broker-protection.privacy-policy.title" - Privacy Policy title for Personal Information Removal - static let dataBrokerProtectionPrivacyPolicyTitle = "Privacy Policy" - // "data-broker-protection.waitlist.notification.title" - Title for Personal Information Removal waitlist notification - static let dataBrokerProtectionWaitlistNotificationTitle = "Personal Information Removal beta is ready!" - // "data-broker-protection.waitlist.notification.text" - Title for Personal Information Removal waitlist notification - static let dataBrokerProtectionWaitlistNotificationText = "Open your invite" - // "data-broker-protection.waitlist.join.title" - Title for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistJoinTitle = "Personal Information Removal Beta" - // "data-broker-protection.waitlist.join.subtitle.1" - First subtitle for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistJoinSubtitle1 = "Automatically scan and remove your data from 17+ sites that sell personal information with DuckDuckGo’s Personal Information Removal." - // "data-broker-protection.waitlist.joined.title" - Title for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistJoinedTitle = "You’re on the list!" - // "data-broker-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for Personal Information Removal joined waitlist screen when notifications are enabled - static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." - // "data-broker-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for Personal Information Removal joined waitlist screen when notifications are enabled - static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." - // "data-broker-protection.waitlist.enable-notifications" - Enable notifications prompt for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistEnableNotifications = "Want to get a notification when your Personal Information Removal invite is ready?" - // "data-broker-protection.waitlist.invited.title" - Title for Personal Information Removal invited screen - static let dataBrokerProtectionWaitlistInvitedTitle = "You’re invited to try\nPersonal Information Removal beta!" - // "data-broker-protection.waitlist.invited.subtitle" - Subtitle for Personal Information Removal invited screen - static let dataBrokerProtectionWaitlistInvitedSubtitle = "Automatically find and remove your personal information – such as your name and address – from 17+ sites that store and sell it, reducing the risk of identity theft and spam." - // "data-broker-protection.waitlist.enable.title" - Title for Personal Information Removal enable screen - static let dataBrokerProtectionWaitlistEnableTitle = "Let’s get started" - // "data-broker-protection.waitlist.enable.subtitle" - Subtitle for Personal Information Removal enable screen - static let dataBrokerProtectionWaitlistEnableSubtitle = "We’ll need your name, address and the year you were born in order to find your personal information on data broker sites\n\nThis info is stored securely on your device, and is never sent to DuckDuckGo." - // "data-broker-protection.waitlist.availability-disclaimer" - Availability disclaimer for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistAvailabilityDisclaimer = "Personal Information Removal is free during the beta.\nJoin the waitlist and we'll notify you when ready." - // "data-broker-protection.waitlist.button.close" - Close button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonClose = "Close" - // "data-broker-protection.waitlist.button.done" - Close button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonDone = "Done" - // "data-broker-protection.waitlist.button.dismiss" - Dismiss button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonDismiss = "Dismiss" - // "data-broker-protection.waitlist.button.cancel" - Cancel button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonCancel = "Cancel" - // "data-broker-protection.waitlist.button.no-thanks" - No Thanks button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonNoThanks = "No Thanks" - // "data-broker-protection.waitlist.button.get-started" - Get Started button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonGetStarted = "Get Started" - // "data-broker-protection.waitlist.button.got-it" - Get started button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonGotIt = "Get started" - // "data-broker-protection.waitlist.button.enable-notifications" - Enable Notifications button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonEnableNotifications = "Enable Notifications" - // "data-broker-protection.waitlist.button.join-waitlist" - Join Waitlist button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" - // "data-broker-protection.waitlist.button.agree-and-continue" - Agree and Continue button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" -} -#endif - // MARK: - Thank You Modals extension UserText { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index bd4d6505d3..f5e57f5a94 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1163,13 +1163,4 @@ struct UserText { // Comment: "Progress view title when completing the purchase" static let completingPurchaseTitle = "Completing purchase..." - // MARK: - DBP Error pages - - static let dbpErrorPageBadPathTitle = "Move DuckDuckGo App to Applications" - static let dbpErrorPageBadPathMessage = "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you." - static let dbpErrorPageBadPathCTA = "Move App for Me..." - - static let dbpErrorPageNoPermissionTitle = "Change System Setting" - static let dbpErrorPageNoPermissionMessage = "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background." - static let dbpErrorPageNoPermissionCTA = "Open System Settings..." } From 71e3c0cd911dcc6240fe1fca54bf560a139ed5d2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 1 May 2024 16:17:06 +0100 Subject: [PATCH 064/134] Revert "Use NotLocalizedString on DBP error screen copy" (#2731) Task/Issue URL: https://app.asana.com/0/1204167627774280/1207212073888336/f Tech Design URL: CC: **Description**: This reverts commit e2d1428530c4f695081252d52ac1041768e36e62. --- DuckDuckGo.xcodeproj/project.pbxproj | 10 --- .../Common/Localizables/UserText+DBP.swift | 86 ------------------- .../UserText+NetworkProtection.swift | 54 ++++++++++++ DuckDuckGo/Common/Localizables/UserText.swift | 9 ++ 4 files changed, 63 insertions(+), 96 deletions(-) delete mode 100644 DuckDuckGo/Common/Localizables/UserText+DBP.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a373ea7b73..975b9f1315 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -185,10 +185,6 @@ 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 31A3A4E22B0C115F0021063C /* DataBrokerProtection */; }; - 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; - 31A83FB62BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; - 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; - 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31B4AF532901A4F20013585E /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B4AF522901A4F20013585E /* NSEventExtension.swift */; }; @@ -2854,7 +2850,6 @@ 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureDisabler.swift; sourceTree = ""; }; 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAppEvents.swift; sourceTree = ""; }; 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionVisibilityTests.swift; sourceTree = ""; }; - 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+DBP.swift"; sourceTree = ""; }; 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemPixels.swift; sourceTree = ""; }; 31B4AF522901A4F20013585E /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 31C3CE0128EDC1E70002C24A /* CustomRoundedCornersShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRoundedCornersShape.swift; sourceTree = ""; }; @@ -6803,7 +6798,6 @@ children = ( AA80EC53256BE3BC007083E7 /* UserText.swift */, 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */, - 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */, ); path = Localizables; sourceTree = ""; @@ -9987,7 +9981,6 @@ 3706FC2F293F65D500E42796 /* MouseOverButton.swift in Sources */, 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 */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, @@ -10689,7 +10682,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, @@ -10703,7 +10695,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, @@ -11466,7 +11457,6 @@ B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, - 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText+DBP.swift b/DuckDuckGo/Common/Localizables/UserText+DBP.swift deleted file mode 100644 index 65590075e0..0000000000 --- a/DuckDuckGo/Common/Localizables/UserText+DBP.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// UserText+DBP.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 Common - -#if DBP -// MARK: - Data Broker Protection Waitlist -extension UserText { - // "data-broker-protection.privacy-policy.title" - Privacy Policy title for Personal Information Removal - static let dataBrokerProtectionPrivacyPolicyTitle = "Privacy Policy" - // "data-broker-protection.waitlist.notification.title" - Title for Personal Information Removal waitlist notification - static let dataBrokerProtectionWaitlistNotificationTitle = "Personal Information Removal beta is ready!" - // "data-broker-protection.waitlist.notification.text" - Title for Personal Information Removal waitlist notification - static let dataBrokerProtectionWaitlistNotificationText = "Open your invite" - // "data-broker-protection.waitlist.join.title" - Title for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistJoinTitle = "Personal Information Removal Beta" - // "data-broker-protection.waitlist.join.subtitle.1" - First subtitle for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistJoinSubtitle1 = "Automatically scan and remove your data from 17+ sites that sell personal information with DuckDuckGo’s Personal Information Removal." - // "data-broker-protection.waitlist.joined.title" - Title for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistJoinedTitle = "You’re on the list!" - // "data-broker-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for Personal Information Removal joined waitlist screen when notifications are enabled - static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." - // "data-broker-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for Personal Information Removal joined waitlist screen when notifications are enabled - static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." - // "data-broker-protection.waitlist.enable-notifications" - Enable notifications prompt for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistEnableNotifications = "Want to get a notification when your Personal Information Removal invite is ready?" - // "data-broker-protection.waitlist.invited.title" - Title for Personal Information Removal invited screen - static let dataBrokerProtectionWaitlistInvitedTitle = "You’re invited to try\nPersonal Information Removal beta!" - // "data-broker-protection.waitlist.invited.subtitle" - Subtitle for Personal Information Removal invited screen - static let dataBrokerProtectionWaitlistInvitedSubtitle = "Automatically find and remove your personal information – such as your name and address – from 17+ sites that store and sell it, reducing the risk of identity theft and spam." - // "data-broker-protection.waitlist.enable.title" - Title for Personal Information Removal enable screen - static let dataBrokerProtectionWaitlistEnableTitle = "Let’s get started" - // "data-broker-protection.waitlist.enable.subtitle" - Subtitle for Personal Information Removal enable screen - static let dataBrokerProtectionWaitlistEnableSubtitle = "We’ll need your name, address and the year you were born in order to find your personal information on data broker sites\n\nThis info is stored securely on your device, and is never sent to DuckDuckGo." - // "data-broker-protection.waitlist.availability-disclaimer" - Availability disclaimer for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistAvailabilityDisclaimer = "Personal Information Removal is free during the beta.\nJoin the waitlist and we'll notify you when ready." - // "data-broker-protection.waitlist.button.close" - Close button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonClose = "Close" - // "data-broker-protection.waitlist.button.done" - Close button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonDone = "Done" - // "data-broker-protection.waitlist.button.dismiss" - Dismiss button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonDismiss = "Dismiss" - // "data-broker-protection.waitlist.button.cancel" - Cancel button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonCancel = "Cancel" - // "data-broker-protection.waitlist.button.no-thanks" - No Thanks button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonNoThanks = "No Thanks" - // "data-broker-protection.waitlist.button.get-started" - Get Started button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonGetStarted = "Get Started" - // "data-broker-protection.waitlist.button.got-it" - Get started button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonGotIt = "Get started" - // "data-broker-protection.waitlist.button.enable-notifications" - Enable Notifications button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonEnableNotifications = "Enable Notifications" - // "data-broker-protection.waitlist.button.join-waitlist" - Join Waitlist button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" - // "data-broker-protection.waitlist.button.agree-and-continue" - Agree and Continue button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" -} - -// MARK: - DBP Error pages -extension UserText { - static let dbpErrorPageBadPathTitle = NotLocalizedString("dbp.errorpage.bad.path.title", value: "Move DuckDuckGo App to Applications", comment: "Title for Personal Information Removal bad path error screen") - static let dbpErrorPageBadPathMessage = NotLocalizedString("dbp.errorpage.bad.path.message", value: "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you.", comment: "Message for Personal Information Removal bad path error screen") - static let dbpErrorPageBadPathCTA = NotLocalizedString("dbp.errorpage.bad.path.cta", value: "Move App for Me...", comment: "Call to action for moving the app to the Applications folder") - - static let dbpErrorPageNoPermissionTitle = NotLocalizedString("dbp.errorpage.no.permission.title", value: "Change System Setting", comment: "Title for error screen when there is no permission") - static let dbpErrorPageNoPermissionMessage = NotLocalizedString("dbp.errorpage.no.permission.message", value: "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background.", comment: "Message for error screen when there is no permission") - static let dbpErrorPageNoPermissionCTA = NotLocalizedString("dbp.errorpage.no.permission.cta", value: "Open System Settings...", comment: "Call to action for opening system settings") -} - -#endif diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index d908735e23..99770904df 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -189,6 +189,60 @@ extension UserText { } } +#if DBP +// MARK: - Data Broker Protection Waitlist +extension UserText { + // "data-broker-protection.privacy-policy.title" - Privacy Policy title for Personal Information Removal + static let dataBrokerProtectionPrivacyPolicyTitle = "Privacy Policy" + // "data-broker-protection.waitlist.notification.title" - Title for Personal Information Removal waitlist notification + static let dataBrokerProtectionWaitlistNotificationTitle = "Personal Information Removal beta is ready!" + // "data-broker-protection.waitlist.notification.text" - Title for Personal Information Removal waitlist notification + static let dataBrokerProtectionWaitlistNotificationText = "Open your invite" + // "data-broker-protection.waitlist.join.title" - Title for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistJoinTitle = "Personal Information Removal Beta" + // "data-broker-protection.waitlist.join.subtitle.1" - First subtitle for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistJoinSubtitle1 = "Automatically scan and remove your data from 17+ sites that sell personal information with DuckDuckGo’s Personal Information Removal." + // "data-broker-protection.waitlist.joined.title" - Title for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistJoinedTitle = "You’re on the list!" + // "data-broker-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for Personal Information Removal joined waitlist screen when notifications are enabled + static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." + // "data-broker-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for Personal Information Removal joined waitlist screen when notifications are enabled + static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." + // "data-broker-protection.waitlist.enable-notifications" - Enable notifications prompt for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistEnableNotifications = "Want to get a notification when your Personal Information Removal invite is ready?" + // "data-broker-protection.waitlist.invited.title" - Title for Personal Information Removal invited screen + static let dataBrokerProtectionWaitlistInvitedTitle = "You’re invited to try\nPersonal Information Removal beta!" + // "data-broker-protection.waitlist.invited.subtitle" - Subtitle for Personal Information Removal invited screen + static let dataBrokerProtectionWaitlistInvitedSubtitle = "Automatically find and remove your personal information – such as your name and address – from 17+ sites that store and sell it, reducing the risk of identity theft and spam." + // "data-broker-protection.waitlist.enable.title" - Title for Personal Information Removal enable screen + static let dataBrokerProtectionWaitlistEnableTitle = "Let’s get started" + // "data-broker-protection.waitlist.enable.subtitle" - Subtitle for Personal Information Removal enable screen + static let dataBrokerProtectionWaitlistEnableSubtitle = "We’ll need your name, address and the year you were born in order to find your personal information on data broker sites\n\nThis info is stored securely on your device, and is never sent to DuckDuckGo." + // "data-broker-protection.waitlist.availability-disclaimer" - Availability disclaimer for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistAvailabilityDisclaimer = "Personal Information Removal is free during the beta.\nJoin the waitlist and we'll notify you when ready." + // "data-broker-protection.waitlist.button.close" - Close button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonClose = "Close" + // "data-broker-protection.waitlist.button.done" - Close button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonDone = "Done" + // "data-broker-protection.waitlist.button.dismiss" - Dismiss button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonDismiss = "Dismiss" + // "data-broker-protection.waitlist.button.cancel" - Cancel button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonCancel = "Cancel" + // "data-broker-protection.waitlist.button.no-thanks" - No Thanks button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonNoThanks = "No Thanks" + // "data-broker-protection.waitlist.button.get-started" - Get Started button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonGetStarted = "Get Started" + // "data-broker-protection.waitlist.button.got-it" - Get started button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonGotIt = "Get started" + // "data-broker-protection.waitlist.button.enable-notifications" - Enable Notifications button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonEnableNotifications = "Enable Notifications" + // "data-broker-protection.waitlist.button.join-waitlist" - Join Waitlist button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" + // "data-broker-protection.waitlist.button.agree-and-continue" - Agree and Continue button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" +} +#endif + // MARK: - Thank You Modals extension UserText { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index f5e57f5a94..bd4d6505d3 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1163,4 +1163,13 @@ struct UserText { // Comment: "Progress view title when completing the purchase" static let completingPurchaseTitle = "Completing purchase..." + // MARK: - DBP Error pages + + static let dbpErrorPageBadPathTitle = "Move DuckDuckGo App to Applications" + static let dbpErrorPageBadPathMessage = "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you." + static let dbpErrorPageBadPathCTA = "Move App for Me..." + + static let dbpErrorPageNoPermissionTitle = "Change System Setting" + static let dbpErrorPageNoPermissionMessage = "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background." + static let dbpErrorPageNoPermissionCTA = "Open System Settings..." } From 398f9b86b9b250fdcc26725a4ec0158816e697f8 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 1 May 2024 22:46:30 +0200 Subject: [PATCH 065/134] =?UTF-8?q?Fixes=20an=20issue=20that's=20causing?= =?UTF-8?q?=20the=20Allow=20VPN=20to=20show=20up=20twice=20when=20dis?= =?UTF-8?q?=E2=80=A6=20(#2728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1207210448609392/f Description We no longer ask twice to allow the VPN configuration if the user decides to not allow it. --- .../BothAppTargets/NetworkProtectionTunnelController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index a9051ab6a2..ed20ffef1d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -572,7 +572,9 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr ) } - await stop() + if await isConnected { + await stop() + } // Always keep the first error message shown, as it's the more actionable one. if controllerErrorStore.lastErrorMessage == nil { From 401b0e92806fa608ce21be136cbf722d88e2c24e Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 1 May 2024 16:09:09 -0700 Subject: [PATCH 066/134] =?UTF-8?q?Break=20the=20Subscription=20module?= =?UTF-8?q?=E2=80=99s=20dependency=20on=20BSK=20(#2721)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1207201481522088/f Tech Design URL: CC: Description: Client PR for duckduckgo/BrowserServicesKit#802. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/NavigationBar/View/AddressBarTextField.swift | 1 + DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift | 1 - .../NetworkProtection+ConvenienceInitializers.swift | 1 + DuckDuckGo/Preferences/Model/PreferencesSection.swift | 1 + ...ltSubscriptionFeatureAvailability+DefaultInitializer.swift | 1 + DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift | 1 + .../Waitlist/Views/WaitlistThankYouPromptPresenter.swift | 2 +- .../Waitlist/Views/WaitlistViewControllerPresenter.swift | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 13 files changed, 13 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 975b9f1315..92b00ba11d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12738,7 +12738,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 142.0.0; + version = 143.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 72e9ff06b4..150efe31c4 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" : "2681b5271a4e0582f175771737617adb8a4d6e78", - "version" : "142.0.0" + "revision" : "7c41d69a93bbe80639fb7489e2018e5957ac2b5c", + "version" : "143.0.0" } }, { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index c3494d313d..25cc50a039 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -22,6 +22,7 @@ import Combine import Common import Suggestions import Subscription +import BrowserServicesKit final class AddressBarTextField: NSTextField { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 5d5a841f87..847452c63c 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -21,7 +21,6 @@ import Combine import Common import BrowserServicesKit import PixelKit - import NetworkProtection import Subscription diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 9898138c7f..fe1993cb94 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -21,6 +21,7 @@ import NetworkProtection import NetworkProtectionIPC import Common import Subscription +import BrowserServicesKit extension NetworkProtectionDeviceManager { diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 777b1a54dd..f9dbccd5b7 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -19,6 +19,7 @@ import Foundation import SwiftUI import Subscription +import BrowserServicesKit struct PreferencesSection: Hashable, Identifiable { let id: PreferencesSectionIdentifier diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift index 7e65e846e5..73e6407c28 100644 --- a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift +++ b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift @@ -18,6 +18,7 @@ import Foundation import Subscription +import BrowserServicesKit extension DefaultSubscriptionFeatureAvailability { convenience init() { diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index 21c18aafc4..d06cb5f3d3 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -19,6 +19,7 @@ import Navigation import Foundation import Subscription +import BrowserServicesKit struct RedirectNavigationResponder: NavigationResponder { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 4db6e135b5..11281dcaba 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -18,7 +18,7 @@ import AppKit import Foundation -import Subscription +import BrowserServicesKit import PixelKit final class WaitlistThankYouPromptPresenter { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index 78e0d51794..55ee0e6584 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -18,7 +18,7 @@ import Foundation import UserNotifications -import Subscription +import BrowserServicesKit protocol WaitlistViewControllerPresenter { static func show(completion: (() -> Void)?) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index e541454331..4170883328 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: "142.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "143.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index bc9b47340f..c9d81bcd9e 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "142.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "143.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 68611f927c..3062d34484 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: "142.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "143.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 8de8379f74d5d5f520536f1fe8270ebb4d998fac Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 2 May 2024 05:13:15 +0000 Subject: [PATCH 067/134] Bump version to 1.86.0 (181) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index ca1afd458a..e94261020d 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 180 +CURRENT_PROJECT_VERSION = 181 From 0c616cc6812b95a4cafc468e803da45cd8d2f5da Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 2 May 2024 16:53:20 +0100 Subject: [PATCH 068/134] Do not consider deprecated scans in the scan UI (#2729) Task/Issue URL: https://app.asana.com/0/1203581873609357/1207208737318475/f **Description**: Do not consider deprecated scans in the scan UI --- .../DataBrokerOperationsCollection.swift | 8 +++---- .../DataBrokerProtectionProcessor.swift | 2 +- .../DataBrokerProtection/UI/UIMapper.swift | 21 ++++++++----------- .../MapperToUITests.swift | 16 ++++++++++++++ 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index cbe22fe8fe..eeae49e168 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -31,7 +31,7 @@ protocol DataBrokerOperationsCollectionErrorDelegate: AnyObject { final class DataBrokerOperationsCollection: Operation { enum OperationType { - case scan + case manualScan case optOut case all } @@ -119,8 +119,8 @@ final class DataBrokerOperationsCollection: Operation { switch operationType { case .optOut: operationsData = brokerProfileQueriesData.flatMap { $0.optOutOperationsData } - case .scan: - operationsData = brokerProfileQueriesData.compactMap { $0.scanOperationData } + case .manualScan: + operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanOperationData } case .all: operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } } @@ -181,7 +181,7 @@ final class DataBrokerOperationsCollection: Operation { runner: runner, pixelHandler: pixelHandler, showWebView: showWebView, - isManualScan: operationType == .scan, + isManualScan: operationType == .manualScan, userNotificationService: userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 74b381eb3e..dee4d346e2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -59,7 +59,7 @@ final class DataBrokerProtectionProcessor { completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() - runOperations(operationType: .scan, + runOperations(operationType: .manualScan, priorityDate: nil, showWebView: showWebView) { errors in os_log("Scans done", log: .dataBrokerProtection) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 87ccad2494..ee89f641f2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -56,10 +56,15 @@ struct MapperToUI { // not by the total real cans that the app is doing. let profileQueriesGroupedByBroker = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.name }) - let totalScans = profileQueriesGroupedByBroker.reduce(0) { accumulator, element in + // We don't want to consider deprecated queries when reporting manual scans to the UI + let filteredProfileQueriesGroupedByBroker = profileQueriesGroupedByBroker.mapValues { queries in + queries.filter { !$0.profileQuery.deprecated } + } + + let totalScans = filteredProfileQueriesGroupedByBroker.reduce(0) { accumulator, element in return accumulator + element.value.totalScans } - let currentScans = profileQueriesGroupedByBroker.reduce(0) { accumulator, element in + let currentScans = filteredProfileQueriesGroupedByBroker.reduce(0) { accumulator, element in return accumulator + element.value.currentScans } @@ -333,23 +338,15 @@ fileprivate extension Array where Element == BrokerProfileQueryData { var totalScans: Int { guard let broker = self.first?.dataBroker else { return 0 } - - let areAllQueriesDeprecated = allSatisfy { $0.profileQuery.deprecated } - - if areAllQueriesDeprecated { - return 0 - } else { - return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count - } + return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count } var currentScans: Int { guard let broker = self.first?.dataBroker else { return 0 } - let areAllQueriesDeprecated = allSatisfy { $0.profileQuery.deprecated } let didAllQueriesFinished = allSatisfy { $0.scanOperationData.lastRunDate != nil } - if areAllQueriesDeprecated || !didAllQueriesFinished { + if !didAllQueriesFinished { return 0 } else { return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index 6629f5c0be..b7fc620d08 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -105,6 +105,22 @@ final class MapperToUITests: XCTestCase { XCTAssertEqual(result.resultsFound.count, 1) } + func testWhenScansHaveDeprecatedProfileQueriesThatDidNotRun_thenThoseAreNotTakenIntoAccount() { + let brokerProfileQueryData: [BrokerProfileQueryData] = [ + .mock(dataBrokerName: "Broker #1", lastRunDate: Date(), extractedProfile: .mockWithRemovedDate), + .mock(dataBrokerName: "Broker #1", lastRunDate: Date()), + .mock(dataBrokerName: "Broker #1", lastRunDate: nil, deprecated: true), + .mock(dataBrokerName: "Broker #2", lastRunDate: Date()), + .mock(dataBrokerName: "Broker #3", lastRunDate: Date(), extractedProfile: .mockWithRemovedDate, deprecated: true) + ] + + let result = sut.initialScanState(brokerProfileQueryData) + + XCTAssertEqual(result.scanProgress.totalScans, 2) + XCTAssertEqual(result.scanProgress.currentScans, 2) + XCTAssertEqual(result.resultsFound.count, 1) + } + func testInProgressAndCompletedOptOuts_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(extractedProfile: .mockWithRemovedDate), From a836a5c2f0ca82439667e3bb1170bdc8080c2051 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 2 May 2024 19:06:54 +0100 Subject: [PATCH 069/134] Use NotLocalizedString on DBP error screen copy (#2732) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207141364289924/f **Description**: Use NotLocalizedString on DBP error screen copy --- DuckDuckGo.xcodeproj/project.pbxproj | 10 +++ .../Common/Localizables/UserText+DBP.swift | 86 +++++++++++++++++++ .../UserText+NetworkProtection.swift | 54 ------------ DuckDuckGo/Common/Localizables/UserText.swift | 9 -- 4 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 DuckDuckGo/Common/Localizables/UserText+DBP.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 92b00ba11d..debf634575 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -185,6 +185,10 @@ 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 31A3A4E22B0C115F0021063C /* DataBrokerProtection */; }; + 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; + 31A83FB62BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; + 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; + 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31B4AF532901A4F20013585E /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B4AF522901A4F20013585E /* NSEventExtension.swift */; }; @@ -2850,6 +2854,7 @@ 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureDisabler.swift; sourceTree = ""; }; 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAppEvents.swift; sourceTree = ""; }; 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionVisibilityTests.swift; sourceTree = ""; }; + 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+DBP.swift"; sourceTree = ""; }; 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemPixels.swift; sourceTree = ""; }; 31B4AF522901A4F20013585E /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 31C3CE0128EDC1E70002C24A /* CustomRoundedCornersShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRoundedCornersShape.swift; sourceTree = ""; }; @@ -6798,6 +6803,7 @@ children = ( AA80EC53256BE3BC007083E7 /* UserText.swift */, 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */, + 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */, ); path = Localizables; sourceTree = ""; @@ -9981,6 +9987,7 @@ 3706FC2F293F65D500E42796 /* MouseOverButton.swift in Sources */, 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 */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, @@ -10682,6 +10689,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, @@ -10695,6 +10703,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, @@ -11457,6 +11466,7 @@ B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, + 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, 1DDC85032B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText+DBP.swift b/DuckDuckGo/Common/Localizables/UserText+DBP.swift new file mode 100644 index 0000000000..65590075e0 --- /dev/null +++ b/DuckDuckGo/Common/Localizables/UserText+DBP.swift @@ -0,0 +1,86 @@ +// +// UserText+DBP.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 Common + +#if DBP +// MARK: - Data Broker Protection Waitlist +extension UserText { + // "data-broker-protection.privacy-policy.title" - Privacy Policy title for Personal Information Removal + static let dataBrokerProtectionPrivacyPolicyTitle = "Privacy Policy" + // "data-broker-protection.waitlist.notification.title" - Title for Personal Information Removal waitlist notification + static let dataBrokerProtectionWaitlistNotificationTitle = "Personal Information Removal beta is ready!" + // "data-broker-protection.waitlist.notification.text" - Title for Personal Information Removal waitlist notification + static let dataBrokerProtectionWaitlistNotificationText = "Open your invite" + // "data-broker-protection.waitlist.join.title" - Title for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistJoinTitle = "Personal Information Removal Beta" + // "data-broker-protection.waitlist.join.subtitle.1" - First subtitle for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistJoinSubtitle1 = "Automatically scan and remove your data from 17+ sites that sell personal information with DuckDuckGo’s Personal Information Removal." + // "data-broker-protection.waitlist.joined.title" - Title for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistJoinedTitle = "You’re on the list!" + // "data-broker-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for Personal Information Removal joined waitlist screen when notifications are enabled + static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." + // "data-broker-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for Personal Information Removal joined waitlist screen when notifications are enabled + static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." + // "data-broker-protection.waitlist.enable-notifications" - Enable notifications prompt for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistEnableNotifications = "Want to get a notification when your Personal Information Removal invite is ready?" + // "data-broker-protection.waitlist.invited.title" - Title for Personal Information Removal invited screen + static let dataBrokerProtectionWaitlistInvitedTitle = "You’re invited to try\nPersonal Information Removal beta!" + // "data-broker-protection.waitlist.invited.subtitle" - Subtitle for Personal Information Removal invited screen + static let dataBrokerProtectionWaitlistInvitedSubtitle = "Automatically find and remove your personal information – such as your name and address – from 17+ sites that store and sell it, reducing the risk of identity theft and spam." + // "data-broker-protection.waitlist.enable.title" - Title for Personal Information Removal enable screen + static let dataBrokerProtectionWaitlistEnableTitle = "Let’s get started" + // "data-broker-protection.waitlist.enable.subtitle" - Subtitle for Personal Information Removal enable screen + static let dataBrokerProtectionWaitlistEnableSubtitle = "We’ll need your name, address and the year you were born in order to find your personal information on data broker sites\n\nThis info is stored securely on your device, and is never sent to DuckDuckGo." + // "data-broker-protection.waitlist.availability-disclaimer" - Availability disclaimer for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistAvailabilityDisclaimer = "Personal Information Removal is free during the beta.\nJoin the waitlist and we'll notify you when ready." + // "data-broker-protection.waitlist.button.close" - Close button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonClose = "Close" + // "data-broker-protection.waitlist.button.done" - Close button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonDone = "Done" + // "data-broker-protection.waitlist.button.dismiss" - Dismiss button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonDismiss = "Dismiss" + // "data-broker-protection.waitlist.button.cancel" - Cancel button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonCancel = "Cancel" + // "data-broker-protection.waitlist.button.no-thanks" - No Thanks button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonNoThanks = "No Thanks" + // "data-broker-protection.waitlist.button.get-started" - Get Started button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonGetStarted = "Get Started" + // "data-broker-protection.waitlist.button.got-it" - Get started button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonGotIt = "Get started" + // "data-broker-protection.waitlist.button.enable-notifications" - Enable Notifications button for Personal Information Removal joined waitlist screen + static let dataBrokerProtectionWaitlistButtonEnableNotifications = "Enable Notifications" + // "data-broker-protection.waitlist.button.join-waitlist" - Join Waitlist button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" + // "data-broker-protection.waitlist.button.agree-and-continue" - Agree and Continue button for Personal Information Removal join waitlist screen + static let dataBrokerProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" +} + +// MARK: - DBP Error pages +extension UserText { + static let dbpErrorPageBadPathTitle = NotLocalizedString("dbp.errorpage.bad.path.title", value: "Move DuckDuckGo App to Applications", comment: "Title for Personal Information Removal bad path error screen") + static let dbpErrorPageBadPathMessage = NotLocalizedString("dbp.errorpage.bad.path.message", value: "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you.", comment: "Message for Personal Information Removal bad path error screen") + static let dbpErrorPageBadPathCTA = NotLocalizedString("dbp.errorpage.bad.path.cta", value: "Move App for Me...", comment: "Call to action for moving the app to the Applications folder") + + static let dbpErrorPageNoPermissionTitle = NotLocalizedString("dbp.errorpage.no.permission.title", value: "Change System Setting", comment: "Title for error screen when there is no permission") + static let dbpErrorPageNoPermissionMessage = NotLocalizedString("dbp.errorpage.no.permission.message", value: "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background.", comment: "Message for error screen when there is no permission") + static let dbpErrorPageNoPermissionCTA = NotLocalizedString("dbp.errorpage.no.permission.cta", value: "Open System Settings...", comment: "Call to action for opening system settings") +} + +#endif diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 99770904df..d908735e23 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -189,60 +189,6 @@ extension UserText { } } -#if DBP -// MARK: - Data Broker Protection Waitlist -extension UserText { - // "data-broker-protection.privacy-policy.title" - Privacy Policy title for Personal Information Removal - static let dataBrokerProtectionPrivacyPolicyTitle = "Privacy Policy" - // "data-broker-protection.waitlist.notification.title" - Title for Personal Information Removal waitlist notification - static let dataBrokerProtectionWaitlistNotificationTitle = "Personal Information Removal beta is ready!" - // "data-broker-protection.waitlist.notification.text" - Title for Personal Information Removal waitlist notification - static let dataBrokerProtectionWaitlistNotificationText = "Open your invite" - // "data-broker-protection.waitlist.join.title" - Title for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistJoinTitle = "Personal Information Removal Beta" - // "data-broker-protection.waitlist.join.subtitle.1" - First subtitle for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistJoinSubtitle1 = "Automatically scan and remove your data from 17+ sites that sell personal information with DuckDuckGo’s Personal Information Removal." - // "data-broker-protection.waitlist.joined.title" - Title for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistJoinedTitle = "You’re on the list!" - // "data-broker-protection.waitlist.joined.with-notifications.subtitle.1" - Subtitle 1 for Personal Information Removal joined waitlist screen when notifications are enabled - static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle1 = "New invites are sent every few days, on a first come, first served basis." - // "data-broker-protection.waitlist.joined.with-notifications.subtitle.2" - Subtitle 2 for Personal Information Removal joined waitlist screen when notifications are enabled - static let dataBrokerProtectionWaitlistJoinedWithNotificationsSubtitle2 = "We’ll notify you when your invite is ready." - // "data-broker-protection.waitlist.enable-notifications" - Enable notifications prompt for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistEnableNotifications = "Want to get a notification when your Personal Information Removal invite is ready?" - // "data-broker-protection.waitlist.invited.title" - Title for Personal Information Removal invited screen - static let dataBrokerProtectionWaitlistInvitedTitle = "You’re invited to try\nPersonal Information Removal beta!" - // "data-broker-protection.waitlist.invited.subtitle" - Subtitle for Personal Information Removal invited screen - static let dataBrokerProtectionWaitlistInvitedSubtitle = "Automatically find and remove your personal information – such as your name and address – from 17+ sites that store and sell it, reducing the risk of identity theft and spam." - // "data-broker-protection.waitlist.enable.title" - Title for Personal Information Removal enable screen - static let dataBrokerProtectionWaitlistEnableTitle = "Let’s get started" - // "data-broker-protection.waitlist.enable.subtitle" - Subtitle for Personal Information Removal enable screen - static let dataBrokerProtectionWaitlistEnableSubtitle = "We’ll need your name, address and the year you were born in order to find your personal information on data broker sites\n\nThis info is stored securely on your device, and is never sent to DuckDuckGo." - // "data-broker-protection.waitlist.availability-disclaimer" - Availability disclaimer for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistAvailabilityDisclaimer = "Personal Information Removal is free during the beta.\nJoin the waitlist and we'll notify you when ready." - // "data-broker-protection.waitlist.button.close" - Close button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonClose = "Close" - // "data-broker-protection.waitlist.button.done" - Close button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonDone = "Done" - // "data-broker-protection.waitlist.button.dismiss" - Dismiss button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonDismiss = "Dismiss" - // "data-broker-protection.waitlist.button.cancel" - Cancel button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonCancel = "Cancel" - // "data-broker-protection.waitlist.button.no-thanks" - No Thanks button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonNoThanks = "No Thanks" - // "data-broker-protection.waitlist.button.get-started" - Get Started button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonGetStarted = "Get Started" - // "data-broker-protection.waitlist.button.got-it" - Get started button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonGotIt = "Get started" - // "data-broker-protection.waitlist.button.enable-notifications" - Enable Notifications button for Personal Information Removal joined waitlist screen - static let dataBrokerProtectionWaitlistButtonEnableNotifications = "Enable Notifications" - // "data-broker-protection.waitlist.button.join-waitlist" - Join Waitlist button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonJoinWaitlist = "Join the Waitlist" - // "data-broker-protection.waitlist.button.agree-and-continue" - Agree and Continue button for Personal Information Removal join waitlist screen - static let dataBrokerProtectionWaitlistButtonAgreeAndContinue = "Agree and Continue" -} -#endif - // MARK: - Thank You Modals extension UserText { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index bd4d6505d3..f5e57f5a94 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1163,13 +1163,4 @@ struct UserText { // Comment: "Progress view title when completing the purchase" static let completingPurchaseTitle = "Completing purchase..." - // MARK: - DBP Error pages - - static let dbpErrorPageBadPathTitle = "Move DuckDuckGo App to Applications" - static let dbpErrorPageBadPathMessage = "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you." - static let dbpErrorPageBadPathCTA = "Move App for Me..." - - static let dbpErrorPageNoPermissionTitle = "Change System Setting" - static let dbpErrorPageNoPermissionMessage = "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background." - static let dbpErrorPageNoPermissionCTA = "Open System Settings..." } From 0a211f21b7bd77cd9046ba223340f7ef32e12c7b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 3 May 2024 10:58:42 +0200 Subject: [PATCH 070/134] Improve VPN underlying error detail (#2736) Task/Issue URL: https://app.asana.com/0/0/1207221937455192/f ## Description Improves VPN underlying error detail for `NetworkProtectionClientError` and `WireGuardError`. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index df5499bc80..754239fdb8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12724,7 +12724,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 141.1.2; + version = "141.1.2-1"; }; }; 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 64aca2b97a..5a30e277d5 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" : "f8c73292d4d6724ec81f98bd29dbb2061f1a8cf6", - "version" : "141.1.2" + "revision" : "35730c74d0600a57d90690867722ea2b615b6935", + "version" : "141.1.2-1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 7657755407..6429cf99a2 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: "141.1.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2-1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 45900c0243..83d9211ff2 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2-1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 7c57479333..ef9b0642aa 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: "141.1.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "141.1.2-1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 1342d8f989154eb8ef288b14536be976fc290f8a Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 3 May 2024 02:00:36 -0700 Subject: [PATCH 071/134] Remove VPN server fetch at app launch (#2738) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207229848611232/f Tech Design URL: CC: Description: This PR removes the server fetch at app launch. --- .../NetworkProtectionAppEvents.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index 25be11ba61..fcc9929d18 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -72,7 +72,6 @@ final class NetworkProtectionAppEvents { } restartNetworkProtectionIfVersionChanged(using: loginItemsManager) - refreshNetworkProtectionServers() } } @@ -97,19 +96,4 @@ final class NetworkProtectionAppEvents { loginItemsManager.restartLoginItems(LoginItemsManager.networkProtectionLoginItems, log: .networkProtection) } - /// Fetches a new list of VPN servers, and updates the existing set. - /// - private func refreshNetworkProtectionServers() { - Task { - let serverCount: Int - do { - serverCount = try await NetworkProtectionDeviceManager.create().refreshServerList().count - } catch { - os_log("Failed to update DuckDuckGo VPN servers", log: .networkProtection, type: .error) - return - } - - os_log("Successfully updated DuckDuckGo VPN servers; total server count = %{public}d", log: .networkProtection, serverCount) - } - } } From d13eec82affb49449d3059c1cdc9ccc8b7211527 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 3 May 2024 09:23:01 +0000 Subject: [PATCH 072/134] Bump version to 1.86.0 (182) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index e94261020d..3d9e7b5269 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 181 +CURRENT_PROJECT_VERSION = 182 From 780958d011d814f0b3b15c1a1c43863dfcd91dbf Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 3 May 2024 15:55:52 +0200 Subject: [PATCH 073/134] Support expired subscription state and subscription repurchase (#2707) Task/Issue URL: https://app.asana.com/0/72649045549333/1206382263248900/f **Description**: Support expired subscription state and subscription repurchase. Repurchase is done on a platform that is matching the app distribution (independent to where it was originally bought). Changes for supporting repurchase flows are/were implemented in BSK in the following PRs: - App Store repurchase: https://github.com/duckduckgo/BrowserServicesKit/pull/788 - Stripe repurchase: https://github.com/duckduckgo/BrowserServicesKit/pull/800 **Steps to test this PR**: 1. Activate expired subscription via email 2. Repurchase expired App Store subscription on Stripe 3. Repurchase expired Stripe subscription on App Store 4. Repurchase expired Stripe subscription on Stripe --- ###### 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) --------- Co-authored-by: Daniel Bernal --- .../SubscriptionPagesUserScript.swift | 17 ++++++++++++----- .../PreferencesSubscriptionView.swift | 8 ++++++-- .../subscription-expired-icon.pdf | Bin 2374 -> 1170 bytes .../Sources/SubscriptionUI/UserText.swift | 7 ++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index bee8634adc..4d9b38724b 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -105,6 +105,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { static let subscriptionsUnknownPriceClicked = "subscriptionsUnknownPriceClicked" static let subscriptionsAddEmailSuccess = "subscriptionsAddEmailSuccess" static let subscriptionsWelcomeFaqClicked = "subscriptionsWelcomeFaqClicked" + static let getAccessToken = "getAccessToken" } // swiftlint:disable:next cyclomatic_complexity @@ -124,6 +125,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case Handlers.subscriptionsUnknownPriceClicked: return subscriptionsUnknownPriceClicked case Handlers.subscriptionsAddEmailSuccess: return subscriptionsAddEmailSuccess case Handlers.subscriptionsWelcomeFaqClicked: return subscriptionsWelcomeFaqClicked + case Handlers.getAccessToken: return getAccessToken default: return nil } @@ -142,11 +144,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let authToken = accountManager.authToken, accountManager.accessToken != nil { - return Subscription(token: authToken) - } else { - return Subscription(token: "") - } + let authToken = accountManager.authToken ?? "" + return Subscription(token: authToken) } func setSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? { @@ -451,6 +450,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return nil } + func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { + if let accessToken = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken { + return ["token": accessToken] + } else { + return [String: String]() + } + } + // MARK: Push actions enum SubscribeActionName: String { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index db764f58e1..a0e31c7d4b 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -185,9 +185,13 @@ public struct PreferencesSubscriptionView: View { TextMenuItemCaption(UserText.preferencesSubscriptionExpiredCaption) } buttons: { // We need to improve re-purchase flow - /* Button(UserText.viewPlansButtonTitle) { model.purchaseAction() } - .buttonStyle(DefaultActionButtonStyle(enabled: true)) */ + Button(UserText.viewPlansExpiredButtonTitle) { model.purchaseAction() } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) Menu { + Button(UserText.addToAnotherDeviceButton) { + model.userEventHandler(.addToAnotherDeviceClick) + showingSheet.toggle() + } Button(UserText.removeFromThisDeviceButton, action: { showingRemoveConfirmationDialog.toggle() }) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf index d43daeb9b999065914b2ae55c80a3467fee00e9a..a971e48c07a4f1e34f2f92c519b02d4e8cc650e6 100644 GIT binary patch delta 695 zcmX>mG>LP9c)h-BMTvWGNn%N=f{l%WzN;aaN={TT&@(as0R=ld2;xf3OUX~l;^Hz= zFi-$76u?T|GIL5&ixl+Ta)4&Iq$cO5qylAtK;I`dFTEr~!5FAK2qHpIoua(}m321S<9H$ zuJ=<&TV{T+N2%rZ^vBy*o(nDsD%kpZns-N`vpuZvJsx7%FObC@)+>IcKF=zs&p<1v6(QTvv2z z?2di>tXMEChwCuswX1?kAMbGQnkyS-EwkMw_CRiy`@dU(l35Bp2CcJBn&ob6S&%SK zok{BED^@A%z24;~9@}z0R^8FKOQYUx#)S3zoBC@`p0_{DsM{!MCa3i4p4_Zyl?$88 z&3fPMOj*--AWN`k7L$#S>)ot!w@&TV-b|ur__*U+%QxQ=ZFAwj`&UeOz)~Oq2fU zy5GJ&`QlDKLq~ms`V;2QQx<*AF45CIq4c=c>sD4Qt0DV3gV<#Y4hu}5>`*>8^xHHM zmoto-3%!)&7Ej@L#k%9aS=1Ze3p)a0CiAe%GMSid)?qJZ^fNb6fFzwfE--CiU}j{9 jA!Gtf7$BekS7l*nh9P8WF!?QqKc}UMC6}tItG^on9E=(P literal 2374 zcmbVOOHbS|5Wf3Y%moQ5tz#!~oJa_%ETyX2s&wfM^^j%_Y{e$+CRDV)zGKH88y2aL z9MJ6NnQtB*&v0;YcD|5YGbTa6@Z~E5I6j7x6K49g{mJqa&)%8-X1av}!Ijk-r=4lH zEITtlHl10$K859F`l}n+cP6=TABZ3LO!dFUC*Qn zgqnM!Z2}g&P`a*4kdjwrE{iHgKn4^^$4H+r684XXpu;GAe$fxM98y6DDj$tKDx+8iAApjb^lsq@`o-d7)Eh|yA6hcFMdGUovURSDVY>oZPCl1$Q^MoP_686;zaiicVYxEr{ zD8|$!fjCbBNtEc6Od_$ZFx86A?c@zJE$5}Ib9~$dSGCmm)MOP7$sj_Q2w6llk)Amv z8Z;*E=#DHXC^8_#PH!I`VI=Gy5rONl7jBH&mFM?aSblcT%}Y`0qM)5v5{$btd)fEH zG{V(q+@1JkpMHKbkX^Jl#slxodefc`4{#Ot55*P}j#6M+nQ^$^b!G&+omZZ2IrNk1 z(W70>0la8;hyk{?gOUYqAzJL&+szt71)ISNf}iDZkG66z{t=Oc3D;-BWD^|8VN874 zPVL=r^VsxZXRet5&E~GMf1y&UP=K)iqyQ&DPf&qz+K(XYM7)zHP>F#(f^s37JqVv` z(3*Db=FaSxy}dVQSdPPS*qS3a+^mo0qGYRKn7}6N3Y=YTei@A3{m0YpCYMewRn86$ IUVS+K4Zq#t=Kufz diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 3d344a38c9..f917d58b71 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -55,7 +55,7 @@ enum UserText { } static func preferencesSubscriptionExpiredCaption(formattedDate: String) -> String { - let localized = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Your Privacy Pro subscription expired on %@.", comment: "Caption for the subscription preferences pane when the subscription has expired. The parameter is date of expiry.") + let localized = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Your Privacy Pro subscription expired on %@", comment: "Caption for the subscription preferences pane when the subscription has expired. The parameter is date of expiry.") return String(format: localized, formattedDate) } @@ -64,8 +64,8 @@ enum UserText { static let addToAnotherDeviceButton = NSLocalizedString("subscription.preferences.add.to.another.device.button", value: "Add to Another Device…", comment: "Button to add subscription to another device") static let manageSubscriptionButton = NSLocalizedString("subscription.preferences.manage.subscription.button", value: "Manage Subscription", comment: "Button to manage subscription") - static let changePlanOrBillingButton = NSLocalizedString("subscription.preferences.change.plan.or.billing.button", value: "Change Plan or Billing...", comment: "Button to add subscription to another device") - static let removeFromThisDeviceButton = NSLocalizedString("subscription.preferences.remove.from.this.device.button", value: "Remove From This Device...", comment: "Button to remove subscription from this device") + static let changePlanOrBillingButton = NSLocalizedString("subscription.preferences.change.plan.or.billing.button", value: "Change Plan or Billing…", comment: "Button to add subscription to another device") + static let removeFromThisDeviceButton = NSLocalizedString("subscription.preferences.remove.from.this.device.button", value: "Remove From This Device…", comment: "Button to remove subscription from this device") // MARK: Preferences when subscription is inactive static let preferencesSubscriptionInactiveHeader = NSLocalizedString("subscription.preferences.subscription.inactive.header", value: "Subscribe to Privacy Pro", comment: "Header for the subscription preferences pane when the subscription is inactive") @@ -81,6 +81,7 @@ enum UserText { // MARK: Preferences when subscription is expired static let preferencesSubscriptionExpiredCaption = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Subscribe again to continue using Privacy Pro.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") + static let viewPlansExpiredButtonTitle = NSLocalizedString("subscription.preferences.button.view.plans", value: "View Plans…", comment: "Button for viewing subscription plans on expired subscription") static let manageDevicesButton = NSLocalizedString("subscription.preferences.manage.devices.button", value: "Manage Devices", comment: "Button to manage devices") // MARK: - Change plan or billing dialogs From 74cee7579361a161203179872be15977e8fed55c Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Fri, 3 May 2024 19:53:17 -0500 Subject: [PATCH 074/134] Address tab feedback (#2705) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207132178310556/f Tech Design URL: CC: @ayoy **Description**: Ship review for "Address tab management feedback" **Steps to test this PR**: 1. --- ###### 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) --------- Co-authored-by: Dominik Kapusta --- DuckDuckGo.xcodeproj/project.pbxproj | 24 + DuckDuckGo/Common/Localizables/UserText.swift | 16 + .../Utilities/UserDefaultsWrapper.swift | 3 + DuckDuckGo/Localizable.xcstrings | 535 +++++++++++++++++- .../Preferences/Model/TabsPreferences.swift | 73 +++ .../View/PreferencesGeneralView.swift | 26 +- .../View/PreferencesRootView.swift | 1 + DuckDuckGo/Tab/Model/NewWindowPolicy.swift | 31 +- DuckDuckGo/Tab/Model/Tab+UIDelegate.swift | 8 +- DuckDuckGo/Tab/Model/Tab.swift | 11 +- .../TabExtensions/ContextMenuManager.swift | 20 +- .../Tab/View/BrowserTabViewController.swift | 5 +- DuckDuckGo/TabBar/Model/TabCollection.swift | 5 + .../TabBar/View/TabBarViewController.swift | 11 +- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 32 +- .../ViewModel/TabCollectionViewModel.swift | 37 +- DuckDuckGo/TabBar/ViewModel/TabIndex.swift | 9 + .../Preferences/TabsPreferencesTests.swift | 53 ++ UnitTests/TabBar/Model/TabIndexTests.swift | 7 + .../TabBar/View/MockTabViewItemDelegate.swift | 7 +- .../TabBar/View/TabBarViewItemTests.swift | 41 +- ...bCollectionViewModelTests+PinnedTabs.swift | 38 ++ ...wModelTests+WithoutPinnedTabsManager.swift | 32 ++ .../TabCollectionViewModelTests.swift | 97 ++++ 24 files changed, 1088 insertions(+), 34 deletions(-) create mode 100644 DuckDuckGo/Preferences/Model/TabsPreferences.swift create mode 100644 UnitTests/Preferences/TabsPreferencesTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4d460fa88..68f3b7a707 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -7,10 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 021EA0802BD2A9D500772C9A /* TabsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */; }; + 021EA0812BD2A9D500772C9A /* TabsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */; }; + 021EA0842BD6E01A00772C9A /* TabsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA0822BD6DF1B00772C9A /* TabsPreferencesTests.swift */; }; + 021EA0852BD6E0EB00772C9A /* TabsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA0822BD6DF1B00772C9A /* TabsPreferencesTests.swift */; }; 0230C0A3272080090018F728 /* KeyedCodingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */; }; 026ADE1426C3010C002518EE /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 028904202A7B25380028369C /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */; }; 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */; }; + 02C0737D2BE5B7E000BFE2F5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02C0737C2BE5B7E000BFE2F5 /* InfoPlist.xcstrings */; }; 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142879D924CE1179005419BB /* SuggestionViewModelTests.swift */; }; 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142879DB24CE1185005419BB /* SuggestionContainerViewModelTests.swift */; }; 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1430DFF424D0580F00B8978C /* TabBarViewController.swift */; }; @@ -2745,9 +2750,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsPreferences.swift; sourceTree = ""; }; + 021EA0822BD6DF1B00772C9A /* TabsPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsPreferencesTests.swift; sourceTree = ""; }; 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedCodingExtension.swift; sourceTree = ""; }; 026ADE1326C3010C002518EE /* macos-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "macos-config.json"; sourceTree = ""; }; 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProviderTests.swift; sourceTree = ""; }; + 02C0737C2BE5B7E000BFE2F5 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 142879D924CE1179005419BB /* SuggestionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionViewModelTests.swift; sourceTree = ""; }; 142879DB24CE1185005419BB /* SuggestionContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionContainerViewModelTests.swift; sourceTree = ""; }; 1430DFF424D0580F00B8978C /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; @@ -4773,6 +4781,7 @@ 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */, 37CD54C527F2FDD100F1F7B9 /* AboutModel.swift */, 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */, + 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */, ); path = Model; sourceTree = ""; @@ -4884,6 +4893,7 @@ 1D9FDEC22B9B63C90040B78C /* DataClearingPreferencesTests.swift */, 3714B1E628EDB7FA0056C57A /* DuckPlayerPreferencesTests.swift */, 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */, + 021EA0822BD6DF1B00772C9A /* TabsPreferencesTests.swift */, ); path = Preferences; sourceTree = ""; @@ -7914,6 +7924,7 @@ B6E6B9F42BA1FD90008AA7E1 /* sandbox-test-tool */ = { isa = PBXGroup; children = ( + 02C0737C2BE5B7E000BFE2F5 /* InfoPlist.xcstrings */, B6E6BA212BA2E4FB008AA7E1 /* Info.plist */, B6E6B9F52BA1FD90008AA7E1 /* SandboxTestTool.swift */, B6E6BA222BA2EDDE008AA7E1 /* FileReadResult.swift */, @@ -8618,6 +8629,7 @@ B6E6B9EF2BA1FD90008AA7E1 /* Sources */, B6E6B9F02BA1FD90008AA7E1 /* Frameworks */, B6AEB5532BA3029B00781A09 /* Cleanup entitlements */, + 02C0737E2BE5B7E000BFE2F5 /* Resources */, ); buildRules = ( ); @@ -8774,6 +8786,14 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 02C0737E2BE5B7E000BFE2F5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02C0737D2BE5B7E000BFE2F5 /* InfoPlist.xcstrings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3706FCB1293F65D500E42796 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9922,6 +9942,7 @@ 3706FC06293F65D500E42796 /* OnboardingViewModel.swift in Sources */, 3706FC07293F65D500E42796 /* ScriptSourceProviding.swift in Sources */, 31EF1E832B63FFCA00E6DB17 /* LoginItem+DataBrokerProtection.swift in Sources */, + 021EA0812BD2A9D500772C9A /* TabsPreferences.swift in Sources */, B6619EFC2B111CC600CD9186 /* InstructionsFormatParser.swift in Sources */, 3706FC08293F65D500E42796 /* CoreDataBookmarkImporter.swift in Sources */, 3706FC09293F65D500E42796 /* SuggestionViewModel.swift in Sources */, @@ -10392,6 +10413,7 @@ C1E961F32B87B273001760E1 /* MockAutofillActionExecutor.swift in Sources */, 376E2D2729428353001CD31B /* BrokenSiteReportingReferenceTests.swift in Sources */, 3707C72F294B5D4F00682A9F /* WebViewTests.swift in Sources */, + 021EA0852BD6E0EB00772C9A /* TabsPreferencesTests.swift in Sources */, 5682C69429B79B57004DE3C8 /* TabBarViewItemTests.swift in Sources */, 3706FE77293F661700E42796 /* PreferencesSidebarModelTests.swift in Sources */, 3706FE78293F661700E42796 /* HistoryCoordinatingMock.swift in Sources */, @@ -11289,6 +11311,7 @@ B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, B6DA06E62913F39400225DE2 /* MenuItemSelectors.swift in Sources */, 85589E9A27BFE3C30038AD11 /* FaviconView.swift in Sources */, + 021EA0802BD2A9D500772C9A /* TabsPreferences.swift in Sources */, 85707F2C276A364E00DC0649 /* OnboardingFlow.swift in Sources */, 4BE65480271FCD4D008D1D63 /* PasswordManagementLoginModel.swift in Sources */, AA9FF95B24A1EFC20039E328 /* TabViewModel.swift in Sources */, @@ -11729,6 +11752,7 @@ 56D145EE29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, 4BB99D0F26FE1A84001E4761 /* ChromiumBookmarksReaderTests.swift in Sources */, 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, + 021EA0842BD6E01A00772C9A /* TabsPreferencesTests.swift in Sources */, 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */, 4BB99D1026FE1A84001E4761 /* FirefoxBookmarksReaderTests.swift in Sources */, 9FBD84702BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index f5e57f5a94..b6c361c836 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -204,6 +204,8 @@ struct UserText { static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") + static let closeAllOtherTabs = NSLocalizedString("close.all.other.tabs", value: "Close All Other Tabs", comment: "Menu item") + static let closeTabsToTheLeft = NSLocalizedString("close.tabs.to.the.left", value: "Close Tabs to the Left", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") static let openInNewWindow = NSLocalizedString("open.in.new.window", value: "Open in New Window", comment: "Menu item that opens the link in a new window") @@ -606,6 +608,20 @@ struct UserText { static let setHomePage = NSLocalizedString("preferences-homepage-set-homePage", value: "Set Homepage", comment: "Set Homepage dialog title") static let addressLabel = NSLocalizedString("preferences-homepage-address", value: "Address:", comment: "Homepage address field label") + static let tabs = NSLocalizedString("preferences-tabs.title", value: "Tabs", comment: "Title for tabs section in settings") + static let preferNewTabsToWindows = NSLocalizedString("preferences-tabs.prefer.new.tabs.to.windows", value: "Open links in new tabs instead of new windows whenever possible", comment: "Option to prefer opening new tabs instead of windows when opening links") + static let switchToNewTabWhenOpened = NSLocalizedString("preferences-tabs.switch.tab.when.opened", value: "When opening links, switch to the new tab or window immediately", comment: "Option to switch to a new tab/window when it is opened") + static let newTabPositionTitle = NSLocalizedString("preferences-tabs.new.tab.position.title", value: "When creating a new tab", comment: "Title for new tab positioning") + + static func newTabPositionMode(for position: NewTabPosition) -> String { + switch position { + case .atEnd: + return NSLocalizedString("context.menu.new.tab.mode.at.end", value: "Add to the right of other tabs", comment: "Preferences > Tabs > At end of list") + case .nextToCurrent: + return NSLocalizedString("context.menu.new.tab.mode.next.to.current", value: "Add to the right of the current tab", comment: "Preferences > Tabs > Next to current tab") + } + } + static func homeButtonMode(for position: HomeButtonPosition) -> String { switch position { case .hidden: diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 92e2f23e0b..5891b891a5 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -89,6 +89,9 @@ public struct UserDefaultsWrapper { case currentThemeName = "com.duckduckgo.macos.currentThemeNameKey" case showFullURL = "preferences.appearance.show-full-url" case showAutocompleteSuggestions = "preferences.appearance.show-autocomplete-suggestions" + case preferNewTabsToWindows = "preferences.tabs.prefer-new-tabs-to-windows" + case switchToNewTabWhenOpened = "preferences.tabs.switch-to-new-tab-when-opened" + case newTabPosition = "preferences.tabs.new-tab-position" case defaultPageZoom = "preferences.appearance.default-page-zoom" case bookmarksBarAppearance = "preferences.appearance.bookmarks-bar" diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index b226cf84c7..71b681ff31 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -11469,6 +11469,66 @@ } } }, + "close.all.other.tabs" : { + "comment" : "Menu item", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle anderen Tabs schließen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close All Other Tabs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar todas las demás pestañas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer tous les autres onglets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudi tutte le altre schede" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle andere tabbladen sluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zamknij wszystkie inne karty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar todos os outros separadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть все остальные вкладки" + } + } + } + }, "close.other.tabs" : { "comment" : "Menu item", "extractionState" : "extracted_with_value", @@ -11709,6 +11769,66 @@ } } }, + "close.tabs.to.the.left" : { + "comment" : "Menu item", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs auf der linken Seite schließen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close Tabs to the Left" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar las pestañas a la izquierda" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer les onglets à gauche" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudi le schede a sinistra" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen links sluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zamknij karty po lewej stronie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar separadores à esquerda" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть вкладки слева" + } + } + } + }, "close.tabs.to.the.right" : { "comment" : "Menu item", "extractionState" : "extracted_with_value", @@ -12002,6 +12122,126 @@ } } }, + "context.menu.new.tab.mode.at.end" : { + "comment" : "Preferences > Tabs > At end of list", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rechts neben anderen Tabs hinzufügen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add to the right of other tabs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir a la derecha de otras pestañas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter à droite des autres onglets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi a destra di altre schede" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toevoegen aan de rechterkant van andere tabbladen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj po prawej stronie innych kart" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar à direita dos outros separadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить справа от остальных вкладок" + } + } + } + }, + "context.menu.new.tab.mode.next.to.current" : { + "comment" : "Preferences > Tabs > Next to current tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rechts neben dem aktuellen Tab hinzufügen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add to the right of the current tab" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir a la derecha de la pestaña actual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter à droite de l'onglet actuel" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi a destra della scheda attuale" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toevoegen aan de rechterkant van het huidige tabblad" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj po prawej stronie bieżącej karty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar à direita do separador atual" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить справа от текущей вкладки" + } + } + } + }, "copy" : { "comment" : "Copy button", "extractionState" : "extracted_with_value", @@ -43652,6 +43892,246 @@ } } }, + "preferences-tabs.new.tab.position.title" : { + "comment" : "Title for new tab positioning", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beim Erstellen eines neuen Tabs" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "When creating a new tab" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al crear una nueva pestaña" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lors de la création d'un nouvel onglet" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quando si crea una nuova scheda" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bij het maken van een nieuw tabblad" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podczas tworzenia nowej karty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ao criar um novo separador" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "При создании новой вкладки" + } + } + } + }, + "preferences-tabs.prefer.new.tabs.to.windows" : { + "comment" : "Option to prefer opening new tabs instead of windows when opening links", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links nach Möglichkeit in neuen Tabs statt in neuen Fenstern öffnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open links in new tabs instead of new windows whenever possible" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir enlaces en nuevas pestañas en lugar de nuevas ventanas siempre que sea posible" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les liens dans de nouveaux onglets plutôt que dans de nouvelles fenêtres si possible" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri i collegamenti in nuove schede anziché in nuove finestre quando possibile" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links waar mogelijk openen in nieuwe tabbladen in plaats van in nieuwe vensters" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwieraj linki w nowych kartach zamiast w nowych oknach, gdy tylko jest to możliwe" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir links em separadores novos em vez de novas janelas sempre que possível" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "По возможности открывать ссылки в новых вкладках, а не окнах" + } + } + } + }, + "preferences-tabs.switch.tab.when.opened" : { + "comment" : "Option to switch to a new tab/window when it is opened", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beim Öffnen von Links sofort zu einem neuen Tab oder Fenster wechseln" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "When opening links, switch to the new tab or window immediately" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al abrir enlaces, cambiar a la nueva pestaña o ventana inmediatamente" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "À l'ouverture des liens, basculer immédiatement sur le nouvel onglet ou la nouvelle fenêtre" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quando apri i collegamenti, passa immediatamente alla nuova scheda o finestra" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meteen overschakelen naar het nieuwe tabblad of venster bij het openen van links" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwierając linki, natychmiast przełącz na nową kartę lub nowe okno" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ao abrir links, mudar imediatamente para o novo separador ou janela" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "При открытии ссылки сразу переключаться на ее вкладку или окно" + } + } + } + }, + "preferences-tabs.title" : { + "comment" : "Title for tabs section in settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabs" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Tabs" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pestañas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onglets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schede" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tabbladen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Separadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вкладки" + } + } + } + }, "preferences.about" : { "comment" : "Title of the option to show the About screen", "extractionState" : "extracted_with_value", @@ -46533,7 +47013,56 @@ } }, "Privacy Pro" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Pro" + } + } + } }, "private.search.explenation" : { "comment" : "feature explanation in settings", @@ -50814,7 +51343,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ohne Titel" + "value" : "Senza titolo" } }, "nl" : { @@ -54335,4 +54864,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/Preferences/Model/TabsPreferences.swift b/DuckDuckGo/Preferences/Model/TabsPreferences.swift new file mode 100644 index 0000000000..f407cc2db1 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/TabsPreferences.swift @@ -0,0 +1,73 @@ +// +// TabsPreferences.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 TabsPreferencesPersistor { + var switchToNewTabWhenOpened: Bool { get set } + var preferNewTabsToWindows: Bool { get set } + var newTabPosition: NewTabPosition { get set } +} + +struct TabsPreferencesUserDefaultsPersistor: TabsPreferencesPersistor { + @UserDefaultsWrapper(key: .preferNewTabsToWindows, defaultValue: true) + var preferNewTabsToWindows: Bool + + @UserDefaultsWrapper(key: .switchToNewTabWhenOpened, defaultValue: false) + var switchToNewTabWhenOpened: Bool + + @UserDefaultsWrapper(key: .newTabPosition, defaultValue: .atEnd) + var newTabPosition: NewTabPosition +} + +final class TabsPreferences: ObservableObject, PreferencesTabOpening { + + static let shared = TabsPreferences() + + @Published var preferNewTabsToWindows: Bool { + didSet { + persistor.preferNewTabsToWindows = preferNewTabsToWindows + } + } + + @Published var switchToNewTabWhenOpened: Bool { + didSet { + persistor.switchToNewTabWhenOpened = switchToNewTabWhenOpened + } + } + + @Published var newTabPosition: NewTabPosition { + didSet { + persistor.newTabPosition = newTabPosition + } + } + + init(persistor: TabsPreferencesPersistor = TabsPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + preferNewTabsToWindows = persistor.preferNewTabsToWindows + switchToNewTabWhenOpened = persistor.switchToNewTabWhenOpened + newTabPosition = persistor.newTabPosition + } + + private var persistor: TabsPreferencesPersistor +} + +enum NewTabPosition: String, CaseIterable { + case atEnd + case nextToCurrent +} diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 7d22bc8867..41e92b6886 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -28,6 +28,7 @@ extension Preferences { @ObservedObject var startupModel: StartupPreferences @ObservedObject var downloadsModel: DownloadsPreferences @ObservedObject var searchModel: SearchPreferences + @ObservedObject var tabsModel: TabsPreferences @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false @@ -60,7 +61,26 @@ extension Preferences { } } - // SECTION 2: Home Page + // SECTION 2: Tabs + PreferencePaneSection(UserText.tabs) { + PreferencePaneSubSection { + ToggleMenuItem(UserText.preferNewTabsToWindows, isOn: $tabsModel.preferNewTabsToWindows) + ToggleMenuItem(UserText.switchToNewTabWhenOpened, isOn: $tabsModel.switchToNewTabWhenOpened) + } + + PreferencePaneSubSection { + HStack { + Picker(UserText.newTabPositionTitle, selection: $tabsModel.newTabPosition) { + ForEach(NewTabPosition.allCases, id: \.self) { position in + Text(UserText.newTabPositionMode(for: position)).tag(position) + } + } + .fixedSize() + } + } + } + + // SECTION 3: Home Page PreferencePaneSection(UserText.homePage) { PreferencePaneSubSection { @@ -104,12 +124,12 @@ extension Preferences { CustomHomePageSheet(startupModel: startupModel, isSheetPresented: $showingCustomHomePageSheet) } - // SECTION 3: Search Settings + // SECTION 4: Search Settings PreferencePaneSection(UserText.privateSearch) { ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions).accessibilityIdentifier("PreferencesGeneralView.showAutocompleteSuggestions") } - // SECTION 4: Downloads + // SECTION 5: Downloads PreferencePaneSection(UserText.downloads) { PreferencePaneSubSection { ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 8cecf62db6..1f4fc03135 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -88,6 +88,7 @@ enum Preferences { GeneralView(startupModel: StartupPreferences.shared, downloadsModel: DownloadsPreferences.shared, searchModel: SearchPreferences.shared, + tabsModel: TabsPreferences.shared, dataClearingModel: DataClearingPreferences.shared) case .sync: SyncView() diff --git a/DuckDuckGo/Tab/Model/NewWindowPolicy.swift b/DuckDuckGo/Tab/Model/NewWindowPolicy.swift index ca9e575d29..80e237929e 100644 --- a/DuckDuckGo/Tab/Model/NewWindowPolicy.swift +++ b/DuckDuckGo/Tab/Model/NewWindowPolicy.swift @@ -20,14 +20,15 @@ import Foundation import WebKit enum NewWindowPolicy { - case tab(selected: Bool, burner: Bool) + case tab(selected: Bool, burner: Bool, contextMenuInitiated: Bool = false) case popup(origin: NSPoint?, size: NSSize?) case window(active: Bool, burner: Bool) - init(_ windowFeatures: WKWindowFeatures, shouldSelectNewTab: Bool = false, isBurner: Bool) { + init(_ windowFeatures: WKWindowFeatures, shouldSelectNewTab: Bool = false, isBurner: Bool, contextMenuInitiated: Bool = false) { if windowFeatures.toolbarsVisibility?.boolValue == true { self = .tab(selected: shouldSelectNewTab, - burner: isBurner) + burner: isBurner, + contextMenuInitiated: contextMenuInitiated) } else if windowFeatures.width != nil { self = .popup(origin: windowFeatures.origin, size: windowFeatures.size) @@ -37,7 +38,7 @@ enum NewWindowPolicy { // See https://app.asana.com/0/1177771139624306/1205690527704551/f. if #available(macOS 14.1, *), windowFeatures.statusBarVisibility == nil && windowFeatures.menuBarVisibility == nil { - self = .tab(selected: shouldSelectNewTab, burner: isBurner) + self = .tab(selected: shouldSelectNewTab, burner: isBurner, contextMenuInitiated: contextMenuInitiated) } else { self = .window(active: true, burner: isBurner) @@ -49,10 +50,30 @@ enum NewWindowPolicy { return false } var isSelectedTab: Bool { - if case .tab(selected: true, burner: _) = self { return true } + if case .tab(selected: true, burner: _, contextMenuInitiated: _) = self { return true } return false } + /** + * Replaces `.tab` with `.window` when user prefers windows over tabs. + */ + func preferringTabsToWindows(_ prefersTabsToWindows: Bool) -> NewWindowPolicy { + guard case .tab(_, let isBurner, contextMenuInitiated: false) = self, !prefersTabsToWindows else { + return self + } + return .window(active: true, burner: isBurner) + } + + /** + * Forces selecting a tab if `true` is passed as argument. + */ + func preferringSelectedTabs(_ prefersSelectedTabs: Bool) -> NewWindowPolicy { + guard case .tab(selected: false, burner: let isBurner, contextMenuInitiated: let contextMenuInitiated) = self, prefersSelectedTabs else { + return self + } + return .tab(selected: true, burner: isBurner, contextMenuInitiated: contextMenuInitiated) + } + } extension WKWindowFeatures { diff --git a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift index fd2e4e8349..81b12ca0b0 100644 --- a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift +++ b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift @@ -85,11 +85,12 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { windowFeatures: WKWindowFeatures, completionHandler: @escaping (WKWebView?) -> Void) { - switch newWindowPolicy(for: navigationAction) { + switch newWindowPolicy(for: navigationAction)?.preferringTabsToWindows(tabsPreferences.preferNewTabsToWindows) { // popup kind is known, action doesn‘t require Popup Permission case .allow(let targetKind): // proceed to web view creation - completionHandler(self.createWebView(from: webView, with: configuration, for: navigationAction, of: targetKind)) + completionHandler(self.createWebView(from: webView, with: configuration, + for: navigationAction, of: targetKind.preferringSelectedTabs(tabsPreferences.switchToNewTabWhenOpened))) return case .cancel: // navigation action was handled before and cancelled @@ -99,9 +100,10 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { break } - let shouldSelectNewTab = !NSApp.isCommandPressed // this is actually not correct, to be fixed later + let shouldSelectNewTab = !NSApp.isCommandPressed || tabsPreferences.switchToNewTabWhenOpened // this is actually not correct, to be fixed later // try to guess popup kind from provided windowFeatures let targetKind = NewWindowPolicy(windowFeatures, shouldSelectNewTab: shouldSelectNewTab, isBurner: burnerMode.isBurner) + .preferringTabsToWindows(tabsPreferences.preferNewTabsToWindows) // action doesn‘t require Popup Permission as it‘s user-initiated // TO BE FIXED: this also opens a new window when a popup ad is shown on click simultaneously with the main frame navigation: diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 031cbbe851..1fd8c17493 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -70,6 +70,7 @@ protocol NewWindowPolicyDecisionMaker { private let webViewConfiguration: WKWebViewConfiguration let startupPreferences: StartupPreferences + let tabsPreferences: TabsPreferences private var extensions: TabExtensions // accesing TabExtensions‘ Public Protocols projecting tab.extensions.extensionName to tab.extensionName @@ -108,7 +109,8 @@ protocol NewWindowPolicyDecisionMaker { webViewSize: CGSize = CGSize(width: 1024, height: 768), startupPreferences: StartupPreferences = StartupPreferences.shared, certificateTrustEvaluator: CertificateTrustEvaluating = CertificateTrustEvaluator(), - tunnelController: NetworkProtectionIPCTunnelController? = TunnelControllerProvider.shared.tunnelController + tunnelController: NetworkProtectionIPCTunnelController? = TunnelControllerProvider.shared.tunnelController, + tabsPreferences: TabsPreferences = TabsPreferences.shared ) { let duckPlayer = duckPlayer @@ -150,7 +152,8 @@ protocol NewWindowPolicyDecisionMaker { webViewSize: webViewSize, startupPreferences: startupPreferences, certificateTrustEvaluator: certificateTrustEvaluator, - tunnelController: tunnelController) + tunnelController: tunnelController, + tabsPreferences: tabsPreferences) } @MainActor @@ -183,7 +186,8 @@ protocol NewWindowPolicyDecisionMaker { webViewSize: CGSize, startupPreferences: StartupPreferences, certificateTrustEvaluator: CertificateTrustEvaluating, - tunnelController: NetworkProtectionIPCTunnelController? + tunnelController: NetworkProtectionIPCTunnelController?, + tabsPreferences: TabsPreferences ) { self.content = content @@ -200,6 +204,7 @@ protocol NewWindowPolicyDecisionMaker { self.interactionState = interactionStateData.map(InteractionState.loadCachedFromTabContent) ?? .none self.lastSelectedAt = lastSelectedAt self.startupPreferences = startupPreferences + self.tabsPreferences = tabsPreferences let configuration = webViewConfiguration ?? WKWebViewConfiguration() configuration.applyStandardConfiguration(contentBlocking: privacyFeatures.contentBlocking, diff --git a/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift b/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift index e966c8b7a0..57174a6ec7 100644 --- a/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift +++ b/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift @@ -24,6 +24,16 @@ import WebKit enum NavigationDecision { case allow(NewWindowPolicy) case cancel + + /** + * Replaces `.tab` with `.window` when user prefers windows over tabs. + */ + func preferringTabsToWindows(_ prefersTabsToWindows: Bool) -> NavigationDecision { + guard case .allow(let targetKind) = self, !prefersTabsToWindows else { + return self + } + return .allow(targetKind.preferringTabsToWindows(prefersTabsToWindows)) + } } @MainActor @@ -35,6 +45,8 @@ final class ContextMenuManager: NSObject { private var selectedText: String? private var linkURL: String? + private var tabsPreferences: TabsPreferences + private var isEmailAddress: Bool { guard let linkURL, let url = URL(string: linkURL) else { return false @@ -52,7 +64,9 @@ final class ContextMenuManager: NSObject { fileprivate weak var webView: WKWebView? @MainActor - init(contextMenuScriptPublisher: some Publisher) { + init(contextMenuScriptPublisher: some Publisher, + tabsPreferences: TabsPreferences = TabsPreferences.shared) { + self.tabsPreferences = tabsPreferences super.init() userScriptCancellable = contextMenuScriptPublisher.sink { [weak self] contextMenuScript in @@ -360,8 +374,8 @@ private extension ContextMenuManager { return } - onNewWindow = { _ in - .allow(.tab(selected: false, burner: burner)) + onNewWindow = { [weak self] _ in + .allow(.tab(selected: self?.tabsPreferences.switchToNewTabWhenOpened ?? false, burner: burner, contextMenuInitiated: true)) } NSApp.sendAction(action, to: originalItem.target, from: originalItem) } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 9c2f65619f..dde4a10a29 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -547,7 +547,8 @@ final class BrowserTabViewController: NSViewController { shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode, webViewSize: view.frame.size) - tabCollectionViewModel.append(tab: tab, selected: true) + + tabCollectionViewModel.insertOrAppend(tab: tab, selected: true) } // MARK: - Browser Tabs @@ -834,7 +835,7 @@ extension BrowserTabViewController: TabDelegate { case .window(active: let active, let isBurner): assert(isBurner == childTab.burnerMode.isBurner) WindowsManager.openNewWindow(with: childTab, showWindow: active) - case .tab(selected: let selected, _): + case .tab(selected: let selected, _, _): self.tabCollectionViewModel.insert(childTab, after: parentTab, selected: selected) } } diff --git a/DuckDuckGo/TabBar/Model/TabCollection.swift b/DuckDuckGo/TabBar/Model/TabCollection.swift index 94b632fa86..87c15fe1a6 100644 --- a/DuckDuckGo/TabBar/Model/TabCollection.swift +++ b/DuckDuckGo/TabBar/Model/TabCollection.swift @@ -78,6 +78,11 @@ final class TabCollection: NSObject { tabs = tab.map { [$0] } ?? [] } + func removeTabs(before index: Int) { + tabsWillClose(range: 0..() + private var tabsPreferences: TabsPreferences private var startupPreferences: StartupPreferences private var homePage: Tab.TabContent { var homePage: Tab.TabContent = .newtab @@ -117,12 +118,14 @@ final class TabCollectionViewModel: NSObject { selectionIndex: Int = 0, pinnedTabsManager: PinnedTabsManager?, burnerMode: BurnerMode = .regular, - startupPreferences: StartupPreferences = StartupPreferences.shared + startupPreferences: StartupPreferences = StartupPreferences.shared, + tabsPreferences: TabsPreferences = TabsPreferences.shared ) { self.tabCollection = tabCollection self.pinnedTabsManager = pinnedTabsManager self.burnerMode = burnerMode self.startupPreferences = startupPreferences + self.tabsPreferences = tabsPreferences super.init() subscribeToTabs() @@ -321,6 +324,10 @@ final class TabCollectionViewModel: NSObject { delegate?.tabCollectionViewModelDidMultipleChanges(self) } + func insertNewTab(after parentTab: Tab, with content: Tab.TabContent = .newtab, selected: Bool = true) { + insert(Tab(content: content, shouldLoadInBackground: true, burnerMode: burnerMode), after: parentTab, selected: selected) + } + func insert(_ tab: Tab, at index: TabIndex, selected: Bool = true) { guard changesEnabled else { return } guard let tabCollection = tabCollection(for: index) else { @@ -363,6 +370,18 @@ final class TabCollectionViewModel: NSObject { } } + func insertOrAppendNewTab(_ content: Tab.TabContent = .newtab, selected: Bool = true) { + insertOrAppend(tab: Tab(content: content, shouldLoadInBackground: true, burnerMode: burnerMode), selected: selected) + } + + func insertOrAppend(tab: Tab, selected: Bool) { + if tabsPreferences.newTabPosition == .nextToCurrent, let selectionIndex { + self.insert(tab, at: selectionIndex.makeNextUnpinned(), selected: selected) + } else { + append(tab: tab, selected: selected) + } + } + // MARK: - Removal func removeAll(with content: Tab.TabContent) { @@ -489,6 +508,22 @@ final class TabCollectionViewModel: NSObject { delegate?.tabCollectionViewModelDidMultipleChanges(self) } + func removeTabs(before index: Int) { + guard changesEnabled else { return } + + tabCollection.removeTabs(before: index) + + if let currentSelection = selectionIndex, currentSelection.isUnpinnedTab { + if currentSelection.item < index { + selectionIndex = .unpinned(0) + } else { + selectionIndex = .unpinned(currentSelection.item - index) + } + } + + delegate?.tabCollectionViewModelDidMultipleChanges(self) + } + func removeTabs(after index: Int) { guard changesEnabled else { return } diff --git a/DuckDuckGo/TabBar/ViewModel/TabIndex.swift b/DuckDuckGo/TabBar/ViewModel/TabIndex.swift index c55bd59a7f..7a2675d1bb 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabIndex.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabIndex.swift @@ -64,6 +64,15 @@ enum TabIndex: Equatable, Comparable { } } + func makeNextUnpinned() -> TabIndex { + switch self { + case .pinned: + return .unpinned(0) + case let .unpinned(index): + return .unpinned(index + 1) + } + } + func isInSameSection(as other: TabIndex) -> Bool { switch (self, other) { case (.pinned, .unpinned), (.unpinned, .pinned): diff --git a/UnitTests/Preferences/TabsPreferencesTests.swift b/UnitTests/Preferences/TabsPreferencesTests.swift new file mode 100644 index 0000000000..4faab128cc --- /dev/null +++ b/UnitTests/Preferences/TabsPreferencesTests.swift @@ -0,0 +1,53 @@ +// +// TabsPreferencesTests.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 +@testable import DuckDuckGo_Privacy_Browser + +class MockTabsPreferencesPersistor: TabsPreferencesPersistor { + var preferNewTabsToWindows: Bool = false + var switchToNewTabWhenOpened: Bool = false + var newTabPosition: NewTabPosition = .atEnd +} + +final class TabsPreferencesTests: XCTestCase { + + func testWhenInitializedThenItLoadsPersistedValues() { + let mockPersistor = MockTabsPreferencesPersistor() + mockPersistor.preferNewTabsToWindows = true + mockPersistor.switchToNewTabWhenOpened = true + mockPersistor.newTabPosition = .nextToCurrent + let tabsPreferences = TabsPreferences(persistor: mockPersistor) + + XCTAssertTrue(tabsPreferences.preferNewTabsToWindows) + XCTAssertTrue(tabsPreferences.switchToNewTabWhenOpened) + XCTAssertEqual(tabsPreferences.newTabPosition, .nextToCurrent) + } + + func testWhenPreferencesUpdatedThenPersistorUpdates() { + let mockPersistor = MockTabsPreferencesPersistor() + let tabsPreferences = TabsPreferences(persistor: mockPersistor) + tabsPreferences.preferNewTabsToWindows = true + tabsPreferences.switchToNewTabWhenOpened = true + tabsPreferences.newTabPosition = .nextToCurrent + + XCTAssertTrue(mockPersistor.preferNewTabsToWindows) + XCTAssertTrue(mockPersistor.switchToNewTabWhenOpened) + XCTAssertEqual(mockPersistor.newTabPosition, .nextToCurrent) + } +} diff --git a/UnitTests/TabBar/Model/TabIndexTests.swift b/UnitTests/TabBar/Model/TabIndexTests.swift index 662eb70f99..2402d42eed 100644 --- a/UnitTests/TabBar/Model/TabIndexTests.swift +++ b/UnitTests/TabBar/Model/TabIndexTests.swift @@ -51,6 +51,13 @@ final class TabIndexTests: XCTestCase { XCTAssertEqual(TabIndex.pinned(16).makeNext(), TabIndex.pinned(17)) } + func testMakeNextUnpinned() { + XCTAssertEqual(TabIndex.unpinned(0).makeNextUnpinned(), TabIndex.unpinned(1)) + XCTAssertEqual(TabIndex.unpinned(41).makeNextUnpinned(), TabIndex.unpinned(42)) + XCTAssertEqual(TabIndex.pinned(0).makeNextUnpinned(), TabIndex.unpinned(0)) + XCTAssertEqual(TabIndex.pinned(2).makeNextUnpinned(), TabIndex.unpinned(0)) + } + func testWhenViewModelHasNoPinnedTabsThenFirstTabIsUnpinned() { let tabCollectionViewModel = TabCollectionViewModel( tabCollection: tabCollection(tabsCount: 1), diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9157000830..63b4a7fc97 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -24,6 +24,7 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { var mockedCurrentTab: Tab? var canBookmarkAllOpenTabs = false + var hasItemsToTheLeft = false var hasItemsToTheRight = false var audioState: WKWebView.AudioState? @@ -45,6 +46,10 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemCloseToTheLeftAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { + + } + func tabBarViewItemCloseToTheRightAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } @@ -106,7 +111,7 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { - OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) + OtherTabBarViewItemsState(hasItemsToTheLeft: hasItemsToTheLeft, hasItemsToTheRight: hasItemsToTheRight) } func clear() { diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 821ca0b5cc..19505467d6 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -52,8 +52,16 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTab) XCTAssertEqual(menu.item(at: 9)?.title, UserText.closeOtherTabs) - XCTAssertEqual(menu.item(at: 10)?.title, UserText.closeTabsToTheRight) - XCTAssertEqual(menu.item(at: 11)?.title, UserText.moveTabToNewWindow) + XCTAssertEqual(menu.item(at: 10)?.title, UserText.moveTabToNewWindow) + + // Check "Close Other Tabs" submenu + guard let submenu = menu.item(at: 9)?.submenu else { + XCTFail("\"Close Other Tabs\" menu item should have a submenu") + return + } + XCTAssertEqual(submenu.item(at: 0)?.title, UserText.closeTabsToTheLeft) + XCTAssertEqual(submenu.item(at: 1)?.title, UserText.closeTabsToTheRight) + XCTAssertEqual(submenu.item(at: 2)?.title, UserText.closeAllOtherTabs) } func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { @@ -77,7 +85,8 @@ final class TabBarViewItemTests: XCTestCase { func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) - let item = menu.items .first { $0.title == UserText.closeOtherTabs } + let submenu = menu.items .first { $0.title == UserText.closeOtherTabs } + let item = submenu?.submenu?.items .first { $0.title == UserText.closeAllOtherTabs } XCTAssertFalse(item?.isEnabled ?? true) } @@ -85,7 +94,8 @@ final class TabBarViewItemTests: XCTestCase { delegate.hasItemsToTheRight = true tabBarViewItem.menuNeedsUpdate(menu) - let item = menu.items .first { $0.title == UserText.closeOtherTabs } + let submenu = menu.items .first { $0.title == UserText.closeOtherTabs } + let item = submenu?.submenu?.items .first { $0.title == UserText.closeAllOtherTabs } XCTAssertTrue(item?.isEnabled ?? false) } @@ -104,10 +114,28 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertTrue(item?.isEnabled ?? false) } + func testWhenNoTabsToTheLeftThenCloseTabsToTheLeftIsDisabled() { + tabBarViewItem.menuNeedsUpdate(menu) + + let submenu = menu.items .first { $0.title == UserText.closeOtherTabs } + let item = submenu?.submenu?.items .first { $0.title == UserText.closeTabsToTheLeft } + XCTAssertFalse(item?.isEnabled ?? true) + } + + func testWhenTabsToTheLeftThenCloseTabsToTheLeftIsEnabled() { + delegate.hasItemsToTheLeft = true + tabBarViewItem.menuNeedsUpdate(menu) + + let submenu = menu.items .first { $0.title == UserText.closeOtherTabs } + let item = submenu?.submenu?.items .first { $0.title == UserText.closeTabsToTheLeft } + XCTAssertTrue(item?.isEnabled ?? false) + } + func testWhenNoTabsToTheRightThenCloseTabsToTheRightIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) - let item = menu.items .first { $0.title == UserText.closeTabsToTheRight } + let submenu = menu.items .first { $0.title == UserText.closeOtherTabs } + let item = submenu?.submenu?.items .first { $0.title == UserText.closeTabsToTheRight } XCTAssertFalse(item?.isEnabled ?? true) } @@ -115,7 +143,8 @@ final class TabBarViewItemTests: XCTestCase { delegate.hasItemsToTheRight = true tabBarViewItem.menuNeedsUpdate(menu) - let item = menu.items .first { $0.title == UserText.closeTabsToTheRight } + let submenu = menu.items .first { $0.title == UserText.closeOtherTabs } + let item = submenu?.submenu?.items .first { $0.title == UserText.closeTabsToTheRight } XCTAssertTrue(item?.isEnabled ?? false) } diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift index 7a8394baef..07932afb14 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift @@ -110,6 +110,16 @@ extension TabCollectionViewModelTests { // MARK: - Insert + func test_WithPinnedTabsManager_WhenInsertNewTabIsCalledThenNewTabIsAlsoSelected() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModelWithPinnedTab() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(0)) + XCTAssertNotNil(tabCollectionViewModel.selectedTabViewModel) + tabCollectionViewModel.insertNewTab(after: tabCollectionViewModel.selectedTabViewModel!.tab) + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) + } + func test_WithPinnedTabs_WhenInsertChildOfPinnedTabAndNoOtherChildTabIsNearParent_ThenTabIsInsertedAsFirstUnpinnedTab() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModelWithPinnedTab() @@ -138,6 +148,34 @@ extension TabCollectionViewModelTests { XCTAssertIdentical(tab, tabCollectionViewModel.tabViewModel(at: 3)?.tab) } + // MARK: - Insert or Append + + func test_WithPinnedTabs_WhenInsertOrAppendCalledPreferencesAreRespected() { + let persistor = MockTabsPreferencesPersistor() + var tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(), pinnedTabsManager: PinnedTabsManager(), + tabsPreferences: TabsPreferences(persistor: persistor)) + tabCollectionViewModel.appendPinnedTab() + + let index = tabCollectionViewModel.tabCollection.tabs.count + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: index)) + + persistor.newTabPosition = .nextToCurrent + tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(), pinnedTabsManager: PinnedTabsManager(), + tabsPreferences: TabsPreferences(persistor: persistor)) + tabCollectionViewModel.appendPinnedTab() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(0)) + XCTAssertNotNil(tabCollectionViewModel.selectedTabViewModel) + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) + + tabCollectionViewModel.select(at: .pinned(0)) + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 0)) + } + // MARK: - Remove func test_WithPinnedTabs_WhenRemoveIsCalledWithIndexOutOfBoundsThenNoTabIsRemoved() { diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift index 875c7746a4..2319fbaf17 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift @@ -231,6 +231,16 @@ extension TabCollectionViewModelTests { // MARK: - Insert + func test_WithoutPinnedTabsManager_WhenInsertNewTabIsCalledThenNewTabIsAlsoSelected() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(0)) + XCTAssertNotNil(tabCollectionViewModel.selectedTabViewModel) + tabCollectionViewModel.insertNewTab(after: tabCollectionViewModel.selectedTabViewModel!.tab) + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) + } + func test_WithoutPinnedTabsManager_WhenInsertChildAndParentIsntPartOfTheTabCollection_ThenNoChildIsInserted() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() @@ -284,6 +294,28 @@ extension TabCollectionViewModelTests { XCTAssert(tab === tabCollectionViewModel.tabViewModel(at: 2)?.tab) } + // MARK: - Insert or Append + + func test_WithoutPinnedTabsManager_WhenInsertOrAppendCalledPreferencesAreRespected() { + let persistor = MockTabsPreferencesPersistor() + var tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(), pinnedTabsManager: nil, + tabsPreferences: TabsPreferences(persistor: persistor)) + + let index = tabCollectionViewModel.tabCollection.tabs.count + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: index)) + + persistor.newTabPosition = .nextToCurrent + tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(), pinnedTabsManager: nil, + tabsPreferences: TabsPreferences(persistor: persistor)) + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(0)) + XCTAssertNotNil(tabCollectionViewModel.selectedTabViewModel) + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) + } + // MARK: - Remove func test_WithoutPinnedTabsManager_WhenRemoveIsCalledWithIndexOutOfBoundsThenNoTabIsRemoved() { diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift index 3415fd4439..5401520d36 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift @@ -222,6 +222,16 @@ final class TabCollectionViewModelTests: XCTestCase { // MARK: - Insert + func testWhenInsertNewTabIsCalledThenNewTabIsAlsoSelected() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(0)) + XCTAssertNotNil(tabCollectionViewModel.selectedTabViewModel) + tabCollectionViewModel.insertNewTab(after: tabCollectionViewModel.selectedTabViewModel!.tab) + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) + } + func testWhenInsertChildAndParentIsntPartOfTheTabCollection_ThenNoChildIsInserted() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() @@ -275,6 +285,28 @@ final class TabCollectionViewModelTests: XCTestCase { XCTAssert(tab === tabCollectionViewModel.tabViewModel(at: 2)?.tab) } + // MARK: - Insert or Append + + func testWhenInsertOrAppendCalledPreferencesAreRespected() { + let persistor = MockTabsPreferencesPersistor() + var tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(), pinnedTabsManager: PinnedTabsManager(), + tabsPreferences: TabsPreferences(persistor: persistor)) + + let index = tabCollectionViewModel.tabCollection.tabs.count + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: index)) + + persistor.newTabPosition = .nextToCurrent + tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(), pinnedTabsManager: PinnedTabsManager(), + tabsPreferences: TabsPreferences(persistor: persistor)) + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(0)) + XCTAssertNotNil(tabCollectionViewModel.selectedTabViewModel) + tabCollectionViewModel.insertOrAppendNewTab() + XCTAssert(tabCollectionViewModel.selectedTabViewModel === tabCollectionViewModel.tabViewModel(at: 1)) + } + // MARK: - Remove func testWhenRemoveIsCalledWithIndexOutOfBoundsThenNoTabIsRemoved() { @@ -318,6 +350,71 @@ final class TabCollectionViewModelTests: XCTestCase { XCTAssertEqual(firstTab, tabCollectionViewModel.selectedTabViewModel?.tab) } + func testWhenTabsToTheLeftAreRemovedAndSelectionIsRemoved_ThenSelectionIsCorrectlyUpdated() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + + tabCollectionViewModel.select(at: .unpinned(1)) + tabCollectionViewModel.removeTabs(before: 2) + + XCTAssertEqual(tabCollectionViewModel.selectionIndex?.item, 0) + } + + func testWhenTabsToTheLeftAreRemovedAndSelectionRemains_ThenSelectionIsCorrectlyUpdated() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(3)) + tabCollectionViewModel.removeTabs(before: 3) + + XCTAssertEqual(tabCollectionViewModel.selectionIndex?.item, 0) + } + + func testWhenTabsToTheLeftAreRemovedAndSelectionRemainsAndIsToTheRight_ThenSelectionIsCorrectlyUpdated() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(4)) + tabCollectionViewModel.removeTabs(before: 2) + + XCTAssertEqual(tabCollectionViewModel.selectionIndex?.item, 2) + } + + func testWhenTabsToTheRightAreRemovedAndSelectionIsRemoved_ThenSelectionIsCorrectlyUpdated() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + + tabCollectionViewModel.select(at: .unpinned(1)) + tabCollectionViewModel.removeTabs(after: 0) + + XCTAssertEqual(tabCollectionViewModel.selectionIndex?.item, 0) + } + + func testWhenTabsToTheRightAreRemovedAndSelectionRemains_ThenSelectionIsCorrectlyUpdated() { + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() + + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.appendNewTab() + tabCollectionViewModel.select(at: .unpinned(1)) + tabCollectionViewModel.removeTabs(after: 1) + + XCTAssertEqual(tabCollectionViewModel.selectionIndex?.item, 1) + } + func testWhenLastTabIsRemoved_ThenSelectionIsNil() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() From be32a7ac5239099b3bcc24080c4b68ff4ed7ebb2 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Sun, 5 May 2024 22:45:42 +0200 Subject: [PATCH 075/134] Enable field validation for Sync payloads (#2725) Task/Issue URL: https://app.asana.com/0/0/1207196051122978/f Description: This change adds a mechanism that filters out syncable objects that would fail validation on the backend before sending Sync patch request. Objects rejected from patch payload are retried on every subsequent Sync request, until they're updated to pass validation or deleted. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../Preferences/Model/SyncPreferences.swift | 20 +++++++++---------- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 68f3b7a707..dc88f48c10 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12772,7 +12772,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 143.0.1; + version = 144.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 70b5f6cd13..41fa2b0e67 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" : "dedd5ef4cc71379e43cbd8244374a53043163e21", - "version" : "143.0.1" + "revision" : "9906b9464f6f12e94f3cc62456b5b5a9c1a43db8", + "version" : "144.0.0" } }, { diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index a0a5591360..7781247f6c 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -172,17 +172,15 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { } .store(in: &cancellables) - if DDGSync.isFieldValidationEnabled { - syncService.isSyncInProgressPublisher - .removeDuplicates() - .filter { !$0 } - .asVoid() - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.updateInvalidObjects() - } - .store(in: &cancellables) - } + syncService.isSyncInProgressPublisher + .removeDuplicates() + .filter { !$0 } + .asVoid() + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.updateInvalidObjects() + } + .store(in: &cancellables) $syncErrorMessage .map { $0 != nil } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index dbbe4f1df5..19b8daa1e2 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: "143.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 73fbc8b7ce..a027d241cb 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "143.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 7c483d7bd5..7eb918dbc2 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: "143.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From ecb266299bb910e2645416a72a12b02b72091b0e Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Mon, 6 May 2024 10:39:21 +0100 Subject: [PATCH 076/134] Delegate SecureVault KeyStore Events in All Scenarios (#2744) Task/Issue URL: https://app.asana.com/0/0/1207236816134198/f **Description**: In all cases where an intermediate is used to communicate with SecureVaultReporter, delegate SecureVault events --- DuckDuckGo/Autofill/ContentOverlayViewController.swift | 4 ++++ DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 5bfdf2ca85..63a6a88368 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -313,6 +313,10 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { SecureVaultReporter.shared.secureVaultError(error) } + public func secureVaultKeyStoreEvent(_ event: SecureStorageKeyStoreEvent) { + SecureVaultReporter.shared.secureVaultKeyStoreEvent(event) + } + public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { if pixel.isEmailPixel { let emailParameters = self.emailManager.emailPixelParameters diff --git a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift index 328b447ea3..c881ebc2c1 100644 --- a/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/AutofillTabExtension.swift @@ -167,6 +167,10 @@ extension AutofillTabExtension: SecureVaultManagerDelegate { SecureVaultReporter.shared.secureVaultError(error) } + func secureVaultKeyStoreEvent(_ event: SecureStorageKeyStoreEvent) { + SecureVaultReporter.shared.secureVaultKeyStoreEvent(event) + } + public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { PixelKit.fire(GeneralPixel.jsPixel(pixel)) } From 799474e1b9c33d575a842954cb7703841082a1d2 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 6 May 2024 13:43:35 +0000 Subject: [PATCH 077/134] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 67 ++++++++++++++++--- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 935d103d42..224457323d 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"3471e126687b3688c3512627a8fde0a1\"" - public static let embeddedDataSHA = "a49fcdf77320604568abb7c69704d1fd93069b952cf1e2f6f3ffcf13020172ec" + public static let embeddedDataETag = "\"6cbc7738304e5eaef20e98417f412fc9\"" + public static let embeddedDataSHA = "0fda36c6cb3a0f2bcd34562ad9d4bc8fe7dc2d21e029b87e323cb4992bfddb01" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index af01839109..cce44b20c4 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1714486769720, + "version": 1714746648846, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -294,6 +294,9 @@ { "domain": "www.michelinman.com" }, + { + "domain": "malmostadsteater.se" + }, { "domain": "marvel.com" }, @@ -309,19 +312,16 @@ ] }, "state": "enabled", - "hash": "bca76e26434e161140776de92c03cb5f" + "hash": "7096d80d39c6c9a98d98ff6bde5644e0" }, "autofill": { "exceptions": [ { "domain": "roll20.net" - }, - { - "domain": "rumble.com" } ], "state": "enabled", - "hash": "5bb5c0fc0d462da4674e4ca07dbc9363" + "hash": "b1ae580a636957bcbe90d16aa92c40bd" }, "clickToLoad": { "exceptions": [ @@ -4898,7 +4898,7 @@ "surveyCardDay7": "disabled", "surveyCardDay14": "disabled", "permanentSurvey": { - "state": "internal", + "state": "enabled", "localization": "disabled", "url": "https://selfserve.decipherinc.com/survey/selfserve/32ab/240404?list=2", "firstDay": 5, @@ -4906,7 +4906,7 @@ "sharePercentage": 60 } }, - "hash": "7f7445d021268ef854b20022d0fb48e6" + "hash": "8a5c9011216219c6576ec362acb51fd0" }, "nonTracking3pCookies": { "settings": { @@ -5850,6 +5850,16 @@ } ] }, + "curalate.com": { + "rules": [ + { + "rule": "edge.curalate.com/sites/", + "domains": [ + "" + ] + } + ] + }, "cxense.com": { "rules": [ { @@ -6177,6 +6187,24 @@ }, "facebook.com": { "rules": [ + { + "rule": "facebook.com/plugins/customer_chat/", + "domains": [ + "danibowman.com" + ] + }, + { + "rule": "facebook.com/v6.0/plugins/customer_chat/", + "domains": [ + "danibowman.com" + ] + }, + { + "rule": "facebook.com/v6.0/plugins/customerchat.php", + "domains": [ + "danibowman.com" + ] + }, { "rule": "facebook.com", "domains": [ @@ -6202,6 +6230,12 @@ "nordicwellness.se" ] }, + { + "rule": "connect.facebook.net/en_US/sdk/xfbml.customerchat.js", + "domains": [ + "danibowman.com" + ] + }, { "rule": "connect.facebook.net/en_UK/sdk.js", "domains": [ @@ -6770,6 +6804,12 @@ "domains": [ "" ] + }, + { + "rule": "api.hubspot.com/livechat-public/v1/message/public", + "domains": [ + "" + ] } ] }, @@ -7170,8 +7210,7 @@ { "rule": "connect.nosto.com/script/shopify/nosto.js", "domains": [ - "oneill.com", - "thefryecompany.com" + "" ] } ] @@ -7221,6 +7260,12 @@ "cigna.com" ] }, + { + "rule": "altriagroupinc.tt.omtrdc.net/rest/v1/delivery", + "domains": [ + "marlboro.com" + ] + }, { "rule": "omtrdc.net", "domains": [ @@ -8300,7 +8345,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "e21e28c6597b4ac6ab6c03bdf359912e" + "hash": "8abdf819c66bc3e2391e900958ed0294" }, "trackingCookies1p": { "settings": { From 04c2aad5b429289c917f7e81792a82e474bee787 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 6 May 2024 13:43:35 +0000 Subject: [PATCH 078/134] Set marketing version to 1.87.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index c23c40bdc0..6b9d59ea7d 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.86.0 +MARKETING_VERSION = 1.87.0 From 3efddf304ce7fe01a1226df14466796b30bec967 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 6 May 2024 13:53:48 +0000 Subject: [PATCH 079/134] Bump version to 1.87.0 (183) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 3d9e7b5269..e5253b1076 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 182 +CURRENT_PROJECT_VERSION = 183 From 6fc3f179475931e49d39a02965eead22fc659e8c Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 May 2024 12:57:54 +0200 Subject: [PATCH 080/134] Add Autocomplete pixels (#2746) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207226295114884/f Description: This change adds a bunch of pixels related to selecting suggestions in the address bar. --- .../View/AddressBarTextField.swift | 23 ++++++++++++++++++- .../Preferences/Model/SearchPreferences.swift | 2 ++ DuckDuckGo/Statistics/GeneralPixel.swift | 18 +++++++++++++++ .../Preferences/SearchPreferencesTests.swift | 11 +++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 25cc50a039..8248039994 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -17,13 +17,15 @@ // import AppKit +import BrowserServicesKit import Carbon.HIToolbox import Combine import Common +import PixelKit import Suggestions import Subscription -import BrowserServicesKit +// swiftlint:disable:next type_body_length final class AddressBarTextField: NSTextField { var tabCollectionViewModel: TabCollectionViewModel! { @@ -299,6 +301,25 @@ final class AddressBarTextField: NSTextField { } private func navigate(suggestion: Suggestion?) { + let pixel: GeneralPixel? = { + switch suggestion { + case .phrase: + return .autocompleteClickPhrase + case .website: + return .autocompleteClickWebsite + case .bookmark(_, _, let isFavorite, _): + return isFavorite ? .autocompleteClickFavorite : .autocompleteClickBookmark + case .historyEntry: + return .autocompleteClickHistory + default: + return nil + } + }() + + if let pixel { + PixelKit.fire(pixel) + } + if NSApp.isCommandPressed { openNew(NSApp.isOptionPressed ? .window : .tab, selected: NSApp.isShiftPressed, suggestion: suggestion) } else { diff --git a/DuckDuckGo/Preferences/Model/SearchPreferences.swift b/DuckDuckGo/Preferences/Model/SearchPreferences.swift index 65d385d72b..96a7f5aff8 100644 --- a/DuckDuckGo/Preferences/Model/SearchPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SearchPreferences.swift @@ -20,6 +20,7 @@ import Foundation import AppKit import Bookmarks import Common +import PixelKit protocol SearchPreferencesPersistor { var showAutocompleteSuggestions: Bool { get set } @@ -37,6 +38,7 @@ final class SearchPreferences: ObservableObject, PreferencesTabOpening { @Published var showAutocompleteSuggestions: Bool { didSet { persistor.showAutocompleteSuggestions = showAutocompleteSuggestions + PixelKit.fire(showAutocompleteSuggestions ? GeneralPixel.autocompleteToggledOn : GeneralPixel.autocompleteToggledOff) } } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index e2eeb8d192..50ad0eeb54 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -157,6 +157,15 @@ enum GeneralPixel: PixelKitEventV2 { case passwordImportKeychainPrompt case passwordImportKeychainPromptDenied + // Autocomplete + case autocompleteClickPhrase + case autocompleteClickWebsite + case autocompleteClickBookmark + case autocompleteClickFavorite + case autocompleteClickHistory + case autocompleteToggledOff + case autocompleteToggledOn + // MARK: - Debug case assertionFailure(message: String, file: StaticString, line: UInt) @@ -525,6 +534,15 @@ enum GeneralPixel: PixelKitEventV2 { case .passwordImportKeychainPrompt: return "m_mac_password_import_keychain_prompt" case .passwordImportKeychainPromptDenied: return "m_mac_password_import_keychain_prompt_denied" + // Autocomplete + case .autocompleteClickPhrase: return "m_mac_autocomplete_click_phrase" + case .autocompleteClickWebsite: return "m_mac_autocomplete_click_website" + case .autocompleteClickBookmark: return "m_mac_autocomplete_click_bookmark" + case .autocompleteClickFavorite: return "m_mac_autocomplete_click_favorite" + case .autocompleteClickHistory: return "m_mac_autocomplete_click_history" + case .autocompleteToggledOff: return "m_mac_autocomplete_toggled_off" + case .autocompleteToggledOn: return "m_mac_autocomplete_toggled_on" + // DEBUG case .assertionFailure: return "assertion_failure" diff --git a/UnitTests/Preferences/SearchPreferencesTests.swift b/UnitTests/Preferences/SearchPreferencesTests.swift index 60f5e3a87c..418211c937 100644 --- a/UnitTests/Preferences/SearchPreferencesTests.swift +++ b/UnitTests/Preferences/SearchPreferencesTests.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import PixelKit import XCTest @testable import DuckDuckGo_Privacy_Browser @@ -25,6 +26,16 @@ class MockSearchPreferencesPersistor: SearchPreferencesPersistor { class SearchPreferencesTests: XCTestCase { + override func setUpWithError() throws { + PixelKit.setUp(appVersion: "", + defaultHeaders: [:], + defaults: UserDefaults()) { _, _, _, _, _, _ in } + } + + override func tearDownWithError() throws { + PixelKit.tearDown() + } + func testWhenInitializedThenItLoadsPersistedValues() { let mockPersistor = MockSearchPreferencesPersistor() mockPersistor.showAutocompleteSuggestions = true From 1c65660e4bf6ad80c270ec7f43fc154e07f4626b Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 7 May 2024 14:41:30 -0300 Subject: [PATCH 081/134] Run more parallel operations on manual scans (#2748) --- .../DataBrokerOperationsCollection.swift | 13 +++--- .../DataBrokerProtectionProcessor.swift | 10 ++--- ...kerProtectionProcessorConfiguration.swift} | 24 ++++++---- .../DataBrokerProtectionScheduler.swift | 1 - ...rotectionProcessorConfigurationTests.swift | 44 +++++++++++++++++++ 5 files changed, 70 insertions(+), 22 deletions(-) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/{DataBrokerProtectionSchedulerConfig.swift => DataBrokerProtectionProcessorConfiguration.swift} (52%) create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index eeae49e168..d585da0cd7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -28,14 +28,13 @@ protocol DataBrokerOperationsCollectionErrorDelegate: AnyObject { didErrorBeforeStartingBrokerOperations error: Error) } -final class DataBrokerOperationsCollection: Operation { - - enum OperationType { - case manualScan - case optOut - case all - } +enum OperationType { + case manualScan + case optOut + case all +} +final class DataBrokerOperationsCollection: Operation { public var error: Error? public weak var errorDelegate: DataBrokerOperationsCollectionErrorDelegate? diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index dee4d346e2..c96a3c31d8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -26,7 +26,7 @@ protocol OperationRunnerProvider { final class DataBrokerProtectionProcessor { private let database: DataBrokerProtectionRepository - private let config: SchedulerConfig + private let config: DataBrokerProtectionProcessorConfiguration private let operationRunnerProvider: OperationRunnerProvider private let notificationCenter: NotificationCenter private let operationQueue: OperationQueue @@ -36,7 +36,7 @@ final class DataBrokerProtectionProcessor { private let eventPixels: DataBrokerProtectionEventPixels init(database: DataBrokerProtectionRepository, - config: SchedulerConfig, + config: DataBrokerProtectionProcessorConfiguration = DataBrokerProtectionProcessorConfiguration(), operationRunnerProvider: OperationRunnerProvider, notificationCenter: NotificationCenter = NotificationCenter.default, pixelHandler: EventMapping, @@ -48,7 +48,6 @@ final class DataBrokerProtectionProcessor { self.notificationCenter = notificationCenter self.operationQueue = OperationQueue() self.pixelHandler = pixelHandler - self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsDifferentBrokers self.userNotificationService = userNotificationService self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) self.eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) @@ -109,11 +108,12 @@ final class DataBrokerProtectionProcessor { } // MARK: - Private functions - private func runOperations(operationType: DataBrokerOperationsCollection.OperationType, + private func runOperations(operationType: OperationType, priorityDate: Date?, showWebView: Bool, completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsFor(operationType) // Before running new operations we check if there is any updates to the broker files. if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) @@ -153,7 +153,7 @@ final class DataBrokerProtectionProcessor { } private func createDataBrokerOperationCollections(from brokerProfileQueriesData: [BrokerProfileQueryData], - operationType: DataBrokerOperationsCollection.OperationType, + operationType: OperationType, priorityDate: Date?, showWebView: Bool) -> [DataBrokerOperationsCollection] { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionSchedulerConfig.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift similarity index 52% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionSchedulerConfig.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift index e76c97156f..3281b66c37 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionSchedulerConfig.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionSchedulerConfig.swift +// DataBrokerProtectionProcessorConfiguration.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,13 +18,19 @@ import Foundation -protocol SchedulerConfig { - var concurrentOperationsDifferentBrokers: Int { get } - var intervalBetweenSameBrokerOperations: TimeInterval { get } -} - -struct DataBrokerProtectionSchedulerConfig: SchedulerConfig { +struct DataBrokerProtectionProcessorConfiguration { // Arbitrary numbers for now - var concurrentOperationsDifferentBrokers: Int = 2 - var intervalBetweenSameBrokerOperations: TimeInterval = 2 + let intervalBetweenSameBrokerOperations: TimeInterval = 2 + private let concurrentOperationsDifferentBrokers: Int = 2 + // https://app.asana.com/0/481882893211075/1206981742767469/f + private let concurrentOperationsOnManualScans: Int = 6 + + func concurrentOperationsFor(_ operation: OperationType) -> Int { + switch operation { + case .all, .optOut: + return concurrentOperationsDifferentBrokers + case .manualScan: + return concurrentOperationsOnManualScans + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 4e7d9a9846..509b13272c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -150,7 +150,6 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch captchaService: captchaService) return DataBrokerProtectionProcessor(database: dataManager.database, - config: DataBrokerProtectionSchedulerConfig(), operationRunnerProvider: runnerProvider, notificationCenter: notificationCenter, pixelHandler: pixelHandler, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift new file mode 100644 index 0000000000..fe8eec11c1 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift @@ -0,0 +1,44 @@ +// +// DataBrokerProtectionProcessorConfigurationTests.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 +import Foundation +@testable import DataBrokerProtection + +final class DataBrokerProtectionProcessorConfigurationTests: XCTestCase { + + private let sut = DataBrokerProtectionProcessorConfiguration() + + func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { + let value = sut.concurrentOperationsFor(.manualScan) + let expectedValue = 6 + XCTAssertEqual(value, expectedValue) + } + + func testWhenOperationIsAll_thenConcurrentOperationsBetweenBrokersIsTwo() { + let value = sut.concurrentOperationsFor(.all) + let expectedValue = 2 + XCTAssertEqual(value, expectedValue) + } + + func testWhenOperationIsOptOut_thenConcurrentOperationsBetweenBrokersIsTwo() { + let value = sut.concurrentOperationsFor(.optOut) + let expectedValue = 2 + XCTAssertEqual(value, expectedValue) + } +} From 17047bd67e16cd81a8373f0a68c664488f6aef43 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Wed, 8 May 2024 10:26:37 +0200 Subject: [PATCH 082/134] "Clear This History.." for today burns windows (#2745) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207195823454499/f **Description**: When user selects "Clear This History" from the history menu to clear history for today, we have to burn windows too. --- DuckDuckGo/Fire/Model/Fire.swift | 12 ++- .../Menus/CleanThisHistoryMenuItem.swift | 41 ++++++++++- DuckDuckGo/Menus/HistoryMenu.swift | 3 +- DuckDuckGo/Menus/MainMenuActions.swift | 5 +- UnitTests/Fire/Model/FireTests.swift | 73 +++++++++++++++++++ 5 files changed, 128 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 4766d8b6e6..29d355c9cd 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -235,6 +235,7 @@ final class Fire { @MainActor func burnVisits(of visits: [Visit], except fireproofDomains: FireproofDomains, + isToday: Bool, completion: (() -> Void)? = nil) { // Get domains to burn @@ -254,7 +255,16 @@ final class Fire { domains = domains.convertedToETLDPlus1(tld: tld) historyCoordinating.burnVisits(visits) { - self.burnEntity(entity: .none(selectedDomains: domains), + let entity: BurningEntity + + // Burn all windows in case we are burning visits for today + if isToday { + entity = .allWindows(mainWindowControllers: self.windowControllerManager.mainWindowControllers, selectedDomains: domains) + } else { + entity = .none(selectedDomains: domains) + } + + self.burnEntity(entity: entity, includingHistory: false, completion: completion) } diff --git a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift index 202bee5105..a2c274cf69 100644 --- a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift +++ b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift @@ -22,13 +22,48 @@ import History final class ClearThisHistoryMenuItem: NSMenuItem { + enum HistoryTimeWindow { + case today + case other(dateString: String) + + var isToday: Bool { + switch self { + case .today: + return true + case .other: + return false + } + } + + var dateString: String? { + switch self { + case .today: + return nil + case .other(let dateString): + return dateString + } + } + + init(dateString: String?) { + if let dateString { + self = .other(dateString: dateString) + } else { + self = .today + } + } + } + // Keep the dateString for alerts so we don't need to use the formatter again - func setDateString(_ dateString: String?) { - representedObject = dateString + func setRepresentingObject(historyTimeWindow: HistoryTimeWindow) { + representedObject = historyTimeWindow } var dateString: String? { - representedObject as? String + (representedObject as? HistoryTimeWindow)?.dateString + } + + var isToday: Bool { + (representedObject as? HistoryTimeWindow)?.isToday ?? false } // Getting visits for the whole menu section in order to perform burning diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index 42fd97d990..54b9c3633a 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -248,7 +248,8 @@ final class HistoryMenu: NSMenu { let headerItem = ClearThisHistoryMenuItem(title: UserText.clearThisHistoryMenuItem, action: #selector(AppDelegate.clearThisHistory(_:)), keyEquivalent: "") - headerItem.setDateString(dateString) + let historyTimeWindow = ClearThisHistoryMenuItem.HistoryTimeWindow(dateString: dateString) + headerItem.setRepresentingObject(historyTimeWindow: historyTimeWindow) return [ headerItem, .separator() diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index eca7f8004d..884a6695d5 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -482,13 +482,16 @@ extension MainViewController { } let dateString = sender.dateString + let isToday = sender.isToday let visits = sender.getVisits() let alert = NSAlert.clearHistoryAndDataAlert(dateString: dateString) alert.beginSheetModal(for: window, completionHandler: { response in guard case .alertFirstButtonReturn = response else { return } - FireCoordinator.fireViewModel.fire.burnVisits(of: visits, except: FireproofDomains.shared) + FireCoordinator.fireViewModel.fire.burnVisits(of: visits, + except: FireproofDomains.shared, + isToday: isToday) }) } diff --git a/UnitTests/Fire/Model/FireTests.swift b/UnitTests/Fire/Model/FireTests.swift index 2e8e3e778e..2d9340bb09 100644 --- a/UnitTests/Fire/Model/FireTests.swift +++ b/UnitTests/Fire/Model/FireTests.swift @@ -192,6 +192,79 @@ final class FireTests: XCTestCase { XCTAssertFalse(appStateRestorationManager.canRestoreLastSessionState) } + func testWhenBurnVisitIsCalledForTodayThenAllExistingTabsAreCleared() { + let manager = WebCacheManagerMock() + let historyCoordinator = HistoryCoordinatingMock() + let permissionManager = PermissionManagerMock() + let faviconManager = FaviconManagerMock() + let recentlyClosedCoordinator = RecentlyClosedCoordinatorMock() + + let fire = Fire(cacheManager: manager, + historyCoordinating: historyCoordinator, + permissionManager: permissionManager, + windowControllerManager: WindowControllersManager.shared, + faviconManagement: faviconManager, + recentlyClosedCoordinator: recentlyClosedCoordinator, + tld: ContentBlocking.shared.tld) + let tabCollectionViewModel = TabCollectionViewModel.makeTabCollectionViewModel() + _ = WindowsManager.openNewWindow(with: tabCollectionViewModel, lazyLoadTabs: true) + XCTAssertNotEqual(tabCollectionViewModel.allTabsCount, 0) + + let finishedBurningExpectation = expectation(description: "Finished burning") + fire.burnVisits(of: [], + except: FireproofDomains.shared, + isToday: true, + completion: { + finishedBurningExpectation.fulfill() + }) + + waitForExpectations(timeout: 5) + XCTAssertEqual(tabCollectionViewModel.allTabsCount, 0) + XCTAssert(manager.clearCalled) + XCTAssert(historyCoordinator.burnVisitsCalled) + XCTAssertFalse(historyCoordinator.burnAllCalled) + XCTAssert(permissionManager.burnPermissionsOfDomainsCalled) + XCTAssertFalse(permissionManager.burnPermissionsCalled) + XCTAssert(recentlyClosedCoordinator.burnCacheCalled) + } + + func testWhenBurnVisitIsCalledForOtherDayThenExistingTabsRemainOpen() { + let manager = WebCacheManagerMock() + let historyCoordinator = HistoryCoordinatingMock() + let permissionManager = PermissionManagerMock() + let faviconManager = FaviconManagerMock() + let recentlyClosedCoordinator = RecentlyClosedCoordinatorMock() + + let fire = Fire(cacheManager: manager, + historyCoordinating: historyCoordinator, + permissionManager: permissionManager, + windowControllerManager: WindowControllersManager.shared, + faviconManagement: faviconManager, + recentlyClosedCoordinator: recentlyClosedCoordinator, + tld: ContentBlocking.shared.tld) + let tabCollectionViewModel = TabCollectionViewModel.makeTabCollectionViewModel() + _ = WindowsManager.openNewWindow(with: tabCollectionViewModel, lazyLoadTabs: true) + XCTAssertNotEqual(tabCollectionViewModel.allTabsCount, 0) + let numberOfTabs = tabCollectionViewModel.allTabsCount + + let finishedBurningExpectation = expectation(description: "Finished burning") + fire.burnVisits(of: [], + except: FireproofDomains.shared, + isToday: false, + completion: { + finishedBurningExpectation.fulfill() + }) + + waitForExpectations(timeout: 5) + XCTAssertEqual(tabCollectionViewModel.allTabsCount, numberOfTabs) + XCTAssert(manager.clearCalled) + XCTAssert(historyCoordinator.burnVisitsCalled) + XCTAssertFalse(historyCoordinator.burnAllCalled) + XCTAssert(permissionManager.burnPermissionsOfDomainsCalled) + XCTAssertFalse(permissionManager.burnPermissionsCalled) + XCTAssert(recentlyClosedCoordinator.burnCacheCalled) + } + func preparePersistedState(withFileName fileName: String) -> FileStore { let fileStore = FileStoreMock() let state = SavedStateMock() From 6cea0b6b179df1aa9a7d4d1866e384a649984eed Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Wed, 8 May 2024 09:28:19 +0100 Subject: [PATCH 083/134] Pixelkit invalid name fix (#2751) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207238594885159/f CC: @ayoy @diegoreymendez @loremattei **Description**: - Adds https://github.com/duckduckgo/BrowserServicesKit/pull/810 - Creates a new NonStandardPixel that uses NonStandardEvent - Names and frequencies fix from https://app.asana.com/0/30173902528854/1207202086855539/f and https://app.asana.com/0/30173902528854/1207236076008989/f `m_mac.` > `m_mac_` --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/Application/AppDelegate.swift | 6 +-- .../Fire/ViewModel/FirePopoverViewModel.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 2 +- .../View/PrivacyDashboardViewController.swift | 2 +- DuckDuckGo/Statistics/GeneralPixel.swift | 39 ++++---------- DuckDuckGo/Statistics/NonStandardPixel.swift | 53 +++++++++++++++++++ .../DuckPlayerTabExtension.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../BrokenSiteReportingReferenceTests.swift | 2 +- .../WebsiteBreakageReportTests.swift | 13 ++++- 14 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 DuckDuckGo/Statistics/NonStandardPixel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index dc88f48c10..a5a233c122 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2552,6 +2552,8 @@ F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */; }; + F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; + F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F188267D2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F18826802BBEB58100D9AC4F /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; @@ -4034,6 +4036,7 @@ EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+VPNAgentConvenienceInitializers.swift"; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; + F118EA842BEACC7000F77634 /* NonStandardPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonStandardPixel.swift; sourceTree = ""; }; F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPixel.swift; sourceTree = ""; }; F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProPixel.swift; sourceTree = ""; }; F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; @@ -7823,6 +7826,7 @@ 857E5AF32A79044900FC0FB4 /* Experiment */, B610F2BA27A145C500FCEBE9 /* RulesCompilationMonitor.swift */, F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */, + F118EA842BEACC7000F77634 /* NonStandardPixel.swift */, F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */, F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */, ); @@ -10022,6 +10026,7 @@ 3706FC38293F65D500E42796 /* BundleExtension.swift in Sources */, 4B9DB04B2A983B24000927DB /* NotificationService.swift in Sources */, 3706FC3A293F65D500E42796 /* NSOpenPanelExtensions.swift in Sources */, + F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */, 3706FC3B293F65D500E42796 /* FirePopover.swift in Sources */, 4B4D60C12A0C848E00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, 3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */, @@ -11232,6 +11237,7 @@ 4B9DB04A2A983B24000927DB /* NotificationService.swift in Sources */, 3775912D29AAC72700E26367 /* SyncPreferences.swift in Sources */, F1B33DF22BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */, + F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */, 1DB9618329F67F6200CF5568 /* FaviconNullStore.swift in Sources */, BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, B693954F26F04BEB0015B914 /* PaddedImageButton.swift in Sources */, @@ -12772,7 +12778,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.0; + version = 144.0.1; }; }; 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 41fa2b0e67..dd9eae0f55 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" : "9906b9464f6f12e94f3cc62456b5b5a9c1a43db8", - "version" : "144.0.0" + "revision" : "f34b0a63938df11ef471aa3301dcc0de09b0d31b", + "version" : "144.0.1" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 2f93c17ac4..3d3ad5d5d2 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -449,7 +449,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { .filter { $0 } .asVoid() .sink { [weak syncService] in - PixelKit.fire(GeneralPixel.syncDaily, frequency: .daily) + PixelKit.fire(GeneralPixel.syncDaily, frequency: .legacyDaily) syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in PixelKit.fire(GeneralPixel.syncSuccessRateDaily, withAdditionalParameters: params) }) @@ -532,7 +532,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } private func emailDidSignInNotification(_ notification: Notification) { - PixelKit.fire(GeneralPixel.emailEnabled) + PixelKit.fire(NonStandardEvent(NonStandardPixel.emailEnabled)) if AppDelegate.isNewUser { PixelKit.fire(GeneralPixel.emailEnabledInitial, frequency: .legacyInitial) } @@ -543,7 +543,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } private func emailDidSignOutNotification(_ notification: Notification) { - PixelKit.fire(GeneralPixel.emailDisabled) + PixelKit.fire(NonStandardEvent(NonStandardPixel.emailDisabled)) if let object = notification.object as? EmailManager, let emailManager = syncDataProviders.settingsAdapter.emailManager, object !== emailManager { syncService?.scheduler.notifyDataChanged() } diff --git a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift index 1221497f5f..0d07e1f473 100644 --- a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift +++ b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift @@ -189,7 +189,7 @@ final class FirePopoverViewModel { // MARK: - Burning func burn() { - PixelKit.fire(GeneralPixel.fireButtonFirstBurn, frequency: .daily) + PixelKit.fire(GeneralPixel.fireButtonFirstBurn, frequency: .legacyDaily) switch (clearingOption, areAllSelected) { case (.currentTab, _): diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 847452c63c..1b48e04049 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -524,7 +524,7 @@ final class EmailOptionsButtonSubMenu: NSMenu { let pixelParameters = self.emailManager.emailPixelParameters self.emailManager.updateLastUseDate() - PixelKit.fire(GeneralPixel.emailUserCreatedAlias, withAdditionalParameters: pixelParameters) + PixelKit.fire(NonStandardEvent(NonStandardPixel.emailUserCreatedAlias), withAdditionalParameters: pixelParameters) NSPasteboard.general.copy(address) NotificationCenter.default.post(name: NSNotification.Name.privateEmailCopiedToClipboard, object: nil) diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index befe61b87a..059652246d 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -45,7 +45,7 @@ final class PrivacyDashboardViewController: NSViewController { private let brokenSiteReporter: BrokenSiteReporter = { BrokenSiteReporter(pixelHandler: { parameters in - PixelKit.fire(GeneralPixel.brokenSiteReport, + PixelKit.fire(NonStandardEvent(NonStandardPixel.brokenSiteReport), withAdditionalParameters: parameters, allowedQueryReservedCharacters: BrokenSiteReport.allowedQueryReservedCharacters) }, keyValueStoring: UserDefaults.standard) diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index e2eeb8d192..f4b375fb66 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -26,7 +26,6 @@ import Configuration enum GeneralPixel: PixelKitEventV2 { case crash - case brokenSiteReport case compileRulesWait(onboardingShown: OnboardingShown, waitTime: CompileRulesWaitTime, result: WaitResult) case launchInitial(cohort: String) @@ -55,12 +54,6 @@ enum GeneralPixel: PixelKitEventV2 { case adClickAttributionActive case adClickAttributionPageLoads - case emailEnabled - case emailDisabled - case emailUserPressedUseAddress - case emailUserPressedUseAlias - case emailUserCreatedAlias - case jsPixel(_ pixel: AutofillUserScript.JSPixel) // Activation Points @@ -321,9 +314,6 @@ enum GeneralPixel: PixelKitEventV2 { case .crash: return "m_mac_crash" - case .brokenSiteReport: - return "epbf_macos_desktop" - case .compileRulesWait(onboardingShown: let onboardingShown, waitTime: let waitTime, result: let result): return "m_mac_cbr-wait_\(onboardingShown)_\(waitTime)_\(result)" @@ -371,13 +361,6 @@ enum GeneralPixel: PixelKitEventV2 { case .adClickAttributionPageLoads: return "m_mac_ad_click_page_loads" - // Deliberately omit the `m_mac_` prefix in order to format these pixels the same way as other platforms - case .emailEnabled: return "email_enabled_macos_desktop" - case .emailDisabled: return "email_disabled_macos_desktop" - case .emailUserPressedUseAddress: return "email_filled_main_macos_desktop" - case .emailUserPressedUseAlias: return "email_filled_random_macos_desktop" - case .emailUserCreatedAlias: return "email_generated_button_macos_desktop" - case .jsPixel(let pixel): // Email pixels deliberately avoid using the `m_mac_` prefix. if pixel.isEmailPixel { @@ -386,22 +369,22 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_\(pixel.pixelName)" } case .emailEnabledInitial: - return "m_mac.enable-email-protection.initial" + return "m_mac_enable-email-protection_initial" case .watchInDuckPlayerInitial: - return "m_mac.watch-in-duckplayer.initial" + return "m_mac_watch-in-duckplayer_initial" case .setAsDefaultInitial: - return "m_mac.set-as-default.initial" + return "m_mac_set-as-default_initial" case .importDataInitial: - return "m_mac.import-data.initial" + return "m_mac_import-data_initial" case .newTabInitial: - return "m_mac.new-tab-opened.initial" + return "m_mac_new-tab-opened_initial" case .favoriteSectionHidden: - return "m_mac.favorite-section-hidden" + return "m_mac_favorite-section-hidden" case .recentActivitySectionHidden: - return "m_mac.recent-activity-section-hidden" + return "m_mac_recent-activity-section-hidden" case .continueSetUpSectionHidden: - return "m_mac.continue-setup-section-hidden" + return "m_mac_continue-setup-section-hidden" // Fire Button case .fireButtonFirstBurn: @@ -434,11 +417,11 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_mp_wlr" case .launchInitial: - return "m.mac.first-launch" + return "m_mac_first-launch" case .serpInitial: - return "m.mac.navigation.first-search" + return "m_mac_navigation_first-search" case .serpDay21to27: - return "m.mac.search-day-21-27.initial" + return "m_mac_search-day-21-27_initial" case .vpnBreakageReport: return "m_mac_vpn_breakage_report" diff --git a/DuckDuckGo/Statistics/NonStandardPixel.swift b/DuckDuckGo/Statistics/NonStandardPixel.swift new file mode 100644 index 0000000000..b528aef0a1 --- /dev/null +++ b/DuckDuckGo/Statistics/NonStandardPixel.swift @@ -0,0 +1,53 @@ +// +// NonStandardPixel.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 +import BrowserServicesKit +import DDGSync +import Configuration + +/// These pixels deliberately omit the `m_mac_` prefix in order to format these pixel the same way as other platforms, they are sent unchanged +enum NonStandardPixel: PixelKitEventV2 { + + case brokenSiteReport + case emailEnabled + case emailDisabled + case emailUserPressedUseAddress + case emailUserPressedUseAlias + case emailUserCreatedAlias + + var name: String { + switch self { + case .brokenSiteReport: return "epbf_macos_desktop" + case .emailEnabled: return "email_enabled_macos_desktop" + case .emailDisabled: return "email_disabled_macos_desktop" + case .emailUserPressedUseAddress: return "email_filled_main_macos_desktop" + case .emailUserPressedUseAlias: return "email_filled_random_macos_desktop" + case .emailUserCreatedAlias: return "email_generated_button_macos_desktop" + } + } + + var parameters: [String: String]? { + return nil + } + + var error: Error? { + return nil + } +} diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift index ca6cec4c5a..c34ab6a88f 100644 --- a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift @@ -312,7 +312,7 @@ extension DuckPlayerTabExtension: NavigationResponder { return } if navigation.url.isDuckPlayer { - PixelKit.fire(GeneralPixel.duckPlayerDailyUniqueView, frequency: .daily) + PixelKit.fire(GeneralPixel.duckPlayerDailyUniqueView, frequency: .legacyDaily) } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 19b8daa1e2..6b804973c4 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: "144.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a027d241cb..6aa9f1b8d2 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 7eb918dbc2..0d69eb5246 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: "144.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift b/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift index 1d38801f44..0621b9b2fa 100644 --- a/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift +++ b/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift @@ -51,7 +51,7 @@ final class BrokenSiteReportingReferenceTests: XCTestCase { APIRequest.Headers.setUserAgent("") var params = parameters params["test"] = "1" - let configuration = APIRequest.Configuration(url: URL.pixelUrl(forPixelNamed: GeneralPixel.brokenSiteReport.name), + let configuration = APIRequest.Configuration(url: URL.pixelUrl(forPixelNamed: NonStandardPixel.brokenSiteReport.name), queryParameters: params, allowedQueryReservedCharacters: BrokenSiteReport.allowedQueryReservedCharacters) return configuration.request diff --git a/UnitTests/WebsiteBreakageReport/WebsiteBreakageReportTests.swift b/UnitTests/WebsiteBreakageReport/WebsiteBreakageReportTests.swift index 1103a8f2eb..94f087a80d 100644 --- a/UnitTests/WebsiteBreakageReport/WebsiteBreakageReportTests.swift +++ b/UnitTests/WebsiteBreakageReport/WebsiteBreakageReportTests.swift @@ -18,12 +18,21 @@ import PrivacyDashboard import XCTest - +import PixelKit +import PixelKitTestingUtilities @testable import DuckDuckGo_Privacy_Browser @testable import Networking class WebsiteBreakageReportTests: XCTestCase { + func testReportBrokenSitePixel() { + fire(NonStandardEvent(NonStandardPixel.brokenSiteReport), + frequency: .standard, + and: .expect(pixelName: "epbf_macos_desktop"), + file: #filePath, + line: #line) + } + func testCommonSetOfFields() throws { let breakage = BrokenSiteReport( siteUrl: URL(string: "https://example.test/")!, @@ -129,7 +138,7 @@ class WebsiteBreakageReportTests: XCTestCase { APIRequest.Headers.setUserAgent("") var params = parameters params["test"] = "1" - let configuration = APIRequest.Configuration(url: URL.pixelUrl(forPixelNamed: GeneralPixel.brokenSiteReport.name), + let configuration = APIRequest.Configuration(url: URL.pixelUrl(forPixelNamed: NonStandardPixel.brokenSiteReport.name), queryParameters: params, allowedQueryReservedCharacters: BrokenSiteReport.allowedQueryReservedCharacters) return configuration.request From 1bdc6b7c4991eefbb29f5908c073818ddad9dd9d Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 8 May 2024 12:02:32 +0200 Subject: [PATCH 084/134] Changes to identify a zombie object (#2752) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207265589515528/f ## Description Partially rolls back some changes so we'll know if the zombie object showing up in stack traces is the `webView` or the `userContentController` property. In other words, depending on how the stack trace looks like one our next reports we'll know which one is the zombie (with a priority checking the web view). --- DuckDuckGo/Tab/Model/Tab.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 1fd8c17493..c66c7a3231 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -333,15 +333,15 @@ protocol NewWindowPolicyDecisionMaker { } deinit { - cleanUpBeforeClosing(onDeinit: true, webView: webView, userContentController: userContentController) + cleanUpBeforeClosing(onDeinit: true, webView: webView) } func cleanUpBeforeClosing() { - cleanUpBeforeClosing(onDeinit: false, webView: webView, userContentController: userContentController) + cleanUpBeforeClosing(onDeinit: false, webView: webView) } @MainActor(unsafe) - private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView, userContentController: UserContentController?) { + private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView) { let job = { [webView, userContentController] in webView.stopAllMedia(shouldStopLoading: true) From 88a75044521d9ac84177abbace2f0c32397cd402 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 8 May 2024 10:29:03 +0000 Subject: [PATCH 085/134] Bump version to 1.87.0 (184) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index e5253b1076..ce05169e2e 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 183 +CURRENT_PROJECT_VERSION = 184 From 63d931b5d1a100a0eb041f749e16609adb15aa0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 8 May 2024 19:11:12 +0200 Subject: [PATCH 086/134] Update BSK ref that fixes bat.js logic (#2755) Task/Issue URL: https://app.asana.com/0/1177771139624306/1205259925028360/f --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/Common/Logging/Logging.swift | 1 - LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a5a233c122..68e06cb532 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12778,7 +12778,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.1; + version = 144.0.3; }; }; 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 dd9eae0f55..5b58ad8c2e 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" : "f34b0a63938df11ef471aa3301dcc0de09b0d31b", - "version" : "144.0.1" + "revision" : "9df0476d399a34e4a35b3fe605a91d446fdee1e0", + "version" : "144.0.3" } }, { diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift index 3740e0e223..72a121cd48 100644 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ b/DuckDuckGo/Common/Logging/Logging.swift @@ -58,7 +58,6 @@ extension OSLog { @OSLogWrapper(.fire) static var fire @OSLogWrapper(.dataImportExport) static var dataImportExport @OSLogWrapper(.pixel) static var pixel - @OSLogWrapper(.contentBlocking) static var contentBlocking @OSLogWrapper(.httpsUpgrade) static var httpsUpgrade @OSLogWrapper(.favicons) static var favicons @OSLogWrapper(.autoLock) static var autoLock diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 6b804973c4..8a11c04029 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: "144.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.3"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6aa9f1b8d2..586354bcae 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.3"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 0d69eb5246..cd509a0479 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: "144.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.3"), .package(path: "../SwiftUIExtensions") ], targets: [ From 4ab25c97c48dbbf8bd7f95f1323d063c5122d35b Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 9 May 2024 11:45:47 +0100 Subject: [PATCH 087/134] Add bookmarks to top hits suggestions on iOS (#2754) Task/Issue URL: https://app.asana.com/0/0/1207251449708624/f Tech Design URL: CC: **Description**: Changes iOS so that bookmarks are included in top hits. No change for macOS. **Steps to test this PR**: See https://github.com/duckduckgo/BrowserServicesKit/pull/812 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 68e06cb532..f75ee40fe1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12778,7 +12778,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.3; + version = 144.0.4; }; }; 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 5b58ad8c2e..090662b70b 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" : "9df0476d399a34e4a35b3fe605a91d446fdee1e0", - "version" : "144.0.3" + "revision" : "8b8092db4bcca19c0fa959a517c004900753644e", + "version" : "144.0.4" } }, { @@ -120,7 +120,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "46989693916f56d1186bd59ac15124caef896560", "version" : "1.3.1" @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", + "location" : "https://github.com/apple/swift-syntax.git", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 8a11c04029..4ffb8d7284 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: "144.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.4"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 586354bcae..1889ff2456 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.4"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index cd509a0479..ff7567877f 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: "144.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.4"), .package(path: "../SwiftUIExtensions") ], targets: [ From 567c0628724b1744eeb0d8de5f5858dd61ab221d Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 9 May 2024 13:39:29 +0000 Subject: [PATCH 088/134] Bump version to 1.86.1 (185) --- Configuration/BuildNumber.xcconfig | 2 +- Configuration/Version.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 3d9e7b5269..97b9ceed87 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 182 +CURRENT_PROJECT_VERSION = 185 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index c23c40bdc0..c634f3aeac 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.86.0 +MARKETING_VERSION = 1.86.1 From 94f3cd9439d0de267d22ee3cf7d578492768e417 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Thu, 9 May 2024 16:16:13 +0200 Subject: [PATCH 089/134] Add temporary survey to Passwords Settings screen (#2753) Task/Issue URL: https://app.asana.com/0/488551667048375/1206567467430497/f Tech Design URL: CC: Description: Adds temporary survey to the Passwords Settings screen --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++-- .../Passwords-DDG-128.imageset/Contents.json | 12 ++++++ .../Passwords-DDG-128.svg | 31 +++++++++++++++ .../Common/Surveys/SurveyURLBuilder.swift | 27 +++++++++++++ .../Utilities/UserDefaultsWrapper.swift | 1 + DuckDuckGo/Menus/MainMenu.swift | 5 +++ .../Model/AutofillPreferences.swift | 4 ++ .../Model/AutofillPreferencesModel.swift | 38 +++++++++++++++++++ .../View/PreferencesAutofillView.swift | 37 ++++++++++++++++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- UnitTests/DataExport/MockSecureVault.swift | 12 ++++++ .../AutofillPreferencesModelTests.swift | 1 + 15 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a5a233c122..986e587e33 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12778,7 +12778,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.1; + version = "144.0.1-1"; }; }; 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 dd9eae0f55..ec24112bc8 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" : "f34b0a63938df11ef471aa3301dcc0de09b0d31b", - "version" : "144.0.1" + "revision" : "6ceabf1d257ff1d1164afb5b9139f9f20baf0c6e", + "version" : "144.0.1-1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "6053999d6af384a716ab0ce7205dbab5d70ed1b3", - "version" : "11.0.1" + "revision" : "10aeff1ec7f533d1705233a9b14f9393a699b1c0", + "version" : "11.0.2" } }, { diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json new file mode 100644 index 0000000000..1c9ce942d3 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Passwords-DDG-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg new file mode 100644 index 0000000000..fefd0c6886 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift index 15ca4a8c76..a431276800 100644 --- a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift +++ b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift @@ -18,6 +18,7 @@ import Foundation import Common +import BrowserServicesKit final class SurveyURLBuilder { @@ -95,6 +96,23 @@ final class SurveyURLBuilder { return components.url } + func buildSurveyURLWithPasswordsCountSurveyParameter(from originalURLString: String) -> URL? { + let surveyURLWithParameters = buildSurveyURL(from: originalURLString) + + guard let surveyURLWithParametersString = surveyURLWithParameters?.absoluteString, + var components = URLComponents(string: surveyURLWithParametersString), + let bucket = passwordsCountBucket() else { + return surveyURLWithParameters + } + + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: "saved_passwords", value: bucket)) + + components.queryItems = queryItems + + return components.url + } + private func queryItem(parameter: SurveyURLParameters, value: String) -> URLQueryItem { let urlAllowed: CharacterSet = .alphanumerics.union(.init(charactersIn: "-._~")) let sanitizedValue = value.addingPercentEncoding(withAllowedCharacters: urlAllowed) @@ -105,4 +123,13 @@ final class SurveyURLBuilder { return URLQueryItem(name: parameter.rawValue, value: String(describing: value)) } + private func passwordsCountBucket() -> String? { + guard let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared), + let bucket = try? secureVault.accountsCountBucket() else { + return nil + } + + return bucket + } + } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 5891b891a5..5876bef57d 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -74,6 +74,7 @@ public struct UserDefaultsWrapper { case askToSavePaymentMethods = "preferences.ask-to-save.payment-methods" case autolockLocksFormFilling = "preferences.lock-autofill-form-fill" case autofillDebugScriptEnabled = "preferences.enable-autofill-debug-script" + case autofillSurveyEnabled = "preferences.enable-autofill-survey" case saveAsPreferredFileType = "saveAs.selected.filetype" diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 66e026dc1d..1b24150862 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -584,6 +584,7 @@ import SubscriptionUI } NSMenuItem(title: "Reset Email Protection InContext Signup Prompt", action: #selector(MainViewController.resetEmailProtectionInContextPrompt)) NSMenuItem(title: "Reset Pixels Storage", action: #selector(MainViewController.resetDailyPixels)) + NSMenuItem(title: "Reset Passwords Survey", action: #selector(enablePasswordsSurveyAction), target: self) }.withAccessibilityIdentifier("MainMenu.resetData") NSMenuItem(title: "UI Triggers") { NSMenuItem(title: "Show Save Credentials Popover", action: #selector(MainViewController.showSaveCredentialsPopover)) @@ -729,6 +730,10 @@ import SubscriptionUI updateAutofillDebugScriptMenuItem() } + @objc private func enablePasswordsSurveyAction(_ sender: NSMenuItem) { + AutofillPreferences().autofillSurveyEnabled = true + } + @objc private func debugLoggingMenuItemAction(_ sender: NSMenuItem) { #if APPSTORE if !OSLog.isRunningInDebugEnvironment { diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift index 58a55e86f1..78920dd0c0 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift @@ -27,6 +27,7 @@ protocol AutofillPreferencesPersistor { var autolockLocksFormFilling: Bool { get set } var passwordManager: PasswordManager { get set } var debugScriptEnabled: Bool { get set } + var autofillSurveyEnabled: Bool { get set } } enum PasswordManager: String, CaseIterable { @@ -149,6 +150,9 @@ final class AutofillPreferences: AutofillPreferencesPersistor { } } + @UserDefaultsWrapper(key: .autofillSurveyEnabled, defaultValue: true) + var autofillSurveyEnabled: Bool + private var statisticsStore: StatisticsStore { return injectedDependencyStore ?? defaultDependencyStore } diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift index 4f254b3a0f..e7e56043b8 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -17,6 +17,8 @@ // import Foundation +import BrowserServicesKit +import Common final class AutofillPreferencesModel: ObservableObject { @@ -59,6 +61,12 @@ final class AutofillPreferencesModel: ObservableObject { } } + @Published private(set) var autofillSurveyEnabled: Bool { + didSet { + persistor.autofillSurveyEnabled = autofillSurveyEnabled && Bundle.main.preferredLocalizations.first == "en" + } + } + @MainActor @Published private(set) var passwordManager: PasswordManager { didSet { @@ -153,6 +161,7 @@ final class AutofillPreferencesModel: ObservableObject { autolockLocksFormFilling = persistor.autolockLocksFormFilling passwordManager = persistor.passwordManager hasNeverPromptWebsites = !neverPromptWebsitesManager.neverPromptWebsites.isEmpty + autofillSurveyEnabled = persistor.autofillSurveyEnabled } private var persistor: AutofillPreferencesPersistor @@ -192,4 +201,33 @@ final class AutofillPreferencesModel: ObservableObject { NSWorkspace.shared.open(.fullDiskAccess) } + func launchSurvey(statisticsStore: StatisticsStore = LocalStatisticsStore(), + activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), + operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description, + appVersion: String = AppVersion.shared.versionNumber, + hardwareModel: String? = HardwareModel.model) { + + let surveyURLBuilder = SurveyURLBuilder( + statisticsStore: statisticsStore, + operatingSystemVersion: operatingSystemVersion, + appVersion: appVersion, + hardwareModel: hardwareModel, + daysSinceActivation: activationDateStore.daysSinceActivation(), + daysSinceLastActive: activationDateStore.daysSinceLastActive() + ) + + guard let surveyUrl = surveyURLBuilder.buildSurveyURLWithPasswordsCountSurveyParameter(from: "https://selfserve.decipherinc.com/survey/selfserve/32ab/240307") else { + return + } + + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .url(surveyUrl, credential: nil, source: .appOpenUrl)) + } + + disableAutofillSurvey() + } + + func disableAutofillSurvey() { + autofillSurveyEnabled = false + } } diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index cb36a0d227..d3e4501562 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -67,6 +67,43 @@ extension Preferences { // Autofill Content Button PreferencePaneSection { + + // New section + if model.autofillSurveyEnabled { + HStack(alignment: .top, spacing: 20) { + Image(.passwordsDDG128) + .frame(width: 64, height: 48) + + VStack(alignment: .leading) { + Text(verbatim: "Help us improve!") + .bold() + Text(verbatim: "We want to make using passwords in DuckDuckGo better.") + .foregroundColor(.greyText) + .padding(.top, 1) + + HStack { + Button(action: { + model.disableAutofillSurvey() + }, label: { + Text(verbatim: "No Thanks") + }) + Button(action: { + model.launchSurvey() + }, label: { + Text(verbatim: "Take Survey") + }) + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + } + .padding(.top, 12) + } + + Spacer() + } + .padding() + .roundedBorder() + .padding(.bottom, 24) + } + Button(UserText.autofillViewContentButton) { model.showAutofillPopover() } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 6b804973c4..525f21d8e1 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: "144.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6aa9f1b8d2..8edcbdcd8a 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 0d69eb5246..d9f3cfbaf7 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: "144.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/DataExport/MockSecureVault.swift b/UnitTests/DataExport/MockSecureVault.swift index d9d55a92fa..5f07ac42f2 100644 --- a/UnitTests/DataExport/MockSecureVault.swift +++ b/UnitTests/DataExport/MockSecureVault.swift @@ -58,6 +58,14 @@ final class MockSecureVault: AutofillSecureVault { return storedAccounts } + func accountsCount() throws -> Int { + return storedAccounts.count + } + + func accountsCountBucket() throws -> String { + return "" + } + func accountsFor(domain: String) throws -> [SecureVaultModels.WebsiteAccount] { return storedAccounts.filter { $0.domain == domain } } @@ -327,6 +335,10 @@ class MockDatabaseProvider: AutofillDatabaseProvider { return _accounts } + func accountsCount() throws -> Int { + return _accounts.count + } + func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites] { return _neverPromptWebsites } diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index 9851f65f72..ef02dc27f1 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -29,6 +29,7 @@ final class AutofillPreferencesPersistorMock: AutofillPreferencesPersistor { var passwordManager: PasswordManager = .duckduckgo var autolockLocksFormFilling: Bool = false var debugScriptEnabled: Bool = false + var autofillSurveyEnabled: Bool = false } final class UserAuthenticatorMock: UserAuthenticating { From 30a663d11d22a9d7c09f409cde11dea61692d634 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 9 May 2024 16:36:46 +0200 Subject: [PATCH 090/134] Fix closing full-screen videos with the X button on macOS 14.4 and above (#2762) Task/Issue URL: https://app.asana.com/0/72649045549333/1206959015087322/f Description: Partially revert a fix to #1618 on macOS 14.4 and above because the original issue seems to be gone and that fix seemed to have started breaking full screen videos in macOS Sonoma 14.4 and above. --- .../Tab/View/WebViewContainerView.swift | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/Tab/View/WebViewContainerView.swift b/DuckDuckGo/Tab/View/WebViewContainerView.swift index e87e18f677..e75c81c627 100644 --- a/DuckDuckGo/Tab/View/WebViewContainerView.swift +++ b/DuckDuckGo/Tab/View/WebViewContainerView.swift @@ -141,21 +141,23 @@ final class WebViewContainerView: NSView { } .store(in: &cancellables) - fullScreenWindowWillCloseCancellable = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: fullScreenWindow) - .sink { [weak self] notification in - self?.fullScreenWindowWillCloseCancellable = nil - let fullScreenWindowController = (notification.object as? NSWindow)?.windowController - DispatchQueue.main.async { [weak fullScreenWindowController] in - guard let fullScreenWindowController else { return } - // just in case. - // if WKFullScreenWindowController receives `close()` the next time it‘s open it will crash because its _webView is nil - // https://errors.duckduckgo.com/organizations/ddg/issues/3411/?project=6&referrer=release-issue-stream - NSException.try { - fullScreenWindowController.setValue(NSView(), forKeyPath: #keyPath(webView)) + // https://app.asana.com/0/72649045549333/1206959015087322/f + if #unavailable(macOS 14.4) { + fullScreenWindowWillCloseCancellable = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: fullScreenWindow) + .sink { [weak self] notification in + self?.fullScreenWindowWillCloseCancellable = nil + let fullScreenWindowController = (notification.object as? NSWindow)?.windowController + DispatchQueue.main.async { [weak fullScreenWindowController] in + guard let fullScreenWindowController else { return } + // just in case. + // if WKFullScreenWindowController receives `close()` the next time it‘s open it will crash because its _webView is nil + // https://errors.duckduckgo.com/organizations/ddg/issues/3411/?project=6&referrer=release-issue-stream + NSException.try { + fullScreenWindowController.setValue(NSView(), forKeyPath: #keyPath(webView)) + } } - } - } + } } From 291ddb95f055d7d7b6c7ddd611a6f1f82281355e Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 9 May 2024 15:46:58 +0000 Subject: [PATCH 091/134] Bump version to 1.87.0 (186) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 97b9ceed87..9df09905e5 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 185 +CURRENT_PROJECT_VERSION = 186 From cf9de4654a474560238f7a4a9681f5fb72fbea64 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Thu, 9 May 2024 18:19:12 +0200 Subject: [PATCH 092/134] Mergeback from release/1.87.0 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 986e587e33..b0b0891703 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12778,7 +12778,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = "144.0.1-1"; + version = 144.0.5; }; }; 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 de5a20fb25..0fabc33d55 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" : "6ceabf1d257ff1d1164afb5b9139f9f20baf0c6e", - "version" : "144.0.1-1" + "revision" : "151737e0b690437a127cc6f1b9f443481cf2f645", + "version" : "144.0.5" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 525f21d8e1..db44fe3e2c 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: "144.0.1-1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.5"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 8edcbdcd8a..19008a34fb 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.5"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index d9f3cfbaf7..8355d9146a 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: "144.0.1-1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.5"), .package(path: "../SwiftUIExtensions") ], targets: [ From 2d116615742e67a57634748151584aef1c8fb965 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 10 May 2024 10:18:20 +0200 Subject: [PATCH 093/134] Revert "Changes to identify a zombie object (#2752)" This reverts commit 1bdc6b7c4991eefbb29f5908c073818ddad9dd9d. --- DuckDuckGo/Tab/Model/Tab.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index c66c7a3231..1fd8c17493 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -333,15 +333,15 @@ protocol NewWindowPolicyDecisionMaker { } deinit { - cleanUpBeforeClosing(onDeinit: true, webView: webView) + cleanUpBeforeClosing(onDeinit: true, webView: webView, userContentController: userContentController) } func cleanUpBeforeClosing() { - cleanUpBeforeClosing(onDeinit: false, webView: webView) + cleanUpBeforeClosing(onDeinit: false, webView: webView, userContentController: userContentController) } @MainActor(unsafe) - private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView) { + private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView, userContentController: UserContentController?) { let job = { [webView, userContentController] in webView.stopAllMedia(shouldStopLoading: true) From b6d345ff75a9b12e02d2f5ac566a837846e38fb6 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Fri, 10 May 2024 10:24:43 +0200 Subject: [PATCH 094/134] Pointing users to the latest Bitwarden (#2763) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207274508235713/f **Description**: Pointing users to the latest fixed Bitwarden version instead of the downgrade. --- DuckDuckGo/Common/Localizables/UserText.swift | 4 +-- DuckDuckGo/Localizable.xcstrings | 36 +++++++++---------- .../View/PreferencesAutofillView.swift | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b6c361c836..ba3424b5a6 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -887,8 +887,8 @@ struct UserText { static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.") static let bitwardenNotInstalled = NSLocalizedString("bitwarden.not.installed", value: "Bitwarden app is not installed", comment: "") static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "Message that warns user they need to update their password manager Bitwarden app vesion") - static let bitwardenIncompatible = NSLocalizedString("bitwarden.incompatible", value: "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please revert to an older version by following these steps:", comment: "Message that warns user that specific Bitwarden app vesions are not compatible with this app") - static let bitwardenIncompatibleStep1 = NSLocalizedString("bitwarden.incompatible.step.1", value: "Download v2014.2.1", comment: "First step to downgrade Bitwarden") + static let bitwardenIncompatible = NSLocalizedString("bitwarden.incompatible", value: "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:", comment: "Message that warns user that specific Bitwarden app vesions are not compatible with this app") + static let bitwardenIncompatibleStep1 = NSLocalizedString("bitwarden.incompatible.step.1", value: "Download v2024.4.3", comment: "First step to downgrade Bitwarden") static let bitwardenIncompatibleStep2 = NSLocalizedString("bitwarden.incompatible.step.2", value: "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder.", comment: "Second step to downgrade Bitwarden") static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.") static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 71b681ff31..5c4e63ca7b 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -6431,55 +6431,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Die folgenden Bitwarden-Versionen sind mit DuckDuckGo inkompatibel: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Bitte kehre zu einer älteren Version zurück, indem du die folgenden Schritte ausführst:" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please revert to an older version by following these steps:" + "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Las siguientes versiones de Bitwarden son incompatibles con DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Vuelve a una versión anterior siguiendo estos pasos:" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Les versions suivantes de Bitwarden sont incompatibles avec DuckDuckGo : v2024.3.0, v2024.3.2, v2024.4.0 et v2024.4.1. Veuillez revenir à une version antérieure en procédant comme suit :" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Le seguenti versioni di Bitwarden non sono compatibili con DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. È necessario tornare a una versione precedente seguendo questi passaggi:" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "De volgende versies van Bitwarden zijn niet compatibel met DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Volg deze stappen om terug te gaan naar een oudere versie:" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Następujące wersje Bitwarden są niezgodne z DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Przywróć starszą wersję, wykonując następujące czynności:" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "As seguintes versões do Bitwarden são incompatíveis com o DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0 e v2024.4.1. Reverte para uma versão mais antiga seguindo estes passos:" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "С DuckDuckGo несовместимы следующие версии Bitwarden: 2024.3.0, 2024.3.2, 2024.4.0, 2024.4.1. Вернитесь к более старой версии, выполнив следующие действия:" } } @@ -6491,55 +6491,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "V2014.2.1 herunterladen" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Download v2014.2.1" + "value" : "Download v2024.4.3" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Descargar v2014.2.1" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Télécharger v2014.2.1" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Scarica la versione v2014.2.1" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2014.2.1" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Pobierz v2014.2.1" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Transfere a versão v2014.2.1" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Скачайте версию 2014.2.1" } } diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index d3e4501562..77fe6bec54 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -388,7 +388,7 @@ struct BitwardenDowngradeInfoView: View, PreferencesTabOpening { HStack { Text("1.") Button(UserText.bitwardenIncompatibleStep1, action: { - openNewTab(with: URL(string: "https://github.com/bitwarden/clients/releases/download/desktop-v2024.2.1/Bitwarden-2024.2.1-universal.dmg")!) + openNewTab(with: URL(string: "https://github.com/bitwarden/clients/releases/download/desktop-v2024.4.3/Bitwarden-2024.4.3-universal.dmg")!) }).foregroundColor(.accentColor) } Text(UserText.bitwardenIncompatibleStep2) From d0949310277f724c7875dbcc4fb0f20b0ad8e0ab Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 10 May 2024 11:25:14 +0200 Subject: [PATCH 095/134] sync error notify user (#2712) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207178541221451/f **Description**: Notify user of more sync errors https://app.asana.com/0/0/1207045332341265/f --- DuckDuckGo.xcodeproj/project.pbxproj | 88 +- DuckDuckGo/Application/AppDelegate.swift | 3 +- .../Common/Extensions/NSAlertExtension.swift | 18 +- DuckDuckGo/Common/Localizables/UserText.swift | 28 +- .../Utilities/UserDefaultsWrapper.swift | 8 + DuckDuckGo/Localizable.xcstrings | 2831 ++++++----------- .../Model/PreferencesSection.swift | 3 +- .../Model/SyncPausedStateManaging.swift | 40 + .../Preferences/Model/SyncPreferences.swift | 79 +- .../View/PreferencesSyncView.swift | 3 +- DuckDuckGo/Sync/SyncBookmarksAdapter.swift | 67 +- DuckDuckGo/Sync/SyncCredentialsAdapter.swift | 64 +- DuckDuckGo/Sync/SyncDataProviders.swift | 8 +- DuckDuckGo/Sync/SyncErrorHandler.swift | 381 ++- .../Sync/Utilities/SyncAlertsPresenter.swift | 42 + .../ViewModels/ManagementViewModel.swift | 13 + .../ManagementView/SyncEnabledView.swift | 49 +- .../ManagementView/SyncWarningMessage.swift | 2 +- .../MockAppearancePreferencesPersistor.swift | 46 + UnitTests/Sync/Mocks/MockDDGSyncing.swift | 103 + .../Mocks/MockSyncPausedStateManaging.swift | 57 + .../Sync/SyncBookmarksAdapterTests.swift | 82 + .../Sync/SyncCredentialsAdapterTests.swift | 120 + UnitTests/Sync/SyncErrorHandlerTests.swift | 561 ++++ UnitTests/Sync/SyncPreferencesTests.swift | 181 +- 25 files changed, 2827 insertions(+), 2050 deletions(-) create mode 100644 DuckDuckGo/Preferences/Model/SyncPausedStateManaging.swift create mode 100644 DuckDuckGo/Sync/Utilities/SyncAlertsPresenter.swift create mode 100644 UnitTests/Sync/Mocks/MockAppearancePreferencesPersistor.swift create mode 100644 UnitTests/Sync/Mocks/MockDDGSyncing.swift create mode 100644 UnitTests/Sync/Mocks/MockSyncPausedStateManaging.swift create mode 100644 UnitTests/Sync/SyncBookmarksAdapterTests.swift create mode 100644 UnitTests/Sync/SyncCredentialsAdapterTests.swift create mode 100644 UnitTests/Sync/SyncErrorHandlerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b0b0891703..ac682a5f8c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 026ADE1426C3010C002518EE /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 028904202A7B25380028369C /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */; }; 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */; }; - 02C0737D2BE5B7E000BFE2F5 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02C0737C2BE5B7E000BFE2F5 /* InfoPlist.xcstrings */; }; 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142879D924CE1179005419BB /* SuggestionViewModelTests.swift */; }; 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142879DB24CE1185005419BB /* SuggestionContainerViewModelTests.swift */; }; 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1430DFF424D0580F00B8978C /* TabBarViewController.swift */; }; @@ -1418,6 +1417,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 */; }; + 561D29C22BDA745A007B91D0 /* MockSyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */; }; + 561D29C32BDA745B007B91D0 /* MockSyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */; }; + 561D29C62BDA74ED007B91D0 /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */; }; + 561D29C72BDA74F4007B91D0 /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */; }; + 561D29CA2BDA752F007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C82BDA751E007B91D0 /* MockAppearancePreferencesPersistor.swift */; }; + 561D29CB2BDA7530007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C82BDA751E007B91D0 /* MockAppearancePreferencesPersistor.swift */; }; 561D66662B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 561D66672B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; @@ -1429,15 +1434,26 @@ 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B195C29CDB692007E38F4 /* MoreOptionsMenuTests.swift */; }; 566B196529CDB828007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B196029CDB7C9007E38F4 /* CapturingOptionsButtonMenuDelegate.swift */; }; 566B196629CDB829007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B196029CDB7C9007E38F4 /* CapturingOptionsButtonMenuDelegate.swift */; }; + 566B73692BECBF8400FF1959 /* SyncAlertsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B73682BECBF8400FF1959 /* SyncAlertsPresenter.swift */; }; + 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 */; }; 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 */; }; 567DA94629E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA94429E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift */; }; + 5681ED402BDB955100F59729 /* SyncCredentialsAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5681ED3F2BDB955100F59729 /* SyncCredentialsAdapterTests.swift */; }; + 5681ED412BDB955100F59729 /* SyncCredentialsAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5681ED3F2BDB955100F59729 /* SyncCredentialsAdapterTests.swift */; }; + 5681ED432BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5681ED422BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift */; }; + 5681ED442BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5681ED422BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift */; }; + 5681ED462BDBAF6E00F59729 /* SyncErrorHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5681ED452BDBAF6E00F59729 /* SyncErrorHandlerTests.swift */; }; + 5681ED472BDBAF6E00F59729 /* SyncErrorHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5681ED452BDBAF6E00F59729 /* SyncErrorHandlerTests.swift */; }; 5682C69429B79B57004DE3C8 /* TabBarViewItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5601FECC29B7973D00068905 /* TabBarViewItemTests.swift */; }; 569277C129DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C029DDCBB500B633EF /* HomePageContinueSetUpModel.swift */; }; 569277C229DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C029DDCBB500B633EF /* HomePageContinueSetUpModel.swift */; }; 569277C429DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */; }; 569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */; }; + 56A51CE82BE65B340098722D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56A51CE72BE65B340098722D /* InfoPlist.xcstrings */; }; 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 */; }; @@ -2757,7 +2773,6 @@ 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedCodingExtension.swift; sourceTree = ""; }; 026ADE1326C3010C002518EE /* macos-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "macos-config.json"; sourceTree = ""; }; 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProviderTests.swift; sourceTree = ""; }; - 02C0737C2BE5B7E000BFE2F5 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 142879D924CE1179005419BB /* SuggestionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionViewModelTests.swift; sourceTree = ""; }; 142879DB24CE1185005419BB /* SuggestionContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionContainerViewModelTests.swift; sourceTree = ""; }; 1430DFF424D0580F00B8978C /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; @@ -3254,6 +3269,9 @@ 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 = ""; }; + 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSyncPausedStateManaging.swift; sourceTree = ""; }; + 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDDGSyncing.swift; sourceTree = ""; }; + 561D29C82BDA751E007B91D0 /* MockAppearancePreferencesPersistor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppearancePreferencesPersistor.swift; sourceTree = ""; }; 561D66692B95C45A008ACC5C /* Suggestion.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Suggestion.storyboard; sourceTree = ""; }; 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferencesTests.swift; sourceTree = ""; }; 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDefaultBrowserProvider.swift; sourceTree = ""; }; @@ -3261,10 +3279,16 @@ 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalPathsTests.swift; sourceTree = ""; }; 566B195C29CDB692007E38F4 /* MoreOptionsMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreOptionsMenuTests.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; + 5681ED422BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapterTests.swift; sourceTree = ""; }; + 5681ED452BDBAF6E00F59729 /* SyncErrorHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandlerTests.swift; sourceTree = ""; }; 569277C029DDCBB500B633EF /* HomePageContinueSetUpModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageContinueSetUpModel.swift; sourceTree = ""; }; 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueSetUpModelTests.swift; sourceTree = ""; }; + 56A51CE72BE65B340098722D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; 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 = ""; }; 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageTabExtensionTest.swift; sourceTree = ""; }; @@ -4630,6 +4654,7 @@ 3775913429AB99DA00E26367 /* Sync */ = { isa = PBXGroup; children = ( + 566B73672BECBF4400FF1959 /* Utilities */, 377D801A2AB47FA1002AF251 /* SettingSyncHandlers */, 370A34B02AB24E3700C77F7C /* SyncDebugMenu.swift */, 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */, @@ -4785,6 +4810,7 @@ 37CD54C527F2FDD100F1F7B9 /* AboutModel.swift */, 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */, 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */, + 566B736B2BECC3C600FF1959 /* SyncPausedStateManaging.swift */, ); path = Model; sourceTree = ""; @@ -5727,10 +5753,24 @@ path = View; sourceTree = ""; }; + 561D29BF2BDA7419007B91D0 /* Mocks */ = { + isa = PBXGroup; + children = ( + 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */, + 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */, + 561D29C82BDA751E007B91D0 /* MockAppearancePreferencesPersistor.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 5629846D2AC460DF00AC20EB /* Sync */ = { isa = PBXGroup; children = ( + 561D29BF2BDA7419007B91D0 /* Mocks */, 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */, + 5681ED3F2BDB955100F59729 /* SyncCredentialsAdapterTests.swift */, + 5681ED422BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift */, + 5681ED452BDBAF6E00F59729 /* SyncErrorHandlerTests.swift */, ); path = Sync; sourceTree = ""; @@ -5762,6 +5802,14 @@ path = Mocks; sourceTree = ""; }; + 566B73672BECBF4400FF1959 /* Utilities */ = { + isa = PBXGroup; + children = ( + 566B73682BECBF8400FF1959 /* SyncAlertsPresenter.swift */, + ); + path = Utilities; + sourceTree = ""; + }; 56BA1E852BAC820D001CF69F /* UserScripts */ = { isa = PBXGroup; children = ( @@ -7928,7 +7976,7 @@ B6E6B9F42BA1FD90008AA7E1 /* sandbox-test-tool */ = { isa = PBXGroup; children = ( - 02C0737C2BE5B7E000BFE2F5 /* InfoPlist.xcstrings */, + 56A51CE72BE65B340098722D /* InfoPlist.xcstrings */, B6E6BA212BA2E4FB008AA7E1 /* Info.plist */, B6E6B9F52BA1FD90008AA7E1 /* SandboxTestTool.swift */, B6E6BA222BA2EDDE008AA7E1 /* FileReadResult.swift */, @@ -8633,7 +8681,7 @@ B6E6B9EF2BA1FD90008AA7E1 /* Sources */, B6E6B9F02BA1FD90008AA7E1 /* Frameworks */, B6AEB5532BA3029B00781A09 /* Cleanup entitlements */, - 02C0737E2BE5B7E000BFE2F5 /* Resources */, + 56A51CE92BE65B340098722D /* Resources */, ); buildRules = ( ); @@ -8790,14 +8838,6 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 02C0737E2BE5B7E000BFE2F5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 02C0737D2BE5B7E000BFE2F5 /* InfoPlist.xcstrings in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 3706FCB1293F65D500E42796 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -8939,6 +8979,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 56A51CE92BE65B340098722D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 56A51CE82BE65B340098722D /* InfoPlist.xcstrings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 56F5C54D2B8F5FDB00E012C7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9539,6 +9587,7 @@ B6E6B9E42BA1F5F1008AA7E1 /* FilePresenter.swift in Sources */, 3706FAF0293F65D500E42796 /* YoutubePlayerNavigationHandler.swift in Sources */, 37197EA32942441D00394917 /* UserDialogRequest.swift in Sources */, + 566B736A2BECC02D00FF1959 /* SyncAlertsPresenter.swift in Sources */, 3706FAF1293F65D500E42796 /* PreferencesAboutView.swift in Sources */, 3706FAF2293F65D500E42796 /* ContentBlocking.swift in Sources */, 31F2D2002AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift in Sources */, @@ -9551,6 +9600,7 @@ B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */, 3706FAF4293F65D500E42796 /* SafariBookmarksReader.swift in Sources */, 31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */, + 566B736D2BECC3C600FF1959 /* SyncPausedStateManaging.swift in Sources */, B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */, 3706FAF5293F65D500E42796 /* SafariVersionReader.swift in Sources */, 3706FAF6293F65D500E42796 /* LoginFaviconView.swift in Sources */, @@ -10217,6 +10267,7 @@ 3706FDF7293F661700E42796 /* WebViewExtensionTests.swift in Sources */, 9FA5A0AA2BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */, 56534DEE29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, + 561D29C32BDA745B007B91D0 /* MockSyncPausedStateManaging.swift in Sources */, 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, 3706FDF9293F661700E42796 /* TabViewModelTests.swift in Sources */, @@ -10254,6 +10305,7 @@ 3706FE10293F661700E42796 /* TestNavigationDelegate.swift in Sources */, 1D9FDEC12B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, 3706FE11293F661700E42796 /* URLSuggestedFilenameTests.swift in Sources */, + 5681ED472BDBAF6E00F59729 /* SyncErrorHandlerTests.swift in Sources */, 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, 3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */, @@ -10271,6 +10323,7 @@ 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, + 561D29C72BDA74F4007B91D0 /* MockDDGSyncing.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, @@ -10376,6 +10429,7 @@ 3706FE5B293F661700E42796 /* FirefoxLoginReaderTests.swift in Sources */, 1D1C36E429FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, 37716D8029707E5D00A9FC6D /* FireproofingReferenceTests.swift in Sources */, + 561D29CB2BDA7530007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */, B6AA64742994B43300D99CD6 /* FutureExtensionTests.swift in Sources */, 3706FE5C293F661700E42796 /* DuckPlayerPreferencesTests.swift in Sources */, 1D9FDEB82B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, @@ -10436,9 +10490,11 @@ 1D8C2FF12B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */, 3706FE7D293F661700E42796 /* WKDownloadMock.swift in Sources */, 3706FE7E293F661700E42796 /* RunLoopExtensionTests.swift in Sources */, + 5681ED412BDB955100F59729 /* SyncCredentialsAdapterTests.swift in Sources */, 3706FE7F293F661700E42796 /* FileDownloadManagerMock.swift in Sources */, 3706FE80293F661700E42796 /* DeallocationTests.swift in Sources */, 3706FE81293F661700E42796 /* PermissionModelTests.swift in Sources */, + 5681ED442BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */, B60C6F8529B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, 567DA94629E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, @@ -11056,6 +11112,7 @@ 4BA1A6BD258B082300F6F690 /* EncryptionKeyStore.swift in Sources */, B6C00ED7292FB4B4009C73A6 /* TabExtensionsBuilder.swift in Sources */, 4BE65474271FCD40008D1D63 /* PasswordManagementIdentityItemView.swift in Sources */, + 566B73692BECBF8400FF1959 /* SyncAlertsPresenter.swift in Sources */, B68412142B694BA10092F66A /* NSObject+performSelector.m in Sources */, B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */, 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, @@ -11330,6 +11387,7 @@ AA8EDF2724923EC70071C2E8 /* StringExtension.swift in Sources */, 85378DA2274E7F25007C5CBF /* EmailManagerRequestDelegate.swift in Sources */, 1D43EB36292ACE690065E5D6 /* ApplicationVersionReader.swift in Sources */, + 566B736C2BECC3C600FF1959 /* SyncPausedStateManaging.swift in Sources */, 4BD18F01283F0BC500058124 /* BookmarksBarViewController.swift in Sources */, 379DE4BD27EA31AC002CC3DE /* PreferencesAutofillView.swift in Sources */, 1DCFBC8A29ADF32B00313531 /* BurnerHomePageView.swift in Sources */, @@ -11580,6 +11638,7 @@ B67C6C3D2654B897006C872E /* WebViewExtensionTests.swift in Sources */, 4BA1A6DE258C100A00F6F690 /* FileStoreTests.swift in Sources */, AAC9C01C24CB594C00AD1325 /* TabViewModelTests.swift in Sources */, + 561D29CA2BDA752F007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */, 567DA93F29E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 37CD54B727F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift in Sources */, 9F3344622BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, @@ -11617,6 +11676,7 @@ B630793526731BC400DCEE41 /* URLSuggestedFilenameTests.swift in Sources */, B603974E29C1F93600902A34 /* TabPermissionsTests.swift in Sources */, 85AC3B4925DAC9BD00C7D2AA /* ConfigurationStorageTests.swift in Sources */, + 561D29C22BDA745A007B91D0 /* MockSyncPausedStateManaging.swift in Sources */, 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */, AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, @@ -11671,6 +11731,7 @@ 373A1AAA283ED86C00586521 /* BookmarksHTMLReaderTests.swift in Sources */, 560C3FFC2BC9911000F589CE /* PermanentSurveyManagerTests.swift in Sources */, 317295D42AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, + 5681ED462BDBAF6E00F59729 /* SyncErrorHandlerTests.swift in Sources */, 9FBD84772BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift in Sources */, AA9C363025518CA9004B1BA3 /* FireTests.swift in Sources */, B6106BB126A7D8720013B453 /* PermissionStoreTests.swift in Sources */, @@ -11710,6 +11771,7 @@ 4B9292C02667103100AD2C21 /* BookmarkManagedObjectTests.swift in Sources */, 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */, 4B723E0626B0003E00E14D75 /* CSVParserTests.swift in Sources */, + 561D29C62BDA74ED007B91D0 /* MockDDGSyncing.swift in Sources */, B60C6F8429B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */, @@ -11736,6 +11798,8 @@ B6106BB326A7F4AA0013B453 /* GeolocationServiceMock.swift in Sources */, 4B8AC93D26B49BE600879451 /* FirefoxLoginReaderTests.swift in Sources */, 3714B1E728EDB7FA0056C57A /* DuckPlayerPreferencesTests.swift in Sources */, + 5681ED402BDB955100F59729 /* SyncCredentialsAdapterTests.swift in Sources */, + 5681ED432BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */, 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */, 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */, 1D1C36E329FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 3d3ad5d5d2..0b2cc141b2 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -424,7 +424,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #else let environment = defaultEnvironment #endif - let syncDataProviders = SyncDataProviders(bookmarksDatabase: BookmarkDatabase.shared.db) + let syncErrorHandler = SyncErrorHandler() + let syncDataProviders = SyncDataProviders(bookmarksDatabase: BookmarkDatabase.shared.db, syncErrorHandler: syncErrorHandler) let syncService = DDGSync( dataProvidersSource: syncDataProviders, errorEvents: SyncErrorHandler(), diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 017389de08..46fd9119ee 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -168,23 +168,13 @@ extension NSAlert { return alert } - static func syncBookmarksPaused() -> NSAlert { + static func syncPaused(title: String, informative: String) -> NSAlert { let alert = NSAlert() - alert.messageText = UserText.syncBookmarkPausedAlertTitle - alert.informativeText = UserText.syncBookmarkPausedAlertDescription + alert.messageText = title + alert.informativeText = informative alert.alertStyle = .warning alert.addButton(withTitle: UserText.ok) - alert.addButton(withTitle: UserText.learnMore) - return alert - } - - static func syncCredentialsPaused() -> NSAlert { - let alert = NSAlert() - alert.messageText = UserText.syncCredentialsPausedAlertTitle - alert.informativeText = UserText.syncCredentialsPausedAlertDescription - alert.alertStyle = .warning - alert.addButton(withTitle: UserText.ok) - alert.addButton(withTitle: UserText.learnMore) + alert.addButton(withTitle: UserText.syncErrorAlertAction) return alert } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b6c361c836..b671be4b1e 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -562,13 +562,33 @@ struct UserText { static let general = NSLocalizedString("preferences.general", value: "General", comment: "Title of the option to show the General preferences") static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Title of the option to show the Sync preferences") static let syncAutoLockPrompt = NSLocalizedString("preferences.sync.auto-lock-prompt", value: "Unlock device to setup Sync & Backup", comment: "Reason for auth when setting up Sync") - static let syncBookmarkPausedAlertTitle = NSLocalizedString("alert.sync-bookmarks-paused-title", value: "Bookmarks Sync is Paused", comment: "Title for alert shown when sync bookmarks paused for too many items") - static let syncBookmarkPausedAlertDescription = NSLocalizedString("alert.sync-bookmarks-paused-description", value: "You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up.", comment: "Description for alert shown when sync bookmarks paused for too many items") - static let syncCredentialsPausedAlertTitle = NSLocalizedString("alert.sync-credentials-paused-title", value: "Passwords Sync is Paused", comment: "Title for alert shown when sync credentials paused for too many items") - static let syncCredentialsPausedAlertDescription = NSLocalizedString("alert.sync-credentials-paused-description", value: "You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up.", comment: "Description for alert shown when sync credentials paused for too many items") + static let syncBookmarkPausedAlertTitle = NSLocalizedString("alert.sync-bookmarks-paused-title", value: "Bookmark Sync is Paused", comment: "Title for alert shown when sync bookmarks paused for too many items") + static let syncBookmarkPausedAlertDescription = NSLocalizedString("alert.sync-bookmarks-paused-description", value: "You've reached the maximum number of bookmarks. Please delete some bookmarks to resume sync.", comment: "Description for alert shown when sync bookmarks paused for too many items") + static let syncCredentialsPausedAlertTitle = NSLocalizedString("alert.sync-credentials-paused-title", value: "Password Sync is Paused", comment: "Title for alert shown when sync credentials paused for too many items") + static let syncCredentialsPausedAlertDescription = NSLocalizedString("alert.sync-credentials-paused-description", value: "You've reached the maximum number of passwords. Please delete some passwords to resume sync.", comment: "Description for alert shown when sync credentials paused for too many items") static let syncPausedTitle = NSLocalizedString("alert.sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message") static let syncUnavailableMessage = NSLocalizedString("alert.sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("alert.sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") + static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error-title", value: "Sync Error", comment: "Title for alert shown when sync error occurs") + static let syncPausedAlertTitle = NSLocalizedString("alert.sync-paused-title", value: "Sync is Paused", comment: "Title for alert shown when sync paused for an error") + static let syncInvalidLoginAlertDescription = NSLocalizedString("alert.sync-invalid-login-error-description", value: "Sync has been paused. If you want to continue syncing this device, reconnect using another device or your recovery code.", comment: "Description for alert shown when sync error occurs because of invalid login credentials") + static let syncTooManyRequestsAlertDescription = NSLocalizedString("alert.sync-too-many-requests-error-description", value: "Sync & Backup is temporarily unavailable.", comment: "Description for alert shown when sync error occurs because of too many requests") + static let syncBookmarksBadRequestAlertDescription = NSLocalizedString("alert.sync-bookmarks-bad-data-error-description", value: "Some bookmarks are formatted incorrectly or too long and were not synced.", comment: "Description for alert shown when sync error occurs because of bad data") + static let syncCredentialsBadRequestAlertDescription = NSLocalizedString("alert.sync-credentials-bad-data-error-description", value: "Some passwords are formatted incorrectly or too long and were not synced.", comment: "Description for alert shown when sync error occurs because of bad data") + static let syncErrorAlertAction = NSLocalizedString("alert.sync-error-action", value: "Sync Settings", comment: "Sync error alert action button title, takes the user to the sync settings page.") + + // Sync Errors + static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", value: "Sync Paused", comment: "Title for sync limits exceeded warning") + static let syncErrorTitle = NSLocalizedString("alert.sync.warning.sync-error", value: "Sync Error", comment: "Title of the warning message that tells the user that there was an error with the sync feature.") + static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", value: "You've reached the maximum number of bookmarks. Please delete some to resume sync.", comment: "Description for sync bookmarks limits exceeded warning") + static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "You've reached the maximum number of passwords. Please delete some to resume sync.", comment: "Description for sync credentials limits exceeded warning") + static let invalidLoginCredentialErrorDescription = NSLocalizedString("prefrences.sync.invalid-login-description", value: "Sync encountered an error. Try disabling sync on this device and then reconnect using another device or your recovery code.", comment: "Description invalid credentials error when syncing.") + static let tooManyRequestsErrorDescription = NSLocalizedString("prefrences.sync.bookmarks.too-many-requests", value: "Sync & Backup is temporarily unavailable.", comment: "Description of too many requests error when syncing.") + static let syncBookmarksBadRequestErrorDescription = NSLocalizedString("prefrences.sync.bad.request.description", value: "Some bookmarks are formatted incorrectly or too long and were not synced.", comment: "Description of incorrectly formatted data error when syncing.") + static let syncCredentialsBadRequestErrorDescription = NSLocalizedString("prefrences.sync.credentials.bad.request.description", value: "Some passwords are formatted incorrectly or too long and were not synced.", comment: "Description of incorrectly formatted data error when syncing.") + static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") + static let privacyProtections = NSLocalizedString("preferences.privacy-protections", value: "Privacy Protections", comment: "The section header in Preferences representing browser features related to privacy protection") static let mainSettings = NSLocalizedString("preferences.main-settings", value: "Main Settings", comment: "Section header in Preferences for main settings") static let preferencesOn = NSLocalizedString("preferences.on", value: "On", comment: "Status indicator of a browser privacy protection feature.") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 5876bef57d..7579b0c4ea 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -182,13 +182,21 @@ public struct UserDefaultsWrapper { case favoritesDisplayMode = "sync.favorites-display-mode" case syncBookmarksPaused = "sync.bookmarks-paused" case syncCredentialsPaused = "sync.credentials-paused" + case syncIsPaused = "sync.paused" case syncBookmarksPausedErrorDisplayed = "sync.bookmarks-paused-error-displayed" case syncCredentialsPausedErrorDisplayed = "sync.credentials-paused-error-displayed" + case syncInvalidLoginPausedErrorDisplayed = "sync.invalid-login-paused-error-displayed" case syncIsFaviconsFetcherEnabled = "sync.is-favicons-fetcher-enabled" case syncIsEligibleForFaviconsFetcherOnboarding = "sync.is-eligible-for-favicons-fetcher-onboarding" case syncDidPresentFaviconsFetcherOnboarding = "sync.did-present-favicons-fetcher-onboarding" case syncDidMigrateToImprovedListsHandling = "sync.did-migrate-to-improved-lists-handling" case syncDidShowSyncPausedByFeatureFlagAlert = "sync.did-show-sync-paused-by-feature-flag-alert" + case syncLastErrorNotificationTime = "sync.last-error-notification-time" + case syncLastSuccesfullTime = "sync.last-time-success" + case syncLastNonActionableErrorCount = "sync.non-actionable-error-count" + case syncCurrentAllPausedError = "sync.current-all-paused-error" + case syncCurrentBookmarksPausedError = "sync.current-bookmarks-paused-error" + case syncCurrentCredentialsPausedError = "sync.current-credentials-paused-error" // Subscription diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 71b681ff31..2bb58ff31c 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -316,59 +316,6 @@ } } }, - "••••••••••••" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "••••••••••••" - } - } - } - }, "`continue`" : { "comment" : "Continue button", "extractionState" : "extracted_with_value", @@ -819,60 +766,6 @@ } } }, - "Add Folder" : { - "comment" : "Add Folder popover: Create folder button", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ordner hinzufügen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Añadir carpeta" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajouter le dossier" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi cartella" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Map toevoegen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj folder" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar pasta" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить папку" - } - } - } - }, "add.bookmark.name" : { "comment" : "New bookmark folder dialog folder name field heading", "extractionState" : "extracted_with_value", @@ -1166,60 +1059,6 @@ } } }, - "Address:" : { - "comment" : "Add Bookmark dialog bookmark url field heading", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adresse:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dirección:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adresse :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Indirizzo:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adres:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adres:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endereço:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Адрес:" - } - } - } - }, "address.bar.placeholder" : { "comment" : "Empty Address Bar placeholder text displayed on the new tab page.", "extractionState" : "extracted_with_value", @@ -1460,6 +1299,66 @@ } } }, + "alert.sync-bookmarks-bad-data-error-description" : { + "comment" : "Description for alert shown when sync error occurs because of bad data", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einige Lesezeichen sind falsch formatiert oder zu lang und wurden nicht synchronisiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Some bookmarks are formatted incorrectly or too long and were not synced." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algunos marcadores tienen un formato incorrecto o demasiado largo y no se han sincronizado." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certains signets sont mal formatés ou trop longs et n'ont pas été synchronisés.\n" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alcuni segnalibri sono formattati in modo errato o sono troppo lunghi e non sono stati sincronizzati." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sommige bladwijzers hebben de verkeerde indeling of zijn te lang en werden niet gesynchroniseerd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niektóre zakładki mają niepoprawny format lub są zbyt długie i nie zostały zsynchronizowane." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alguns marcadores estão formatados incorretamente ou são demasiado longos e não foram sincronizados." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые закладки не синхронизируются из-за неверного формата или превышения ограничений по длине." + } + } + } + }, "alert.sync-bookmarks-paused-description" : { "comment" : "Description for alert shown when sync bookmarks paused for too many items", "extractionState" : "extracted_with_value", @@ -1467,55 +1366,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Du hast das Limit für die Synchronisierung von Lesezeichen überschritten. Versuche, einige Lesezeichen zu löschen. Bis dieses Problem behoben ist, werden deine Lesezeichen nicht gesichert." + "value" : "Du hast die maximale Anzahl von Lesezeichen erreicht. Bitte lösche einige Lesezeichen, um die Synchronisierung fortzusetzen." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up." + "value" : "You've reached the maximum number of bookmarks. Please delete some bookmarks to resume sync." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Has superado el límite de sincronización de marcadores. Prueba a eliminar algunos marcadores. No se realizará una copia de seguridad de los marcadores hasta que se resuelva esto." + "value" : "Has alcanzado el número máximo de marcadores. Elimina algunos marcadores para reanudar la sincronización." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Vous avez dépassé le nombre maximal de signets à synchroniser. Veuillez en supprimer quelques-uns pour pouvoir sauvegarder vos signets." + "value" : "Vous avez atteint le nombre maximal de signets. Veuillez en supprimer quelques-uns pour reprendre la synchronisation." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Hai superato il limite di sincronizzazione dei segnalibri. Prova a eliminarne qualcuno. Fino a quando il problema non verrà risolto, non sarà eseguito il backup dei tuoi segnalibri." + "value" : "Hai raggiunto il numero massimo di segnalibri. Elimina alcuni segnalibri per riprendere la sincronizzazione." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je hebt de synchronisatielimiet voor bladwijzers overschreden. Probeer enkele bladwijzers te verwijderen. Er wordt geen back-up van je bladwijzers gemaakt totdat dit probleem is opgelost." + "value" : "Je hebt het maximumaantal bladwijzers bereikt. Verwijder enkele bladwijzers om de synchronisatie te hervatten." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przekroczono limit synchronizacji zakładek. Spróbuj usunąć niektóre zakładki. Dopóki ten problem nie zostanie rozwiązany, nie będzie tworzona kopia zapasowa zakładek." + "value" : "Osiągnięto maksymalną liczbę zakładek. Usuń część zakładek, aby wznowić synchronizację." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Excedeste o limite de sincronização de favoritos. Experimenta eliminar alguns favoritos. Não é possível fazer uma cópia de segurança dos favoritos até este problema estar resolvido." + "value" : "Atingiste o número máximo de marcadores. Elimina alguns marcadores para retomar a sincronização." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вы превысили лимит синхронизации закладок. Попробуйте удалить некоторые из них. Пока вы не устраните эту проблему, резервная копия закладок создаваться не будет." + "value" : "Достигнуто максимальное число закладок. Чтобы возобновить синхронизацию, удалите некоторые из них." } } } @@ -1533,7 +1432,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Bookmarks Sync is Paused" + "value" : "Bookmark Sync is Paused" } }, "es" : { @@ -1551,7 +1450,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La sincronizzazione dei segnalibri è in pausa" + "value" : "Sync dei segnalibri è in pausa" } }, "nl" : { @@ -1569,7 +1468,7 @@ "pt" : { "stringUnit" : { "state" : "translated", - "value" : "A sincronização de favoritos está em pausa" + "value" : "A sincronização de marcadores está em pausa" } }, "ru" : { @@ -1580,6 +1479,66 @@ } } }, + "alert.sync-credentials-bad-data-error-description" : { + "comment" : "Description for alert shown when sync error occurs because of bad data", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einige Passwörter sind falsch formatiert oder zu lang und wurden nicht synchronisiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Some passwords are formatted incorrectly or too long and were not synced." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algunas contraseñas tienen un formato incorrecto o demasiado largo y no se han sincronizado." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certains mots de passe sont mal formatés ou trop longs et n'ont pas été synchronisés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alcune password sono formattate in modo errato o sono troppo lunghe e non sono state sincronizzate." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sommige wachtwoorden hebben de verkeerde indeling of zijn te lang en werden niet gesynchroniseerd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niektóre hasła mają niepoprawny format lub są zbyt długie i nie zostały zsynchronizowane." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algumas palavras-passe estão formatadas incorretamente ou são demasiado longas e não foram sincronizadas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые пароли не синхронизируются из-за неверного формата или превышения ограничений по длине." + } + } + } + }, "alert.sync-credentials-paused-description" : { "comment" : "Description for alert shown when sync credentials paused for too many items", "extractionState" : "extracted_with_value", @@ -1587,55 +1546,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Du hast das Limit für die Synchronisierung von Passwörtern überschritten. Versuche, einige Passwörter zu löschen. Bis dieses Problem behoben ist, werden deine Passwörter nicht gesichert." + "value" : "Du hast die maximale Anzahl von Passwörtern erreicht. Lösche Passwörter, um die Synchronisierung fortzusetzen." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up." + "value" : "You've reached the maximum number of passwords. Please delete some passwords to resume sync." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ha superado el límite de sincronización de contraseñas. Prueba a eliminar algunas contraseñas. No se realizará una copia de seguridad de las contraseñas hasta que se resuelva esto." + "value" : "Has alcanzado el número máximo de contraseñas. Elimina algunas contraseñas para reanudar la sincronización." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Vous avez dépassé le nombre maximal de mots de passe à synchroniser. Veuillez en supprimer quelques-uns pour pouvoir sauvegarder vos mots de passe." + "value" : "Vous avez atteint le nombre maximal de mots de passe. Veuillez en supprimer quelques-uns pour reprendre la synchronisation." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Hai superato il limite di sincronizzazione delle password. Prova a eliminarne qualcuna. Fino a quando il problema non verrà risolto, non sarà eseguito il backup delle tue password." + "value" : "Hai raggiunto il numero massimo di password. Elimina alcune password per riprendere la sincronizzazione." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je hebt de synchronisatielimiet voor wachtwoorden overschreden. Probeer enkele wachtwoorden te verwijderen. Er wordt geen back-up van je wachtwoorden gemaakt totdat dit probleem is opgelost." + "value" : "Je hebt het maximumaantal wachtwoorden bereikt. Verwijder enkele wachtwoorden om de synchronisatie te hervatten." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przekroczono limit synchronizacji haseł. Spróbuj usunąć niektóre hasła. Dopóki ten problem nie zostanie rozwiązany, nie będzie tworzona kopia zapasowa haseł." + "value" : "Osiągnięto maksymalną liczbę haseł. Usuń część haseł, aby wznowić synchronizację." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Excedeste o limite de sincronização de palavras-passe. Experimenta eliminar algumas palavras-passe. Não é possível fazer uma cópia de segurança das palavras-passe até este problema estar resolvido." + "value" : "Atingiste o número máximo de palavras-passe. Elimina algumas palavras-passe para retomar a sincronização." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вы превысили лимит синхронизации паролей. Попробуйте удалить некоторые из них. Пока вы не устраните эту проблему, резервная копия закладок создаваться не будет." + "value" : "Достигнуто максимальное число паролей. Чтобы возобновить синхронизацию, удалите некоторые из них." } } } @@ -1647,13 +1606,13 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Passwortsynchronisierung ist angehalten" + "value" : "Kennwortsynchronisierung ist angehalten" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Passwords Sync is Paused" + "value" : "Password Sync is Paused" } }, "es" : { @@ -1671,7 +1630,7 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La sincronizzazione delle password è in pausa" + "value" : "Sync delle password è in pausa" } }, "nl" : { @@ -1700,6 +1659,306 @@ } } }, + "alert.sync-error-action" : { + "comment" : "Sync error alert action button title, takes the user to the sync settings page.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisierungseinstellungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync Settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajustes de sincronización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réglages de synchronisation" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impostazioni Sync" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisatie-instellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia synchronizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definições de sincronização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Синхронизация настроек" + } + } + } + }, + "alert.sync-error-title" : { + "comment" : "Title for alert shown when sync error occurs", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisierungsfehler" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync Error" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de sincronización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur de synchronisation" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore di sincronizzazione" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisatiefout" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd synchronizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro de sincronização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка синхронизации" + } + } + } + }, + "alert.sync-invalid-login-error-description" : { + "comment" : "Description for alert shown when sync error occurs because of invalid login credentials", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisierung wurde angehalten. Wenn du die Synchronisierung dieses Geräts fortsetzen möchtest, stelle die Verbindung mit einem anderen Gerät oder deinem Wiederherstellungscode wieder her." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync has been paused. If you want to continue syncing this device, reconnect using another device or your recovery code." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La sincronización se ha pausado. Si quieres seguir sincronizando este dispositivo, vuelve a conectarte con otro dispositivo o con tu código de recuperación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La synchronisation a été suspendue. Si vous souhaitez continuer à synchroniser cet appareil, reconnectez-vous à l’aide d’un autre appareil ou de votre code de récupération." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La sincronizzazione è stata sospesa. Se vuoi continuare a sincronizzare questo dispositivo, riconnettiti utilizzando un altro dispositivo o il tuo codice di ripristino." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisatie is onderbroken. Als je dit apparaat wilt blijven synchroniseren, maak dan opnieuw verbinding met een ander apparaat of met je herstelcode." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronizacja została wstrzymana. Jeśli chcesz kontynuować synchronizowanie tego urządzenia, ponownie nawiąż połączenie za pomocą innego urządzenia lub użyj kodu odzyskiwania." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A sincronização foi pausada. Se quiseres continuar a sincronizar este dispositivo, inicia sessão novamente noutro dispositivo ou com o teu código de recuperação." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Синхронизация приостановлена. Чтобы возобновить ее, выполните повторную привязку, используя другое устройство или код восстановления." + } + } + } + }, + "alert.sync-paused-title" : { + "comment" : "Title for alert shown when sync paused for an error", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisierung angehalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync is Paused" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La sincronización está en pausa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La synchronisation est suspendue" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync è in pausa" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisatie is gepauzeerd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronizacja jest wstrzymana" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A sincronização está em pausa" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Синхронизация приостановлена" + } + } + } + }, + "alert.sync-too-many-requests-error-description" : { + "comment" : "Description for alert shown when sync error occurs because of too many requests", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup ist vorübergehend nicht verfügbar." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup is temporarily unavailable." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La sincronización y la copia de seguridad no están disponibles temporalmente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup est temporairement indisponible." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup non è temporaneamente disponibile." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "'Synchronisatie en back-up' is tijdelijk niet beschikbaar." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Funkcja Sync & Backup jest tymczasowo niedostępna." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Sync & Backup está temporariamente indisponível." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Функция «Синхронизация и резервное копирование» временно недоступна." + } + } + } + }, "alert.sync.warning.data-syncing-disabled-upgrade-required" : { "comment" : "Data syncing unavailable warning message", "extractionState" : "extracted_with_value", @@ -1760,6 +2019,66 @@ } } }, + "alert.sync.warning.sync-error" : { + "comment" : "Title of the warning message that tells the user that there was an error with the sync feature.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisierungsfehler" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync Error" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de sincronización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur de synchronisation" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore di sincronizzazione" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisatiefout" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd synchronizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro de sincronização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка синхронизации" + } + } + } + }, "alert.sync.warning.sync-paused" : { "comment" : "Title of the warning message", "extractionState" : "extracted_with_value", @@ -3013,66 +3332,6 @@ } } }, - "autoconsent.title" : { - "comment" : "Autoconsent settings section title", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cookie-Pop-ups" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Cookie Pop-ups" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ventanas emergentes de cookies" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fenêtres contextuelles de cookies" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pop-up per i cookie" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pop-ups voor cookies" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wyskakujące okienka dotyczące plików cookie" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pop-ups de cookies" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Всплывающие окна куки" - } - } - } - }, "autofill.addresses" : { "comment" : "Autofill autosaved data type", "extractionState" : "extracted_with_value", @@ -7624,60 +7883,6 @@ } } }, - "Bookmark Added" : { - "comment" : "Bookmark Added popover title", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lesezeichen hinzugefügt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Marcador añadido" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signet ajouté" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Segnalibro aggiunto" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bladwijzer toegevoegd" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodano zakładkę" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Marcador adicionado" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Закладка добавлена" - } - } - } - }, "Bookmark import failed:" : { "comment" : "Data import summary format of how many bookmarks (%lld) failed to import.", "localizations" : { @@ -12302,60 +12507,6 @@ } } }, - "Copy" : { - "comment" : "Command", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopieren" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copiar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copier" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copia" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopiëren" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopiuj" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copiar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Копировать" - } - } - } - }, "copy-selection" : { "comment" : "Copy selection menu item", "extractionState" : "extracted_with_value", @@ -13542,60 +13693,6 @@ } } }, - "Delete" : { - "comment" : "Command", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Löschen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Supprimer" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancella" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijderen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usuń" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Удалить" - } - } - } - }, "delete-bookmark" : { "comment" : "Delete Bookmark button", "extractionState" : "extracted_with_value", @@ -16799,60 +16896,6 @@ } } }, - "Edit…" : { - "comment" : "Command", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bearbeiten…" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar..." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifier…" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifica..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bewerken …" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edytuj..." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar…" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изменить..." - } - } - } - }, "edit.favorite" : { "comment" : "Header of the view that edits a favorite bookmark", "extractionState" : "extracted_with_value", @@ -20972,66 +21015,6 @@ } } }, - "gpc.title" : { - "comment" : "GPC settings title", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Global Privacy Control (GPC)" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Global Privacy Control (GPC)" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Control Global de Privacidad (GPC)" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Global Privacy Control (GPC)" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Global Privacy Control (GPC)" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Global Privacy Control (GPC)" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Globalna Kontrola Prywatności (GPC)" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Controlo Global de Privacidade (CGP)" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Глобальный контроль конфиденциальности (GPC)" - } - } - } - }, "Help" : { "comment" : "Main Menu Help", "localizations" : { @@ -21085,60 +21068,6 @@ } } }, - "Hide" : { - "comment" : "Main Menu > View > Home Button > None item\n Preferences > Home Button > None item", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ausblenden" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Masquer" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nascondi" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbergen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukryj" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скрыть" - } - } - } - }, "History" : { "comment" : "Main Menu ", "localizations" : { @@ -26289,60 +26218,6 @@ } } }, - "Location:" : { - "comment" : "Add Folder popover: parent folder picker title", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Emplacement :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posizione:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Locatie:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lokalizacja:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Localização:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Папка:" - } - } - } - }, "looking.for.bitwarden" : { "comment" : "Setup of the integration with Bitwarden app", "extractionState" : "extracted_with_value", @@ -31394,546 +31269,6 @@ } } }, - "newTab.setup.survey.day.0.action" : { - "comment" : "Action title of the Day 0 survey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Sign Up To Participate" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - } - } - }, - "newTab.setup.survey.day.0.summary" : { - "comment" : "Summary of the card on the new tab page that invites users to partecipate to a survey", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - } - } - }, - "newTab.setup.survey.day.0.title" : { - "comment" : "Title of the Day 0 durvey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Share Your Thoughts With Us" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - } - } - }, - "newTab.setup.survey.day.7.action" : { - "comment" : "Action title of the Day 7 durvey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teile deine Gedanken" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Share Your Thoughts" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comparte tus ideas" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partagez votre avis" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comunicaci la tua opinione" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deel je mening" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podziel się przemyśleniami" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partilha as tuas opiniões" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поделитесь своими впечатлениями" - } - } - } - }, - "newTab.setup.survey.day.7.summary" : { - "comment" : "Summary of the Day 7 durvey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nimm an unserer kurzen Umfrage teil und hilf uns, den besten Browser zu entwickeln." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Take our short survey and help us build the best browser." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Responde a nuestra breve encuesta y ayúdanos a crear el mejor navegador." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Répondez à notre courte enquête et aidez-nous à créer le meilleur navigateur." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rispondi al nostro breve sondaggio e aiutaci a creare il browser migliore." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vul onze korte enquête in en help ons de beste browser te bouwen." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weź udział w krótkiej ankiecie i pomóż nam opracować najlepszą przeglądarkę." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Responde ao nosso curto inquérito e ajuda-nos a criar o melhor navegador." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Заполните короткий опрос, чтобы помочь нам создать лучший на свете браузер." - } - } - } - }, - "newTab.setup.survey.day.7.title" : { - "comment" : "Title of the Day 7 durvey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hilf uns, uns zu verbessern" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Help Us Improve" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayúdanos a mejorar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aidez-nous à nous améliorer" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aiutaci a migliorare" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Help ons om te verbeteren" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pomóż nam we wprowadzaniu ulepszeń" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajuda-nos a melhorar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Помогите нам стать лучше" - } - } - } - }, - "newTab.setup.survey.day.14.action" : { - "comment" : "Action title of the Day 14 survey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Sign Up To Participate" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sign Up To Participate" - } - } - } - }, - "newTab.setup.survey.day.14.summary" : { - "comment" : "Summary of the card on the new tab page that invites users to partecipate to a survey", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join an interview with a member of our research team to help us build the best browser." - } - } - } - }, - "newTab.setup.survey.day.14.title" : { - "comment" : "Title of the Day 14 durvey of the Set Up section in the home page", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Share Your Thoughts With Us" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share Your Thoughts With Us" - } - } - } - }, "next" : { "comment" : "Next button", "extractionState" : "extracted_with_value", @@ -32174,126 +31509,6 @@ } } }, - "no.access.to.selected.folder" : { - "comment" : "Alert presented to user if the app doesn't have rights to access selected folder", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erlaube den Zugriff auf den Ort des Downloads." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Grant access to the location of download." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Concede acceso a la ubicación de descarga." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accordez l'accès à l'emplacement du téléchargement." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Concedi l'accesso al percorso di download." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geef toegang tot de downloadlocatie." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Przyznaj dostęp do lokalizacji pobierania." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Concede acesso à localização da transferência." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нужен доступ к папке для сохранения загрузки." - } - } - } - }, - "no.access.to.selected.folder.header" : { - "comment" : "Header of the alert dialog informing user about failed download", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo benötigt die Erlaubnis, auf den ausgewählten Ordner zuzugreifen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "DuckDuckGo needs permission to access selected folder" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo necesita permiso para acceder a la carpeta seleccionada" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo a besoin d'une autorisation pour accéder au dossier sélectionné" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo necessita dell'autorizzazione per accedere alla cartella selezionata" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo heeft toestemming nodig om toegang te krijgen tot de geselecteerde map" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo potrzebuje pozwolenia na dostęp do wybranego folderu" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "O DuckDuckGo precisa de permissão para aceder à pasta selecionada" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo запрашивает доступ к выбранной папке" - } - } - } - }, "notification.badge.cookiesmanaged" : { "comment" : "Notification that appears when browser automatically handle cookies", "extractionState" : "extracted_with_value", @@ -45212,66 +44427,6 @@ } } }, - "preferences.appearance.zoom" : { - "comment" : "Zoom settings section title", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Zoom" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoomniveau" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Масштаб" - } - } - } - }, "preferences.appearance.zoom-picker" : { "comment" : "Default page zoom picker title", "extractionState" : "extracted_with_value", @@ -46352,66 +45507,6 @@ } } }, - "preferences.privacy" : { - "comment" : "Show privacy browser preferences", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privatsphäre" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Privacy" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacidad" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Confidentialité" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prywatność" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacidade" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настройки конфиденциальности" - } - } - } - }, "preferences.privacy-protections" : { "comment" : "The section header in Preferences representing browser features related to privacy protection", "extractionState" : "extracted_with_value", @@ -46952,6 +46047,546 @@ } } }, + "prefrences.sync.bad.request.description" : { + "comment" : "Description of incorrectly formatted data error when syncing.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einige Lesezeichen sind falsch formatiert oder zu lang und wurden nicht synchronisiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Some bookmarks are formatted incorrectly or too long and were not synced." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algunos marcadores tienen un formato incorrecto o demasiado largo y no se han sincronizado." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certains signets sont mal formatés ou trop longs et n'ont pas été synchronisés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alcuni segnalibri sono formattati in modo errato o sono troppo lunghi e non sono stati sincronizzati." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sommige bladwijzers hebben de verkeerde indeling of zijn te lang en werden niet gesynchroniseerd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niektóre zakładki mają niepoprawny format lub są zbyt długie i nie zostały zsynchronizowane." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alguns marcadores estão formatados incorretamente ou são demasiado longos e não foram sincronizados." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые закладки не синхронизируются из-за неверного формата или превышения ограничений по длине." + } + } + } + }, + "prefrences.sync.bookmarks-limit-exceeded-action" : { + "comment" : "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lesezeichen verwalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Manage Bookmarks" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestionar marcadores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gérer les signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestisci segnalibri" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bladwijzers beheren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarządzaj zakładkami" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerir marcadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управление закладками" + } + } + } + }, + "prefrences.sync.bookmarks-limit-exceeded-description" : { + "comment" : "Description for sync bookmarks limits exceeded warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast die maximale Anzahl von Lesezeichen erreicht. Lösche Lesezeichen, um die Synchronisierung fortzusetzen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You've reached the maximum number of bookmarks. Please delete some to resume sync." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has alcanzado el número máximo de marcadores. Elimina algunos para reanudar la sincronización." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez atteint le nombre maximal de signets. Veuillez en supprimer quelques-uns pour reprendre la synchronisation." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hai raggiunto il numero massimo di segnalibri. Eliminane alcuni per riprendere la sincronizzazione." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt het maximumaantal bladwijzers bereikt. Verwijder er een paar om de synchronisatie te hervatten." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Osiągnięto maksymalną liczbę zakładek. Usuń część z nich, aby wznowić synchronizację." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atingiste o número máximo de marcadores. Elimina algumas para retomar a sincronização." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Достигнуто максимальное число закладок. Чтобы возобновить синхронизацию, удалите некоторые из них." + } + } + } + }, + "prefrences.sync.bookmarks.too-many-requests" : { + "comment" : "Description of too many requests error when syncing.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup ist vorübergehend nicht verfügbar." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync & Backup is temporarily unavailable." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La sincronización y la copia de seguridad no están disponibles temporalmente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup est temporairement indisponible." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync & Backup non è temporaneamente disponibile." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "'Synchronisatie en back-up' is tijdelijk niet beschikbaar." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Funkcja Sync & Backup jest tymczasowo niedostępna." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Sync & Backup está temporariamente indisponível." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Функция «Синхронизация и резервное копирование» временно недоступна." + } + } + } + }, + "prefrences.sync.credentials-limit-exceeded-action" : { + "comment" : "Button title for sync credentials limits exceeded warning to go to manage passwords", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter verwalten …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Manage passwords…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar contraseñas..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gérer les mots de passe…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestisci password…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden beheren …" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarządzaj hasłami…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerir palavras-passe…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управление паролями..." + } + } + } + }, + "prefrences.sync.credentials-limit-exceeded-description" : { + "comment" : "Description for sync credentials limits exceeded warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast die maximale Anzahl von Passwörtern erreicht. Lösche einige, um die Synchronisierung fortzusetzen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You've reached the maximum number of passwords. Please delete some to resume sync." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has alcanzado el número máximo de contraseñas. Elimina algunas para reanudar la sincronización." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez atteint le nombre maximal de mots de passe. Veuillez en supprimer quelques-uns pour reprendre la synchronisation." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hai raggiunto il numero massimo di password. Eliminane alcuni per riprendere la sincronizzazione." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt het maximumaantal wachtwoorden bereikt. Verwijder er een paar om de synchronisatie te hervatten." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Osiągnięto maksymalną liczbę haseł. Usuń część z nich, aby wznowić synchronizację." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atingiste o número máximo de palavras-passe. Elimina algumas para retomar a sincronização." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Достигнуто максимальное число паролей. Чтобы возобновить синхронизацию, удалите некоторые из них." + } + } + } + }, + "prefrences.sync.credentials.bad.request.description" : { + "comment" : "Description of incorrectly formatted data error when syncing.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einige Passwörter sind falsch formatiert oder zu lang und wurden nicht synchronisiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Some passwords are formatted incorrectly or too long and were not synced." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algunas contraseñas tienen un formato incorrecto o demasiado largo y no se han sincronizado." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certains mots de passe sont mal formatés ou trop longs et n'ont pas été synchronisés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alcune password sono formattate in modo errato o sono troppo lunghe e non sono state sincronizzate." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sommige wachtwoorden hebben de verkeerde indeling of zijn te lang en werden niet gesynchroniseerd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niektóre hasła mają niepoprawny format lub są zbyt długie i nie zostały zsynchronizowane." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algumas palavras-passe estão formatadas incorretamente ou são demasiado longas e não foram sincronizadas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Некоторые пароли не синхронизируются из-за неверного формата или превышения ограничений по длине." + } + } + } + }, + "prefrences.sync.invalid-login-description" : { + "comment" : "Description invalid credentials error when syncing.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bei der Synchronisierung ist ein Fehler aufgetreten. Versuche, die Synchronisierung auf diesem Gerät zu deaktivieren, und stelle dann die Verbindung mit einem anderen Gerät oder deinem Wiederherstellungscode wieder her." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync encountered an error. Try disabling sync on this device and then reconnect using another device or your recovery code." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se ha producido un error en la sincronización. Intenta desactivar la sincronización en este dispositivo y, a continuación, vuelve a conectarte con otro dispositivo o con tu código de recuperación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La synchronisation a rencontré une erreur. Essayez de désactiver la synchronisation sur cet appareil, puis reconnectez-vous à l’aide d’un autre appareil ou de votre code de récupération." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync ha riscontrato un errore. Prova a disattivare la sincronizzazione su questo dispositivo e riconnettiti utilizzando un altro dispositivo o il codice di ripristino." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er is een fout opgetreden bij het synchroniseren. Probeer synchronisatie op dit apparaat uit te schakelen en maak dan opnieuw verbinding met een ander apparaat of met je herstelcode." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podczas synchronizacji wystąpił błąd. Spróbuj wyłączyć synchronizację na tym urządzeniu, a następnie ponownie nawiąż połączenie za pomocą innego urządzenia lub kodu odzyskiwania." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro durante a sincronização. Experimenta desativar a sincronização neste dispositivo e, em seguida, inicia sessão novamente noutro dispositivo ou com o teu código de recuperação." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Произошла ошибка синхронизации. Попробуйте отключить синхронизацию на этом устройстве, а затем выполнить повторную привязку с помощью другого устройства или кода восстановления." + } + } + } + }, + "prefrences.sync.limit-exceeded-title" : { + "comment" : "Title for sync limits exceeded warning", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisierung angehalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sync Paused" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sincronización pausada" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisation suspendue" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sincronizzazione in pausa" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronisatie onderbroken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchronizacja wstrzymana" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sincronização em pausa" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Синхронизация на паузе" + } + } + } + }, "print.menu.item" : { "comment" : "Menu item title", "extractionState" : "extracted_with_value", @@ -47410,60 +47045,6 @@ } } }, - "Remove" : { - "comment" : "Remove bookmark button title", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Entfernen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Supprimer" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rimuovi" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijderen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usuń" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remover" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Удалить" - } - } - } - }, "remove-favorite" : { "comment" : "Remove Favorite button", "extractionState" : "extracted_with_value", @@ -49387,114 +48968,6 @@ } } }, - "Show left of the back button" : { - "comment" : "Preferences > Home Button > left position item", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Links neben der Zurück-Schaltfläche anzeigen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar a la izquierda del botón Atrás" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afficher la gauche du bouton de retour" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra a sinistra del pulsante Indietro" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Links van de 'Terug'-knop weergeven" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pokaż na lewo od przycisku Wstecz" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar à esquerda do botão de retrocesso" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывать слева от кнопки «Назад»" - } - } - } - }, - "Show Left of the Back Button" : { - "comment" : "Main Menu > View > Home Button > left position item", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Links vom Zurück-Button anzeigen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar a la izquierda del botón Atrás" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afficher la gauche du bouton Retour" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra a sinistra del pulsante Indietro" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Links van de 'Terug'-knop weergeven" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pokaż na lewo od przycisku Wstecz" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar à esquerda do botão Retroceder" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывать слева от кнопки «Назад»" - } - } - } - }, "Show Next Tab" : { "comment" : "Main Menu Window item", "localizations" : { @@ -49707,114 +49180,6 @@ } } }, - "Show right of the reload button" : { - "comment" : "Preferences > Home Button > right position item", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rechts neben der Schaltfläche „Neu laden“ anzeigen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar a la derecha del botón Volver a cargar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afficher la droite du bouton de rechargement" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra a destra del pulsante Ricarica" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rechts van de knop 'Opnieuw laden' weergeven" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pokaż na prawo od przycisku ponownego ładowania" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar à direita do botão de recarga" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывать справа от кнопки «Перезагрузить»" - } - } - } - }, - "Show Right of the Reload Button" : { - "comment" : "Main Menu > View > Home Button > right position item", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rechts vom Reload-Button anzeigen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar a la derecha del botón Volver a cargar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afficher la droite du bouton Recharger" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra a destra del pulsante Ricarica" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rechts van de knop 'Opnieuw laden' weergeven" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pokaż na prawo od przycisku ponownego ładowania" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostrar à direita do botão Recarregar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Показывать справа от кнопки «Перезагрузить»" - } - } - } - }, "Show Substitutions" : { "comment" : "Main Menu Edit-Substitutions item", "localizations" : { @@ -53497,114 +52862,6 @@ } } }, - "Try importing bookmarks manually instead." : { - "comment" : "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file.", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versuche stattdessen, Lesezeichen manuell zu importieren." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "En su lugar, intenta importar los marcadores de forma manual." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Essayez plutôt d'importer les signets manuellement." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prova a importare i segnalibri manualmente." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Probeer bladwijzers handmatig te importeren." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zamiast tego spróbuj zaimportować zakładki ręcznie." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Em vez disso, tenta importar os marcadores manualmente." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Попробуйте импортировать закладки вручную." - } - } - } - }, - "Try importing passwords manually instead." : { - "comment" : "Data import error subtitle: suggestion to import Passwords manually by selecting a CSV or HTML file.", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versuche stattdessen, Passwörter manuell zu importieren." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "En su lugar, intenta importar las contraseñas de forma manual." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Essayez plutôt d'importer les mots de passe manuellement." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prova a importare le password manualmente." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Probeer wachtwoorden handmatig te importeren." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zamiast tego spróbuj zaimportować hasła ręcznie." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Em vez disso, tenta importar as palavras-passe manualmente." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Попробуйте импортировать пароли вручную." - } - } - } - }, "uninstall" : { "comment" : "Uninstall button", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index f9dbccd5b7..29e8333811 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -153,9 +153,10 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable, CaseIt case .sync: let isSyncBookmarksPaused = UserDefaults.standard.bool(forKey: UserDefaultsWrapper.Key.syncBookmarksPaused.rawValue) let isSyncCredentialsPaused = UserDefaults.standard.bool(forKey: UserDefaultsWrapper.Key.syncCredentialsPaused.rawValue) + let isSyncPaused = UserDefaults.standard.bool(forKey: UserDefaultsWrapper.Key.syncIsPaused.rawValue) let syncService = NSApp.delegateTyped.syncService let isDataSyncingDisabled = syncService?.featureFlags.contains(.dataSyncing) == false && syncService?.authState == .active - if isSyncBookmarksPaused || isSyncCredentialsPaused || isDataSyncingDisabled { + if isSyncPaused || isSyncBookmarksPaused || isSyncCredentialsPaused || isDataSyncingDisabled { return UserText.sync + " ⚠️" } return UserText.sync diff --git a/DuckDuckGo/Preferences/Model/SyncPausedStateManaging.swift b/DuckDuckGo/Preferences/Model/SyncPausedStateManaging.swift new file mode 100644 index 0000000000..64f8100657 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/SyncPausedStateManaging.swift @@ -0,0 +1,40 @@ +// +// SyncPausedStateManaging.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 Combine + +/// The SyncPausedStateManaging protocol manages sync error states. It provides properties and methods to detect and handle changes in the synchronization status, aiding in error user notification. +protocol SyncPausedStateManaging: ObservableObject { + var isSyncPaused: Bool { get } + var isSyncBookmarksPaused: Bool { get } + var isSyncCredentialsPaused: Bool { get } + var syncPausedChangedPublisher: AnyPublisher { get } + var syncPausedMessageData: SyncPausedMessageData? { get } + var syncBookmarksPausedMessageData: SyncPausedMessageData? { get } + var syncCredentialsPausedMessageData: SyncPausedMessageData? { get } + + func syncDidTurnOff() +} + +struct SyncPausedMessageData { + let title: String + let description: String + let buttonTitle: String + let action: (() -> Void)? +} diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index 7781247f6c..f0b6a10fc5 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -39,6 +39,53 @@ extension SyncDevice { } final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { + var syncPausedTitle: String? { + return syncPausedStateManager.syncPausedMessageData?.title + } + + var syncPausedMessage: String? { + return syncPausedStateManager.syncPausedMessageData?.description + } + + var syncPausedButtonTitle: String? { + return syncPausedStateManager.syncPausedMessageData?.buttonTitle + } + + var syncPausedButtonAction: (() -> Void)? { + return syncPausedStateManager.syncPausedMessageData?.action + } + + var syncBookmarksPausedTitle: String? { + return syncPausedStateManager.syncBookmarksPausedMessageData?.title + } + + var syncBookmarksPausedMessage: String? { + return syncPausedStateManager.syncBookmarksPausedMessageData?.description + } + + var syncBookmarksPausedButtonTitle: String? { + return syncPausedStateManager.syncBookmarksPausedMessageData?.buttonTitle + } + + var syncBookmarksPausedButtonAction: (() -> Void)? { + return syncPausedStateManager.syncBookmarksPausedMessageData?.action + } + + var syncCredentialsPausedTitle: String? { + return syncPausedStateManager.syncCredentialsPausedMessageData?.title + } + + var syncCredentialsPausedMessage: String? { + return syncPausedStateManager.syncCredentialsPausedMessageData?.description + } + + var syncCredentialsPausedButtonTitle: String? { + return syncPausedStateManager.syncCredentialsPausedMessageData?.buttonTitle + } + + var syncCredentialsPausedButtonAction: (() -> Void)? { + return syncPausedStateManager.syncCredentialsPausedMessageData?.action + } struct Consts { static let syncPausedStateChanged = Notification.Name("com.duckduckgo.app.SyncPausedStateChanged") @@ -82,9 +129,9 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { } } - @Published var isSyncBookmarksPaused: Bool - - @Published var isSyncCredentialsPaused: Bool + @Published var isSyncPaused: Bool = false + @Published var isSyncBookmarksPaused: Bool = false + @Published var isSyncCredentialsPaused: Bool = false @Published var invalidBookmarksTitles: [String] = [] @Published var invalidCredentialsTitles: [String] = [] @@ -105,6 +152,8 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { @Published var isAccountRecoveryAvailable: Bool = true @Published var isAppVersionNotSupported: Bool = true + private let syncPausedStateManager: any SyncPausedStateManaging + private func updateSyncFeatureFlags(_ syncFeatureFlags: SyncFeatureFlags) { isDataSyncingAvailable = syncFeatureFlags.contains(.dataSyncing) isConnectingDevicesAvailable = syncFeatureFlags.contains(.connectFlows) @@ -123,7 +172,8 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { syncCredentialsAdapter: SyncCredentialsAdapter, appearancePreferences: AppearancePreferences = .shared, managementDialogModel: ManagementDialogModel = ManagementDialogModel(), - userAuthenticator: UserAuthenticating = DeviceAuthenticator.shared + userAuthenticator: UserAuthenticating = DeviceAuthenticator.shared, + syncPausedStateManager: any SyncPausedStateManaging ) { self.syncService = syncService self.syncBookmarksAdapter = syncBookmarksAdapter @@ -131,11 +181,10 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { self.appearancePreferences = appearancePreferences self.syncFeatureFlags = syncService.featureFlags self.userAuthenticator = userAuthenticator + self.syncPausedStateManager = syncPausedStateManager self.isFaviconsFetchingEnabled = syncBookmarksAdapter.isFaviconsFetchingEnabled self.isUnifiedFavoritesEnabled = appearancePreferences.favoritesDisplayMode.isDisplayUnified - isSyncBookmarksPaused = UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false).wrappedValue - isSyncCredentialsPaused = UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false).wrappedValue self.managementDialogModel = managementDialogModel self.managementDialogModel.delegate = self @@ -143,6 +192,13 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { updateSyncFeatureFlags(self.syncFeatureFlags) setUpObservables() setUpSyncOptionsObservables(apperancePreferences: appearancePreferences) + updateSyncPausedState() + } + + private func updateSyncPausedState() { + self.isSyncPaused = syncPausedStateManager.isSyncPaused + self.isSyncBookmarksPaused = syncPausedStateManager.isSyncBookmarksPaused + self.isSyncCredentialsPaused = syncPausedStateManager.isSyncCredentialsPaused } private func updateInvalidObjects() { @@ -197,11 +253,10 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { } .store(in: &cancellables) - NotificationCenter.default.publisher(for: Self.Consts.syncPausedStateChanged) + syncPausedStateManager.syncPausedChangedPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in - self?.isSyncBookmarksPaused = UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false).wrappedValue - self?.isSyncCredentialsPaused = UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false).wrappedValue + self?.updateSyncPausedState() } .store(in: &cancellables) @@ -238,8 +293,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { do { try await syncService.disconnect() managementDialogModel.endFlow() - UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncBookmarksPaused.rawValue) - UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncCredentialsPaused.rawValue) + syncPausedStateManager.syncDidTurnOff() } catch { managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToTurnSyncOff, description: error.localizedDescription) PixelKit.fire(DebugEvent(GeneralPixel.syncLogoutError(error: error))) @@ -390,8 +444,7 @@ extension SyncPreferences: ManagementDialogModelDelegate { do { try await syncService.deleteAccount() managementDialogModel.endFlow() - UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncBookmarksPaused.rawValue) - UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.syncCredentialsPaused.rawValue) + syncPausedStateManager.syncDidTurnOff() } catch { managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToDeleteData, description: error.localizedDescription) PixelKit.fire(DebugEvent(GeneralPixel.syncDeleteAccountError(error: error))) diff --git a/DuckDuckGo/Preferences/View/PreferencesSyncView.swift b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift index c4de382680..b78c746ab3 100644 --- a/DuckDuckGo/Preferences/View/PreferencesSyncView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift @@ -29,7 +29,8 @@ struct SyncView: View { let syncPreferences = SyncPreferences( syncService: syncService, syncBookmarksAdapter: syncDataProviders.bookmarksAdapter, - syncCredentialsAdapter: syncDataProviders.credentialsAdapter + syncCredentialsAdapter: syncDataProviders.credentialsAdapter, + syncPausedStateManager: syncDataProviders.syncErrorHandler ) SyncUI.ManagementView(model: syncPreferences) .onAppear { diff --git a/DuckDuckGo/Sync/SyncBookmarksAdapter.swift b/DuckDuckGo/Sync/SyncBookmarksAdapter.swift index 54dae6e41d..53fc8a10fb 100644 --- a/DuckDuckGo/Sync/SyncBookmarksAdapter.swift +++ b/DuckDuckGo/Sync/SyncBookmarksAdapter.swift @@ -41,6 +41,7 @@ final class SyncBookmarksAdapter { private(set) var provider: BookmarksProvider? let databaseCleaner: BookmarkDatabaseCleaner + let syncErrorHandler: SyncErrorHandling @Published var isFaviconsFetchingEnabled: Bool = UserDefaultsWrapper(key: .syncIsFaviconsFetcherEnabled, defaultValue: false).wrappedValue { @@ -61,24 +62,16 @@ final class SyncBookmarksAdapter { @UserDefaultsWrapper(key: .syncDidMigrateToImprovedListsHandling, defaultValue: false) private var didMigrateToImprovedListsHandling: Bool - @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) - private var isSyncBookmarksPaused: Bool { - didSet { - NotificationCenter.default.post(name: SyncPreferences.Consts.syncPausedStateChanged, object: nil) - } - } - - @UserDefaultsWrapper(key: .syncBookmarksPausedErrorDisplayed, defaultValue: false) - private var didShowBookmarksSyncPausedError: Bool - init( database: CoreDataDatabase, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - appearancePreferences: AppearancePreferences = .shared + appearancePreferences: AppearancePreferences = .shared, + syncErrorHandler: SyncErrorHandling ) { self.database = database self.bookmarkManager = bookmarkManager self.appearancePreferences = appearancePreferences + self.syncErrorHandler = syncErrorHandler databaseCleaner = BookmarkDatabaseCleaner( bookmarkDatabase: database, errorEvents: BookmarksCleanupErrorHandling(), @@ -115,9 +108,9 @@ final class SyncBookmarksAdapter { metricsEvents: metricsEventsHandler, log: OSLog.sync, syncDidUpdateData: { [weak self] in - LocalBookmarkManager.shared.loadBookmarks() - self?.isSyncBookmarksPaused = false - self?.didShowBookmarksSyncPausedError = false + self?.syncErrorHandler.syncBookmarksSucceded() + guard let manager = self?.bookmarkManager as? LocalBookmarkManager else { return } + manager.loadBookmarks() }, syncDidFinish: { [weak self] faviconsFetcherInput in if let faviconsFetcher, self?.isFaviconsFetchingEnabled == true { @@ -166,32 +159,7 @@ final class SyncBookmarksAdapter { private func bindSyncErrorPublisher(_ provider: BookmarksProvider) { syncErrorCancellable = provider.syncErrorPublisher .sink { [weak self] error in - switch error { - case let syncError as SyncError: - PixelKit.fire(DebugEvent(GeneralPixel.syncBookmarksFailed, error: syncError)) - switch syncError { - case .unexpectedStatusCode(409): - // If bookmarks count limit has been exceeded - self?.isSyncBookmarksPaused = true - PixelKit.fire(GeneralPixel.syncBookmarksCountLimitExceededDaily, frequency: .daily) - self?.showSyncPausedAlert() - case .unexpectedStatusCode(413): - // If bookmarks request size limit has been exceeded - self?.isSyncBookmarksPaused = true - PixelKit.fire(GeneralPixel.syncBookmarksRequestSizeLimitExceededDaily, frequency: .daily) - self?.showSyncPausedAlert() - default: - break - } - default: - let nsError = error as NSError - if nsError.domain != NSURLErrorDomain { - let processedErrors = CoreDataErrorsParser.parse(error: error as NSError) - let params = processedErrors.errorPixelParameters - PixelKit.fire(DebugEvent(GeneralPixel.syncBookmarksFailed, error: error), withAdditionalParameters: params) - } - } - os_log(.error, log: OSLog.sync, "Bookmarks Sync error: %{public}s", String(reflecting: error)) + self?.syncErrorHandler.handleBookmarkError(error) } } @@ -202,25 +170,6 @@ final class SyncBookmarksAdapter { } } - private func showSyncPausedAlert() { - guard !didShowBookmarksSyncPausedError else { return } - Task { - await MainActor.run { - let alert = NSAlert.syncBookmarksPaused() - let response = alert.runModal() - didShowBookmarksSyncPausedError = true - - switch response { - case .alertSecondButtonReturn: - alert.window.sheetParent?.endSheet(alert.window) - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) - default: - break - } - } - } - } - private var syncErrorCancellable: AnyCancellable? private let bookmarkManager: BookmarkManager private let database: CoreDataDatabase diff --git a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift index c9f495901e..4ffaa44081 100644 --- a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift +++ b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift @@ -28,20 +28,13 @@ final class SyncCredentialsAdapter { private(set) var provider: CredentialsProvider? let databaseCleaner: CredentialsDatabaseCleaner + let syncErrorHandler: SyncErrorHandling let syncDidCompletePublisher: AnyPublisher - @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) - private var isSyncCredentialsPaused: Bool { - didSet { - NotificationCenter.default.post(name: SyncPreferences.Consts.syncPausedStateChanged, object: nil) - } - } - - @UserDefaultsWrapper(key: .syncCredentialsPausedErrorDisplayed, defaultValue: false) - private var didShowCredentialsSyncPausedError: Bool - - init(secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory) { + init(secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, + syncErrorHandler: SyncErrorHandling) { syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() + self.syncErrorHandler = syncErrorHandler databaseCleaner = CredentialsDatabaseCleaner( secureVaultFactory: secureVaultFactory, secureVaultErrorReporter: SecureVaultReporter.shared, @@ -77,39 +70,13 @@ final class SyncCredentialsAdapter { log: OSLog.sync, syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() - self?.isSyncCredentialsPaused = false - self?.didShowCredentialsSyncPausedError = false + self?.syncErrorHandler.syncCredentialsSucceded() } ) syncErrorCancellable = provider.syncErrorPublisher .sink { [weak self] error in - switch error { - case let syncError as SyncError: - PixelKit.fire(DebugEvent(GeneralPixel.syncCredentialsFailed, error: syncError)) - switch syncError { - case .unexpectedStatusCode(409): - // If credentials count limit has been exceeded - self?.isSyncCredentialsPaused = true - PixelKit.fire(GeneralPixel.syncCredentialsCountLimitExceededDaily, frequency: .daily) - self?.showSyncPausedAlert() - case .unexpectedStatusCode(413): - // If credentials request size limit has been exceeded - self?.isSyncCredentialsPaused = true - PixelKit.fire(GeneralPixel.syncCredentialsRequestSizeLimitExceededDaily, frequency: .daily) - self?.showSyncPausedAlert() - default: - break - } - default: - let nsError = error as NSError - if nsError.domain != NSURLErrorDomain { - let processedErrors = CoreDataErrorsParser.parse(error: error as NSError) - let params = processedErrors.errorPixelParameters - PixelKit.fire(DebugEvent(GeneralPixel.syncCredentialsFailed, error: error), withAdditionalParameters: params) - } - } - os_log(.error, log: OSLog.sync, "Credentials Sync error: %{public}s", String(reflecting: error)) + self?.syncErrorHandler.handleCredentialError(error) } self.provider = provider @@ -121,25 +88,6 @@ final class SyncCredentialsAdapter { } } - private func showSyncPausedAlert() { - guard !didShowCredentialsSyncPausedError else { return } - Task { - await MainActor.run { - let alert = NSAlert.syncCredentialsPaused() - let response = alert.runModal() - didShowCredentialsSyncPausedError = true - - switch response { - case .alertSecondButtonReturn: - alert.window.sheetParent?.endSheet(alert.window) - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) - default: - break - } - } - } - } - private var syncDidCompleteSubject = PassthroughSubject() private var syncErrorCancellable: AnyCancellable? } diff --git a/DuckDuckGo/Sync/SyncDataProviders.swift b/DuckDuckGo/Sync/SyncDataProviders.swift index 4da4a2d5c5..f3a7054f7e 100644 --- a/DuckDuckGo/Sync/SyncDataProviders.swift +++ b/DuckDuckGo/Sync/SyncDataProviders.swift @@ -28,6 +28,7 @@ final class SyncDataProviders: DataProvidersSource { public let bookmarksAdapter: SyncBookmarksAdapter public let credentialsAdapter: SyncCredentialsAdapter public let settingsAdapter: SyncSettingsAdapter + public let syncErrorHandler: SyncErrorHandler @MainActor func makeDataProviders() -> [DataProviding] { @@ -94,11 +95,12 @@ final class SyncDataProviders: DataProvidersSource { } } - init(bookmarksDatabase: CoreDataDatabase, secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory) { + init(bookmarksDatabase: CoreDataDatabase, secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, syncErrorHandler: SyncErrorHandler) { self.bookmarksDatabase = bookmarksDatabase self.secureVaultFactory = secureVaultFactory - bookmarksAdapter = SyncBookmarksAdapter(database: bookmarksDatabase) - credentialsAdapter = SyncCredentialsAdapter(secureVaultFactory: secureVaultFactory) + self.syncErrorHandler = syncErrorHandler + bookmarksAdapter = SyncBookmarksAdapter(database: bookmarksDatabase, syncErrorHandler: syncErrorHandler) + credentialsAdapter = SyncCredentialsAdapter(secureVaultFactory: secureVaultFactory, syncErrorHandler: syncErrorHandler) settingsAdapter = SyncSettingsAdapter() } diff --git a/DuckDuckGo/Sync/SyncErrorHandler.swift b/DuckDuckGo/Sync/SyncErrorHandler.swift index 036c041fb7..5d26731e23 100644 --- a/DuckDuckGo/Sync/SyncErrorHandler.swift +++ b/DuckDuckGo/Sync/SyncErrorHandler.swift @@ -20,10 +20,73 @@ import Common import DDGSync import Foundation import PixelKit +import Persistence +import Combine -public class SyncErrorHandler: EventMapping { +/// The SyncErrorHandling protocol defines methods for handling sync errors related to specific data types such as bookmarks and credentials. +protocol SyncErrorHandling { + func handleBookmarkError(_ error: Error) + func handleCredentialError(_ error: Error) + func syncBookmarksSucceded() + func syncCredentialsSucceded() +} + +public class SyncErrorHandler: EventMapping, ObservableObject { + + @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) + private (set) var isSyncBookmarksPaused: Bool { + didSet { + isSyncPausedChangedPublisher.send() + } + } + + @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) + private (set) var isSyncCredentialsPaused: Bool { + didSet { + isSyncPausedChangedPublisher.send() + } + } + + @UserDefaultsWrapper(key: .syncIsPaused, defaultValue: false) + private (set) var isSyncPaused: Bool { + didSet { + isSyncPausedChangedPublisher.send() + } + } + + @UserDefaultsWrapper(key: .syncBookmarksPausedErrorDisplayed, defaultValue: false) + private var didShowBookmarksSyncPausedError: Bool - public init() { + @UserDefaultsWrapper(key: .syncCredentialsPausedErrorDisplayed, defaultValue: false) + private var didShowCredentialsSyncPausedError: Bool + + @UserDefaultsWrapper(key: .syncInvalidLoginPausedErrorDisplayed, defaultValue: false) + private var didShowInvalidLoginSyncPausedError: Bool + + @UserDefaultsWrapper(key: .syncLastErrorNotificationTime, defaultValue: nil) + private var lastErrorNotificationTime: Date? + + @UserDefaultsWrapper(key: .syncLastSuccesfullTime, defaultValue: nil) + private var lastSyncSuccessTime: Date? + + @UserDefaultsWrapper(key: .syncLastNonActionableErrorCount, defaultValue: 0) + private var nonActionableErrorCount: Int + + @UserDefaultsWrapper(key: .syncCurrentAllPausedError, defaultValue: nil) + private var currentSyncAllPausedError: String? + + @UserDefaultsWrapper(key: .syncCurrentBookmarksPausedError, defaultValue: nil) + private var currentSyncBookmarksPausedError: String? + + @UserDefaultsWrapper(key: .syncCurrentCredentialsPausedError, defaultValue: nil) + private var currentSyncCredentialsPausedError: String? + + var isSyncPausedChangedPublisher = PassthroughSubject() + + let alertPresenter: SyncAlertsPresenting + + public init(alertPresenter: SyncAlertsPresenting = SyncAlertsPresenter()) { + self.alertPresenter = alertPresenter super.init { event, _, _, _ in PixelKit.fire(DebugEvent(GeneralPixel.syncSentUnauthenticatedRequest, error: event)) } @@ -32,4 +95,318 @@ public class SyncErrorHandler: EventMapping { override init(mapping: @escaping EventMapping.Mapping) { fatalError("Use init()") } + + var addErrorPublisher: AnyPublisher { + addErrorSubject.eraseToAnyPublisher() + } + + private let addErrorSubject: PassthroughSubject = .init() + public let objectWillChange = ObservableObjectPublisher() + + private func resetBookmarksErrors() { + isSyncBookmarksPaused = false + didShowBookmarksSyncPausedError = false + currentSyncAllPausedError = nil + resetGeneralErrors() + } + + private func resetCredentialsErrors() { + isSyncCredentialsPaused = false + didShowCredentialsSyncPausedError = false + currentSyncCredentialsPausedError = nil + resetGeneralErrors() + } + + private func resetGeneralErrors() { + isSyncPaused = false + didShowInvalidLoginSyncPausedError = false + lastErrorNotificationTime = nil + currentSyncAllPausedError = nil + nonActionableErrorCount = 0 + } + + private func shouldShowAlertForNonActionableError() -> Bool { + let timeStamp = Date() + nonActionableErrorCount += 1 + let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: timeStamp)! + var lastErrorNotificationWasMoreThan24hAgo: Bool + if let lastErrorNotificationTime { + lastErrorNotificationWasMoreThan24hAgo = lastErrorNotificationTime < oneDayAgo + } else { + lastErrorNotificationWasMoreThan24hAgo = true + } + let areThere10ConsecutiveError = nonActionableErrorCount >= 10 + if nonActionableErrorCount >= 10 { + nonActionableErrorCount = 0 + } + let twelveHoursAgo = Calendar.current.date(byAdding: .hour, value: -12, to: timeStamp)! + let noSuccessfulSyncInLast12h = nonActionableErrorCount > 1 && lastSyncSuccessTime ?? Date() <= twelveHoursAgo + + return lastErrorNotificationWasMoreThan24hAgo && + (areThere10ConsecutiveError || noSuccessfulSyncInLast12h) + } + + private func getErrorType(from errorString: String?) -> AsyncErrorType? { + guard let errorString = errorString else { + return nil + } + return AsyncErrorType(rawValue: errorString) + } + + private var syncPausedTitle: String? { + guard let error = getErrorType(from: currentSyncAllPausedError) else { return nil } + switch error { + case .invalidLoginCredentials: + return UserText.syncPausedTitle + case .tooManyRequests: + return UserText.syncErrorTitle + default: + assertionFailure("Sync Paused error should be one of those listes") + return nil + } + } + + private var syncPausedMessage: String? { + guard let error = getErrorType(from: currentSyncAllPausedError) else { return nil } + switch error { + case .invalidLoginCredentials: + return UserText.invalidLoginCredentialErrorDescription + case .tooManyRequests: + return UserText.tooManyRequestsErrorDescription + default: + assertionFailure("Sync Paused error should be one of those listes") + return nil + } + } + + private var syncBookmarksPausedMessage: String? { + guard let error = getErrorType(from: currentSyncBookmarksPausedError) else { return nil } + switch error { + case .bookmarksCountLimitExceeded, .bookmarksRequestSizeLimitExceeded: + return UserText.bookmarksLimitExceededDescription + case .badRequestBookmarks: + return UserText.syncBookmarksBadRequestErrorDescription + default: + assertionFailure("Sync Bookmarks Paused error should be one of those listes") + return nil + } + } + + private var syncCredentialsPausedMessage: String? { + guard let error = getErrorType(from: currentSyncBookmarksPausedError) else { return nil } + switch error { + case .credentialsCountLimitExceeded, .credentialsRequestSizeLimitExceeded: + return UserText.bookmarksLimitExceededDescription + case .badRequestBookmarks: + return UserText.syncCredentialsBadRequestErrorDescription + default: + assertionFailure("Sync Bookmarks Paused error should be one of those listes") + return nil + } + } +} + +extension SyncErrorHandler: SyncErrorHandling { + func syncCredentialsSucceded() { + lastSyncSuccessTime = Date() + resetCredentialsErrors() + } + + func syncBookmarksSucceded() { + lastSyncSuccessTime = Date() + resetBookmarksErrors() + } + + func handleBookmarkError(_ error: Error) { + handleError(error, modelType: .bookmarks) + } + + func handleCredentialError(_ error: Error) { + handleError(error, modelType: .credentials) + } + + private func handleError(_ error: Error, modelType: ModelType) { + switch error { + case let syncError as SyncError: + switch modelType { + case .bookmarks: + PixelKit.fire(DebugEvent(GeneralPixel.syncBookmarksFailed, error: syncError)) + case .credentials: + PixelKit.fire(DebugEvent(GeneralPixel.syncCredentialsFailed, error: syncError)) + } + handleSyncError(syncError, modelType: modelType) + default: + let nsError = error as NSError + if nsError.domain != NSURLErrorDomain { + let processedErrors = CoreDataErrorsParser.parse(error: error as NSError) + let params = processedErrors.errorPixelParameters + PixelKit.fire(DebugEvent(GeneralPixel.syncBookmarksFailed, error: error), withAdditionalParameters: params) + } + } + } + + // swiftlint:disable:next cyclomatic_complexity + private func handleSyncError(_ syncError: SyncError, modelType: ModelType) { + switch syncError { + case .unexpectedStatusCode(409): + switch modelType { + case .bookmarks: + syncIsPaused(errorType: .bookmarksCountLimitExceeded) + case .credentials: + syncIsPaused(errorType: .credentialsCountLimitExceeded) + } + case .unexpectedStatusCode(413): + switch modelType { + case .bookmarks: + syncIsPaused(errorType: .bookmarksRequestSizeLimitExceeded) + case .credentials: + syncIsPaused(errorType: .credentialsRequestSizeLimitExceeded) + } + case .unexpectedStatusCode(400): + switch modelType { + case .bookmarks: + syncIsPaused(errorType: .badRequestBookmarks) + case .credentials: + syncIsPaused(errorType: .badRequestCredentials) + } + case .unexpectedStatusCode(401): + syncIsPaused(errorType: .invalidLoginCredentials) + case .unexpectedStatusCode(418), .unexpectedStatusCode(429): + syncIsPaused(errorType: .tooManyRequests) + default: + break + } + } + + private func syncIsPaused(errorType: AsyncErrorType) { + showSyncPausedAlertIfNeeded(for: errorType) + switch errorType { + case .bookmarksCountLimitExceeded: + currentSyncBookmarksPausedError = errorType.rawValue + self.isSyncBookmarksPaused = true + PixelKit.fire(GeneralPixel.syncBookmarksCountLimitExceededDaily, frequency: .daily) + case .credentialsCountLimitExceeded: + currentSyncCredentialsPausedError = errorType.rawValue + self.isSyncCredentialsPaused = true + PixelKit.fire(GeneralPixel.syncCredentialsCountLimitExceededDaily, frequency: .daily) + case .bookmarksRequestSizeLimitExceeded: + currentSyncBookmarksPausedError = errorType.rawValue + self.isSyncBookmarksPaused = true + PixelKit.fire(GeneralPixel.syncBookmarksRequestSizeLimitExceededDaily, frequency: .daily) + case .credentialsRequestSizeLimitExceeded: + currentSyncCredentialsPausedError = errorType.rawValue + self.isSyncCredentialsPaused = true + PixelKit.fire(GeneralPixel.syncCredentialsRequestSizeLimitExceededDaily, frequency: .daily) + case .badRequestBookmarks: + currentSyncBookmarksPausedError = errorType.rawValue + self.isSyncBookmarksPaused = true + case .badRequestCredentials: + currentSyncCredentialsPausedError = errorType.rawValue + self.isSyncCredentialsPaused = true + case .invalidLoginCredentials: + currentSyncAllPausedError = errorType.rawValue + self.isSyncPaused = true + case .tooManyRequests: + currentSyncAllPausedError = errorType.rawValue + self.isSyncPaused = true + } + } + + // swiftlint:disable:next cyclomatic_complexity + private func showSyncPausedAlertIfNeeded(for errorType: AsyncErrorType) { + switch errorType { + case .bookmarksCountLimitExceeded, .bookmarksRequestSizeLimitExceeded: + guard !didShowBookmarksSyncPausedError else { return } + alertPresenter.showSyncPausedAlert(title: UserText.syncBookmarkPausedAlertTitle, informative: UserText.syncBookmarkPausedAlertDescription) + didShowBookmarksSyncPausedError = true + case .credentialsCountLimitExceeded, .credentialsRequestSizeLimitExceeded: + guard !didShowCredentialsSyncPausedError else { return } + alertPresenter.showSyncPausedAlert(title: UserText.syncCredentialsPausedAlertTitle, informative: UserText.syncCredentialsPausedAlertDescription) + didShowCredentialsSyncPausedError = true + case .badRequestBookmarks: + guard !didShowBookmarksSyncPausedError else { return } + alertPresenter.showSyncPausedAlert(title: UserText.syncBookmarkPausedAlertTitle, informative: UserText.syncBookmarksBadRequestAlertDescription) + didShowBookmarksSyncPausedError = true + case .badRequestCredentials: + guard !didShowCredentialsSyncPausedError else { return } + alertPresenter.showSyncPausedAlert(title: UserText.syncBookmarkPausedAlertTitle, informative: UserText.syncCredentialsBadRequestAlertDescription) + didShowCredentialsSyncPausedError = true + case .invalidLoginCredentials: + guard !didShowInvalidLoginSyncPausedError else { return } + alertPresenter.showSyncPausedAlert(title: UserText.syncPausedAlertTitle, informative: UserText.syncInvalidLoginAlertDescription) + didShowInvalidLoginSyncPausedError = true + case .tooManyRequests: + guard shouldShowAlertForNonActionableError() == true else { return } + alertPresenter.showSyncPausedAlert(title: UserText.syncErrorAlertTitle, informative: UserText.syncTooManyRequestsAlertDescription) + lastErrorNotificationTime = Date() + } + } + + private enum AsyncErrorType: String { + case bookmarksCountLimitExceeded + case credentialsCountLimitExceeded + case bookmarksRequestSizeLimitExceeded + case credentialsRequestSizeLimitExceeded + case invalidLoginCredentials + case tooManyRequests + case badRequestBookmarks + case badRequestCredentials + } + + private enum ModelType { + case bookmarks + case credentials + } + + @MainActor + private func manageBookmarks() { + guard let mainVC = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController else { return } + mainVC.showManageBookmarks(self) + } + + @MainActor + private func manageLogins() { + guard let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController else { return } + let navigationViewController = parentWindowController.mainViewController.navigationBarViewController + navigationViewController.showPasswordManagerPopover(selectedCategory: .allItems) + } + +} + +extension SyncErrorHandler: SyncPausedStateManaging { + var syncPausedMessageData: SyncPausedMessageData? { + guard let syncPausedMessage else { return nil } + guard let syncPausedTitle else { return nil } + return SyncPausedMessageData(title: syncPausedTitle, + description: syncPausedMessage, + buttonTitle: "", + action: nil) + } + + @MainActor + var syncBookmarksPausedMessageData: SyncPausedMessageData? { + guard let syncBookmarksPausedMessage else { return nil } + return SyncPausedMessageData(title: UserText.syncLimitExceededTitle, + description: syncBookmarksPausedMessage, + buttonTitle: "", + action: manageBookmarks) + } + + @MainActor + var syncCredentialsPausedMessageData: SyncPausedMessageData? { + guard let syncCredentialsPausedMessage else { return nil } + return SyncPausedMessageData(title: UserText.syncLimitExceededTitle, + description: syncCredentialsPausedMessage, + buttonTitle: "", + action: manageLogins) + } + + var syncPausedChangedPublisher: AnyPublisher { + isSyncPausedChangedPublisher.eraseToAnyPublisher() + } + + func syncDidTurnOff() { + resetBookmarksErrors() + resetCredentialsErrors() + } } diff --git a/DuckDuckGo/Sync/Utilities/SyncAlertsPresenter.swift b/DuckDuckGo/Sync/Utilities/SyncAlertsPresenter.swift new file mode 100644 index 0000000000..08d2cb0d25 --- /dev/null +++ b/DuckDuckGo/Sync/Utilities/SyncAlertsPresenter.swift @@ -0,0 +1,42 @@ +// +// SyncAlertsPresenter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public protocol SyncAlertsPresenting { + func showSyncPausedAlert(title: String, informative: String) +} + +public struct SyncAlertsPresenter: SyncAlertsPresenting { + public init () {} + public func showSyncPausedAlert(title: String, informative: String) { + Task { @MainActor in + let alert = NSAlert.syncPaused(title: title, informative: informative) + let response = alert.runModal() + + switch response { + case .alertSecondButtonReturn: + alert.window.sheetParent?.endSheet(alert.window) + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) + default: + break + } + } + + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift index 9c17db0f43..52c8b61dff 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/ManagementViewModel.swift @@ -30,8 +30,21 @@ public protocol ManagementViewModel: ObservableObject { var isCreatingAccount: Bool { get } var shouldShowErrorMessage: Bool { get set } var syncErrorMessage: SyncErrorMessage? { get } + var isSyncPaused: Bool { get } var isSyncBookmarksPaused: Bool { get } var isSyncCredentialsPaused: Bool { get } + var syncPausedTitle: String? { get } + var syncPausedMessage: String? { get } + var syncPausedButtonTitle: String? { get } + var syncPausedButtonAction: (() -> Void)? { get } + var syncBookmarksPausedTitle: String? { get } + var syncBookmarksPausedMessage: String? { get } + var syncBookmarksPausedButtonTitle: String? { get } + var syncBookmarksPausedButtonAction: (() -> Void)? { get } + var syncCredentialsPausedTitle: String? { get } + var syncCredentialsPausedMessage: String? { get } + var syncCredentialsPausedButtonTitle: String? { get } + var syncCredentialsPausedButtonAction: (() -> Void)? { get } var invalidBookmarksTitles: [String] { get } var invalidCredentialsTitles: [String] { get } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift index 4affcab8b7..d90040998d 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncEnabledView.swift @@ -26,6 +26,9 @@ struct SyncEnabledView: View where ViewModel: ManagementViewModel { // Errors VStack(alignment: .leading, spacing: 16) { syncUnavailableView() + if model.isSyncPaused { + syncPaused() + } if model.isSyncBookmarksPaused { syncPaused(for: .bookmarks) } @@ -85,30 +88,58 @@ struct SyncEnabledView: View where ViewModel: ManagementViewModel { } } + @ViewBuilder + func syncPaused() -> some View { + if let title = model.syncPausedTitle, + let message = model.syncPausedMessage, + let buttonTitle = model.syncPausedButtonTitle { + if let action = model.syncPausedButtonAction { + SyncWarningMessage(title: title, message: message, buttonTitle: buttonTitle) { + action() + } + } else { + SyncWarningMessage(title: title, message: message, buttonTitle: buttonTitle) + } + } + + } + @ViewBuilder func syncPaused(for itemType: LimitedItemType) -> some View { - var description: String { + var title: String? { switch itemType { case .bookmarks: - return UserText.bookmarksLimitExceededDescription + return model.syncBookmarksPausedTitle case .credentials: - return UserText.credentialsLimitExceededDescription + return model.syncCredentialsPausedTitle } } - var actionTitle: String { + var message: String? { switch itemType { case .bookmarks: - return UserText.bookmarksLimitExceededAction + return model.syncBookmarksPausedMessage case .credentials: - return UserText.credentialsLimitExceededAction + return model.syncCredentialsPausedMessage } } - SyncWarningMessage(title: UserText.syncLimitExceededTitle, message: description, buttonTitle: actionTitle) { + var buttonTitle: String? { switch itemType { case .bookmarks: - model.manageBookmarks() + return model.syncBookmarksPausedButtonTitle case .credentials: - model.manageLogins() + return model.syncCredentialsPausedButtonTitle + } + } + + if let title, + let message, + let buttonTitle { + if let action = model.syncPausedButtonAction { + SyncWarningMessage(title: title, message: message, buttonTitle: buttonTitle) { + action() + } + } else { + SyncWarningMessage(title: title, message: message, buttonTitle: buttonTitle) } } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncWarningMessage.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncWarningMessage.swift index b1247ca72a..42dbdbc6f6 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncWarningMessage.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/ManagementView/SyncWarningMessage.swift @@ -44,8 +44,8 @@ struct SyncWarningMessage: View { .padding(.top, 8) } } + .frame(maxWidth: .infinity, alignment: .leading) } - .padding(.leading, -12) .frame(maxWidth: .infinity) .padding() .background(RoundedRectangle(cornerRadius: 8).foregroundColor(Color(.alertBubbleBackground))) diff --git a/UnitTests/Sync/Mocks/MockAppearancePreferencesPersistor.swift b/UnitTests/Sync/Mocks/MockAppearancePreferencesPersistor.swift new file mode 100644 index 0000000000..f3d1de7fef --- /dev/null +++ b/UnitTests/Sync/Mocks/MockAppearancePreferencesPersistor.swift @@ -0,0 +1,46 @@ +// +// MockAppearancePreferencesPersistor.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 +@testable import DuckDuckGo_Privacy_Browser + +struct MockAppearancePreferencesPersistor: AppearancePreferencesPersistor { + + var homeButtonPosition: HomeButtonPosition = .hidden + + var showFullURL: Bool = false + + var showAutocompleteSuggestions: Bool = false + + var currentThemeName: String = "" + + var defaultPageZoom: CGFloat = 1.0 + + var favoritesDisplayMode: String? + + var isFavoriteVisible: Bool = true + + var isContinueSetUpVisible: Bool = true + + var isRecentActivityVisible: Bool = true + + var showBookmarksBar: Bool = false + + var bookmarksBarAppearance: BookmarksBarAppearance = .alwaysOn + +} diff --git a/UnitTests/Sync/Mocks/MockDDGSyncing.swift b/UnitTests/Sync/Mocks/MockDDGSyncing.swift new file mode 100644 index 0000000000..e400ec9a8a --- /dev/null +++ b/UnitTests/Sync/Mocks/MockDDGSyncing.swift @@ -0,0 +1,103 @@ +// +// MockDDGSyncing.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 Combine +import TestUtils +@testable import DuckDuckGo_Privacy_Browser +@testable import DDGSync + +class MockDDGSyncing: DDGSyncing { + + let registeredDevices = [RegisteredDevice(id: "1", name: "Device 1", type: "desktop"), RegisteredDevice(id: "2", name: "Device 2", type: "mobile"), RegisteredDevice(id: "3", name: "Device 1", type: "desktop")] + var disconnectCalled = false + + var dataProvidersSource: DataProvidersSource? + + @Published var featureFlags: SyncFeatureFlags = .all + + var featureFlagsPublisher: AnyPublisher { + $featureFlags.eraseToAnyPublisher() + } + + @Published var authState: SyncAuthState = .inactive + + var authStatePublisher: AnyPublisher { + $authState.eraseToAnyPublisher() + } + + var account: SyncAccount? + + var scheduler: Scheduling + + var syncDailyStats = SyncDailyStats(store: MockKeyValueStore()) + + @Published var isSyncInProgress: Bool + + var isSyncInProgressPublisher: AnyPublisher { + $isSyncInProgress.eraseToAnyPublisher() + } + + init(dataProvidersSource: DataProvidersSource? = nil, authState: SyncAuthState, account: SyncAccount? = nil, scheduler: Scheduling = CapturingScheduler(), isSyncInProgress: Bool) { + self.dataProvidersSource = dataProvidersSource + self.authState = authState + self.account = account + self.scheduler = scheduler + self.isSyncInProgress = isSyncInProgress + } + + func initializeIfNeeded() { + } + + func createAccount(deviceName: String, deviceType: String) async throws { + } + + func login(_ recoveryKey: SyncCode.RecoveryKey, deviceName: String, deviceType: String) async throws -> [RegisteredDevice] { + return [] + } + + func remoteConnect() throws -> RemoteConnecting { + return MockRemoteConnecting() + } + + func transmitRecoveryKey(_ connectCode: SyncCode.ConnectCode) async throws { + } + + func disconnect() async throws { + disconnectCalled = true + } + + func disconnect(deviceId: String) async throws { + } + + func fetchDevices() async throws -> [RegisteredDevice] { + return registeredDevices + } + + func updateDeviceName(_ name: String) async throws -> [RegisteredDevice] { + return [] + } + + func deleteAccount() async throws { + } + + var serverEnvironment: ServerEnvironment = .production + + func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) { + } +} diff --git a/UnitTests/Sync/Mocks/MockSyncPausedStateManaging.swift b/UnitTests/Sync/Mocks/MockSyncPausedStateManaging.swift new file mode 100644 index 0000000000..3d5007d6ed --- /dev/null +++ b/UnitTests/Sync/Mocks/MockSyncPausedStateManaging.swift @@ -0,0 +1,57 @@ +// +// MockSyncPausedStateManaging.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 Combine +@testable import DuckDuckGo_Privacy_Browser + +class MockSyncPausedStateManaging: SyncPausedStateManaging { + static var syncBookmarksPausedData = SyncPausedMessageData(title: "Bookmarks Paused", description: "Something with bookmark is wrong", buttonTitle: "Manage Bookmarks", action: {print("something bookmarks")}) + static var syncCredentialsPausedData = SyncPausedMessageData(title: "Credentials Paused", description: "Something with Credentials is wrong", buttonTitle: "Manage Credentials", action: {print("something Credentials")}) + static var syncIsPausedData = SyncPausedMessageData(title: "Paused", description: "Something is wrong", buttonTitle: "", action: nil) + + var isSyncPausedChangedPublisher = PassthroughSubject() + + var syncDidTurnOffCalled = false + + var isSyncPaused: Bool = false + + var isSyncBookmarksPaused: Bool = false + + var isSyncCredentialsPaused: Bool = false + + var syncPausedChangedPublisher: AnyPublisher { + isSyncPausedChangedPublisher.eraseToAnyPublisher() + } + + var syncPausedMessageData: SyncPausedMessageData? { + return Self.syncIsPausedData + } + + var syncBookmarksPausedMessageData: SyncPausedMessageData? { + return Self.syncBookmarksPausedData + } + + var syncCredentialsPausedMessageData: SyncPausedMessageData? { + return Self.syncCredentialsPausedData + } + + func syncDidTurnOff() { + syncDidTurnOffCalled = true + } +} diff --git a/UnitTests/Sync/SyncBookmarksAdapterTests.swift b/UnitTests/Sync/SyncBookmarksAdapterTests.swift new file mode 100644 index 0000000000..52672c6137 --- /dev/null +++ b/UnitTests/Sync/SyncBookmarksAdapterTests.swift @@ -0,0 +1,82 @@ +// +// SyncBookmarksAdapterTests.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 BrowserServicesKit +import Combine +import DDGSync +import Persistence +@testable import DuckDuckGo_Privacy_Browser + +final class SyncBookmarksAdapterTests: XCTestCase { + + var errorHandler: CapturingAdapterErrorHandler! + var adapter: SyncBookmarksAdapter! + let metadataStore = MockMetadataStore() + var cancellables: Set! + var database: CoreDataDatabase! + + override func setUpWithError() throws { + errorHandler = CapturingAdapterErrorHandler() + let bundle = DDGSync.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "SyncMetadata") else { + XCTFail("Failed to load model") + return + } + database = CoreDataDatabase(name: "", containerLocation: FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString), model: model, readOnly: true, options: [:]) + adapter = SyncBookmarksAdapter(database: database, bookmarkManager: MockBookmarkManager(), syncErrorHandler: errorHandler) + cancellables = [] + } + + override func tearDownWithError() throws { + errorHandler = nil + adapter = nil + cancellables = nil + } + + func testWhenSyncErrorPublished_ThenErrorHandlerHandleCredentialErrorCalled() async { + let expectation = XCTestExpectation(description: "Sync did fail") + let expectedError = NSError(domain: "some error", code: 400) + await adapter.setUpProviderIfNeeded(database: database, metadataStore: metadataStore) + adapter.provider!.syncErrorPublisher + .sink { error in + expectation.fulfill() + } + .store(in: &cancellables) + + adapter.provider?.handleSyncError(expectedError) + + await self.fulfillment(of: [expectation], timeout: 10.0) + XCTAssertTrue(errorHandler.handleBookmarkErrorCalled) + XCTAssertEqual(errorHandler.capturedError as? NSError, expectedError) + } + + func testWhenSyncErrorPublished_ThenErrorHandlerSyncCredentialsSuccededCalled() async { + let expectation = XCTestExpectation(description: "Sync Did Update") + await adapter.setUpProviderIfNeeded(database: database, metadataStore: metadataStore) + + Task { + adapter.provider?.syncDidUpdateData() + expectation.fulfill() + } + + await fulfillment(of: [expectation], timeout: 5.0) + XCTAssertTrue(errorHandler.syncBookmarksSuccededCalled) + } + +} diff --git a/UnitTests/Sync/SyncCredentialsAdapterTests.swift b/UnitTests/Sync/SyncCredentialsAdapterTests.swift new file mode 100644 index 0000000000..dc4b3f86a4 --- /dev/null +++ b/UnitTests/Sync/SyncCredentialsAdapterTests.swift @@ -0,0 +1,120 @@ +// +// SyncCredentialsAdapterTests.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 BrowserServicesKit +import Combine +import DDGSync +@testable import DuckDuckGo_Privacy_Browser + +final class SyncCredentialsAdapterTests: XCTestCase { + + var errorHandler: CapturingAdapterErrorHandler! + var adapter: SyncCredentialsAdapter! + let metadataStore = MockMetadataStore() + var cancellables: Set! + + override func setUpWithError() throws { + errorHandler = CapturingAdapterErrorHandler() + adapter = SyncCredentialsAdapter(syncErrorHandler: errorHandler) + cancellables = [] + } + + override func tearDownWithError() throws { + errorHandler = nil + adapter = nil + cancellables = nil + } + + func testWhenSyncErrorPublished_ThenErrorHandlerHandleCredentialErrorCalled() async { + let expectation = XCTestExpectation(description: "Sync did fail") + let expectedError = NSError(domain: "some error", code: 400) + adapter.setUpProviderIfNeeded(secureVaultFactory: AutofillSecureVaultFactory, metadataStore: metadataStore) + adapter.provider!.syncErrorPublisher + .sink { error in + expectation.fulfill() + } + .store(in: &cancellables) + + adapter.provider?.handleSyncError(expectedError) + + await self.fulfillment(of: [expectation], timeout: 10.0) + XCTAssertTrue(errorHandler.handleCredentialErrorCalled) + XCTAssertEqual(errorHandler.capturedError as? NSError, expectedError) + } + + func testWhenSyncErrorPublished_ThenErrorHandlerSyncCredentialsSuccededCalled() async { + let expectation = XCTestExpectation(description: "Sync Did Update") + adapter.setUpProviderIfNeeded(secureVaultFactory: AutofillSecureVaultFactory, metadataStore: metadataStore) + + Task { + adapter.provider?.syncDidUpdateData() + expectation.fulfill() + } + + await fulfillment(of: [expectation], timeout: 5.0) + XCTAssertTrue(errorHandler.syncCredentialsSuccededCalled) + } + +} + +class CapturingAdapterErrorHandler: SyncErrorHandling { + var handleBookmarkErrorCalled = false + var syncCredentialsSuccededCalled = false + var handleCredentialErrorCalled = false + var syncBookmarksSuccededCalled = false + var capturedError: Error? + + func handleBookmarkError(_ error: Error) { + handleBookmarkErrorCalled = true + capturedError = error + } + + func handleCredentialError(_ error: Error) { + handleCredentialErrorCalled = true + capturedError = error + } + + func syncBookmarksSucceded() { + syncBookmarksSuccededCalled = true + } + + func syncCredentialsSucceded() { + syncCredentialsSuccededCalled = true + } +} + +class MockMetadataStore: SyncMetadataStore { + func isFeatureRegistered(named name: String) -> Bool { + return false + } + func registerFeature(named name: String, setupState: FeatureSetupState) throws {} + func deregisterFeature(named name: String) throws {} + func timestamp(forFeatureNamed name: String) -> String? { + return nil + } + func localTimestamp(forFeatureNamed name: String) -> Date? { + return nil + } + func updateLocalTimestamp(_ localTimestamp: Date?, forFeatureNamed name: String) { + } + func state(forFeatureNamed name: String) -> FeatureSetupState { + return .readyToSync + } + func update(_ serverTimestamp: String?, _ localTimestamp: Date?, _ state: FeatureSetupState, forFeatureNamed name: String) {} +} diff --git a/UnitTests/Sync/SyncErrorHandlerTests.swift b/UnitTests/Sync/SyncErrorHandlerTests.swift new file mode 100644 index 0000000000..94a33f0063 --- /dev/null +++ b/UnitTests/Sync/SyncErrorHandlerTests.swift @@ -0,0 +1,561 @@ +// +// SyncErrorHandlerTests.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 DDGSync +import Combine +@testable import DuckDuckGo_Privacy_Browser + +final class SyncErrorHandlerTests: XCTestCase { + + var cancellables: Set! + var handler: SyncErrorHandler! + var alertPresenter: CapturingAlertPresenter! + let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! + + override func setUpWithError() throws { + UserDefaultsWrapper.clearAll() + cancellables = [] + alertPresenter = CapturingAlertPresenter() + handler = SyncErrorHandler(alertPresenter: alertPresenter) + } + + override func tearDownWithError() throws { + cancellables = nil + alertPresenter = nil + handler = nil + } + + func testInitialization_DefaultsNotSet() { + let handler = SyncErrorHandler() + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenIsSyncBookmarksPaused_ThenSyncPausedChangedPublisherIsTriggered() async { + let expectation = XCTestExpectation(description: "syncPausedChangedPublisher") + handler.syncPausedChangedPublisher + .sink { _ in + expectation.fulfill() + } + .store(in: &cancellables) + + handler.handleBookmarkError(SyncError.unexpectedStatusCode(409)) + + await self.fulfillment(of: [expectation], timeout: 4.0) + XCTAssertTrue(handler.isSyncBookmarksPaused) + } + + func test_WhenIsSyncCredentialsPaused_ThenSyncPausedChangedPublisherIsTriggered() async { + let expectation = XCTestExpectation(description: "syncPausedChangedPublisher") + handler.syncPausedChangedPublisher + .sink { _ in + expectation.fulfill() + } + .store(in: &cancellables) + + handler.handleCredentialError(SyncError.unexpectedStatusCode(409)) + + await self.fulfillment(of: [expectation], timeout: 4.0) + XCTAssertTrue(handler.isSyncCredentialsPaused) + } + + func test_WhenIsSyncPaused_ThenSyncPausedChangedPublisherIsTriggered() async { + let expectation = XCTestExpectation(description: "syncPausedChangedPublisher") + + handler.syncPausedChangedPublisher + .sink { _ in + expectation.fulfill() + } + .store(in: &cancellables) + + handler.handleBookmarkError(SyncError.unexpectedStatusCode(401)) + + await self.fulfillment(of: [expectation], timeout: 4.0) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError409_ThenIsSyncBookmarksPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleBookmarkError(error) + + XCTAssertTrue(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleCredentialsError409_ThenIsSyncCredentialsPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleCredentialError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertTrue(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError413_ThenIsSyncBookmarksPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleBookmarkError(error) + + XCTAssertTrue(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleCredentialsError413_ThenIsSyncCredentialsPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleCredentialError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertTrue(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError401_ThenIsSyncPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(401) + + handler.handleBookmarkError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleCredentialsError401_ThenIsSyncIsPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(401) + + handler.handleCredentialError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError418_ThenIsSyncPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(418) + + handler.handleBookmarkError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleCredentialsError418_ThenIsSyncIsPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(418) + + handler.handleCredentialError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError429_ThenIsSyncPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(429) + + handler.handleBookmarkError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleCredentialsError429_ThenIsSyncIsPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(429) + + handler.handleCredentialError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError400_ThenIsSyncPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(400) + + handler.handleBookmarkError(error) + + XCTAssertTrue(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleCredentialsError400_ThenIsSyncIsPausedIsUpdatedToTrue() async { + let error = SyncError.unexpectedStatusCode(400) + + handler.handleCredentialError(error) + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertTrue(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError409_AndHandlerIsReinitialised_ThenErrorIsStillPresent() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleBookmarkError(error) + + handler = SyncErrorHandler() + + XCTAssertTrue(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleCredentialError409_AndHandlerIsReinitialised_ThenErrorIsStillPresent() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleCredentialError(error) + + handler = SyncErrorHandler() + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertTrue(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + } + + func test_WhenHandleCredentialError429_AndHandlerIsReinitialised_ThenErrorIsStillPresent() async { + let error = SyncError.unexpectedStatusCode(429) + + handler.handleCredentialError(error) + + handler = SyncErrorHandler() + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertTrue(handler.isSyncPaused) + } + + func test_WhenHandleBookmarksError409ForTheFirstTime_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleBookmarkError(error) + + XCTAssertTrue(alertPresenter.showAlertCalled) + } + + func test_WhenHandleBookmarksError409ForTheSecondTime_ThenAlertNotShown() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleBookmarkError(error) + + handler = SyncErrorHandler(alertPresenter: alertPresenter) + + handler.handleBookmarkError(error) + + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_WhenHandleCredentialsError409ForTheFirstTime_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleCredentialError(_:)(error) + + XCTAssertTrue(alertPresenter.showAlertCalled) + } + + func test_WhenHandleCredentialsError409ForTheSecondTime_ThenAlertNotShown() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleCredentialError(error) + + handler = SyncErrorHandler(alertPresenter: alertPresenter) + + handler.handleCredentialError(error) + + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_WhenHandleBookmarksError413ForTheFirstTime_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleBookmarkError(error) + + XCTAssertTrue(alertPresenter.showAlertCalled) + } + + func test_WhenHandleBookmarksError413ForTheSecondTime_ThenAlertNotShown() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleBookmarkError(error) + + handler = SyncErrorHandler(alertPresenter: alertPresenter) + + handler.handleBookmarkError(error) + + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_WhenHandleCredentialsError413ForTheFirstTime_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleCredentialError(_:)(error) + + XCTAssertTrue(alertPresenter.showAlertCalled) + } + + func test_WhenHandleCredentialsError413ForTheSecondTime_ThenAlertNotShown() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleCredentialError(error) + + handler = SyncErrorHandler(alertPresenter: alertPresenter) + + handler.handleCredentialError(error) + + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_WhenHandleCredentialsError413_AndThenHandleBookmarksError413_ThenAlertShownTwice() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleCredentialError(error) + + handler = SyncErrorHandler(alertPresenter: alertPresenter) + + handler.handleBookmarkError(_:)(error) + + XCTAssertEqual(alertPresenter.showAlertCount, 2) + } + + func test_WhenHandleCredentialsError401ForTheFirstTime_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(401) + + handler.handleCredentialError(_:)(error) + + XCTAssertTrue(alertPresenter.showAlertCalled) + } + + func test_WhenHandleBookmarksError401ForTheSecondTime_ThenNoAlertShown() async { + let error = SyncError.unexpectedStatusCode(401) + + handler.handleBookmarkError(_:)(error) + + handler = SyncErrorHandler(alertPresenter: alertPresenter) + + handler.handleBookmarkError(_:)(error) + + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_WhenHandleCredentialsError429ForTheFirstTime_ThenNoAlertShown() async { + let error = SyncError.unexpectedStatusCode(429) + + handler.handleCredentialError(_:)(error) + + XCTAssertFalse(alertPresenter.showAlertCalled) + } + + func test_WhenHandleBookarksError418ForTheFirstTime_ThenNoAlertShown() async { + let error = SyncError.unexpectedStatusCode(418) + + handler.handleCredentialError(_:)(error) + + XCTAssertFalse(alertPresenter.showAlertCalled) + } + + func test_WhenHandleBookarksError429ForTheFirstTime_ThenNoAlertShown() async { + let error = SyncError.unexpectedStatusCode(429) + + handler.handleCredentialError(_:)(error) + + XCTAssertFalse(alertPresenter.showAlertCalled) + } + + func test_When429ErrorFired9Times_ThenNoAlertShown() async { + let error = SyncError.unexpectedStatusCode(429) + + for _ in 0...8 { + handler.handleCredentialError(_:)(error) + } + + XCTAssertFalse(alertPresenter.showAlertCalled) + } + + func test_When429ErrorFired10Times_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(429) + + for _ in 0...9 { + handler.handleCredentialError(_:)(error) + } + + let currentTime = Date() + let timeDifference = currentTime.timeIntervalSince(userDefaults.value(forKey: UserDefaultsWrapper.Key.syncLastErrorNotificationTime.rawValue) as! Date) + XCTAssertTrue(alertPresenter.showAlertCalled) + XCTAssertTrue(abs(timeDifference) <= 5) + } + + func test_When418ErrorFired10TimesTwice_ThenAlertShownOnce() async { + let error = SyncError.unexpectedStatusCode(418) + + for _ in 0...20 { + handler.handleCredentialError(_:)(error) + } + + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_whenSyncBookmarksSucced_ThenDateSaved() { + handler.syncBookmarksSucceded() + let actualTime = userDefaults.value(forKey: UserDefaultsWrapper.Key.syncLastSuccesfullTime.rawValue) as! Date + let currentTime = Date() + let timeDifference = currentTime.timeIntervalSince(actualTime) + + XCTAssertNotNil(actualTime) + XCTAssertTrue(abs(timeDifference) <= 5) + } + + func test_whenSyncBookmarksSucced_ThenError401AlertCanBeShownAgain() async { + let error = SyncError.unexpectedStatusCode(401) + + handler.handleBookmarkError(_:)(error) + + XCTAssertTrue(handler.isSyncPaused) + XCTAssertEqual(alertPresenter.showAlertCount, 1) + handler.syncBookmarksSucceded() + + handler.handleBookmarkError(_:)(error) + + XCTAssertTrue(handler.isSyncPaused) + XCTAssertEqual(alertPresenter.showAlertCount, 2) + } + + func test_whenSyncBookmarksSucced_ThenError409AlertCanBeShownAgain() async { + let error = SyncError.unexpectedStatusCode(409) + + handler.handleBookmarkError(_:)(error) + + XCTAssertTrue(handler.isSyncBookmarksPaused) + XCTAssertEqual(alertPresenter.showAlertCount, 1) + handler.syncBookmarksSucceded() + + handler.handleBookmarkError(_:)(error) + + XCTAssertTrue(handler.isSyncBookmarksPaused) + XCTAssertEqual(alertPresenter.showAlertCount, 2) + } + + func test_whenSyncCredentialsSucced_ThenError413AlertCanBeShownAgain() async { + let error = SyncError.unexpectedStatusCode(413) + + handler.handleCredentialError(_:)(error) + + XCTAssertTrue(handler.isSyncCredentialsPaused) + XCTAssertEqual(alertPresenter.showAlertCount, 1) + handler.syncCredentialsSucceded() + + handler.handleCredentialError(_:)(error) + + XCTAssertTrue(handler.isSyncCredentialsPaused) + XCTAssertEqual(alertPresenter.showAlertCount, 2) + } + + func test_whenCredentialsSucced_ThenDateSaved() { + handler.syncCredentialsSucceded() + let actualTime = userDefaults.value(forKey: UserDefaultsWrapper.Key.syncLastSuccesfullTime.rawValue) as! Date + let currentTime = Date() + let timeDifference = currentTime.timeIntervalSince(actualTime) + + XCTAssertNotNil(actualTime) + XCTAssertTrue(abs(timeDifference) <= 5) + } + + func test_When429ErrorFiredAfter12HoursFromLastSuccessfulSync_ThenAlertShown() async { + let error = SyncError.unexpectedStatusCode(429) + let thirteenHoursAgo = Calendar.current.date(byAdding: .hour, value: -13, to: Date())! + + handler.handleCredentialError(_:)(error) + + userDefaults.set(thirteenHoursAgo, forKey: UserDefaultsWrapper.Key.syncLastSuccesfullTime.rawValue) + + handler.handleCredentialError(_:)(error) + + XCTAssertTrue(alertPresenter.showAlertCalled) + XCTAssertEqual(alertPresenter.showAlertCount, 1) + } + + func test_When429ErrorFiredAfter12HoursFromLastSuccessfulSync_ButNoErrorRegisteredBefore_ThenNoAlertShown() async { + let error = SyncError.unexpectedStatusCode(418) + let thirteenHoursAgo = Calendar.current.date(byAdding: .hour, value: -13, to: Date())! + userDefaults.set(thirteenHoursAgo, forKey: UserDefaultsWrapper.Key.syncLastSuccesfullTime.rawValue) + + handler.handleCredentialError(_:)(error) + + XCTAssertFalse(alertPresenter.showAlertCalled) + } + + func test_When429ErrorFired10Times_AndAfter24H_400ErrorFired10TimesAgain_ThenAlertShownTwice() async { + let error = SyncError.unexpectedStatusCode(429) + + for _ in 0...9 { + handler.handleCredentialError(_:)(error) + } + + XCTAssertTrue(alertPresenter.showAlertCalled) + let oneDayAgo = Calendar.current.date(byAdding: .hour, value: -25, to: Date())! + userDefaults.set(oneDayAgo, forKey: UserDefaultsWrapper.Key.syncLastErrorNotificationTime.rawValue) + + for _ in 0...9 { + handler.handleCredentialError(_:)(error) + } + + XCTAssertTrue(alertPresenter.showAlertCalled) + XCTAssertEqual(alertPresenter.showAlertCount, 2) + } + + func test_whenSyncTurnedOff_errorsAreReset() { + handler.handleCredentialError(_:)(SyncError.unexpectedStatusCode(409)) + handler.handleBookmarkError(_:)(SyncError.unexpectedStatusCode(409)) + handler.handleBookmarkError(_:)(SyncError.unexpectedStatusCode(401)) + + userDefaults.set(Date(), forKey: UserDefaultsWrapper.Key.syncLastErrorNotificationTime.rawValue) + userDefaults.set(true, forKey: UserDefaultsWrapper.Key.syncBookmarksPausedErrorDisplayed.rawValue) + userDefaults.set(true, forKey: UserDefaultsWrapper.Key.syncCredentialsPausedErrorDisplayed.rawValue) + userDefaults.set(true, forKey: UserDefaultsWrapper.Key.syncInvalidLoginPausedErrorDisplayed.rawValue) + userDefaults.set(true, forKey: UserDefaultsWrapper.Key.syncLastErrorNotificationTime.rawValue) + userDefaults.set(6, forKey: UserDefaultsWrapper.Key.syncLastNonActionableErrorCount.rawValue) + + handler.syncDidTurnOff() + + XCTAssertFalse(handler.isSyncBookmarksPaused) + XCTAssertFalse(handler.isSyncCredentialsPaused) + XCTAssertFalse(handler.isSyncPaused) + + XCTAssertNil(userDefaults.value(forKey: UserDefaultsWrapper.Key.syncLastSuccesfullTime.rawValue)) + XCTAssertFalse(userDefaults.bool(forKey: UserDefaultsWrapper.Key.syncBookmarksPausedErrorDisplayed.rawValue)) + XCTAssertFalse(userDefaults.bool(forKey: UserDefaultsWrapper.Key.syncCredentialsPausedErrorDisplayed.rawValue)) + XCTAssertFalse(userDefaults.bool(forKey: UserDefaultsWrapper.Key.syncInvalidLoginPausedErrorDisplayed.rawValue)) + XCTAssertNil(userDefaults.value(forKey: UserDefaultsWrapper.Key.syncLastErrorNotificationTime.rawValue)) + XCTAssertEqual(userDefaults.integer(forKey: UserDefaultsWrapper.Key.syncLastNonActionableErrorCount.rawValue), 0) + } +} + +class CapturingAlertPresenter: SyncAlertsPresenting { + var showAlertCalled = false + var showAlertCount = 0 + func showSyncPausedAlert(title: String, informative: String) { + showAlertCalled = true + showAlertCount += 1 + } +} diff --git a/UnitTests/Sync/SyncPreferencesTests.swift b/UnitTests/Sync/SyncPreferencesTests.swift index 1a0557f5b3..ea2de50259 100644 --- a/UnitTests/Sync/SyncPreferencesTests.swift +++ b/UnitTests/Sync/SyncPreferencesTests.swift @@ -42,21 +42,25 @@ final class SyncPreferencesTests: XCTestCase { var ddgSyncing: MockDDGSyncing! var syncBookmarksAdapter: SyncBookmarksAdapter! var syncCredentialsAdapter: SyncCredentialsAdapter! - var appearancePersistor = MockPersistor() + var appearancePersistor = MockAppearancePreferencesPersistor() var appearancePreferences: AppearancePreferences! var syncPreferences: SyncPreferences! + var pausedStateManager: MockSyncPausedStateManaging! var testRecoveryCode = "some code" + var cancellables: Set! var bookmarksDatabase: CoreDataDatabase! var location: URL! override func setUp() { + cancellables = [] setUpDatabase() appearancePreferences = AppearancePreferences(persistor: appearancePersistor) ddgSyncing = MockDDGSyncing(authState: .inactive, scheduler: scheduler, isSyncInProgress: false) + pausedStateManager = MockSyncPausedStateManaging() - syncBookmarksAdapter = SyncBookmarksAdapter(database: bookmarksDatabase, appearancePreferences: appearancePreferences) - syncCredentialsAdapter = SyncCredentialsAdapter(secureVaultFactory: AutofillSecureVaultFactory) + syncBookmarksAdapter = SyncBookmarksAdapter(database: bookmarksDatabase, appearancePreferences: appearancePreferences, syncErrorHandler: SyncErrorHandler()) + syncCredentialsAdapter = SyncCredentialsAdapter(secureVaultFactory: AutofillSecureVaultFactory, syncErrorHandler: SyncErrorHandler()) syncPreferences = SyncPreferences( syncService: ddgSyncing, @@ -64,13 +68,15 @@ final class SyncPreferencesTests: XCTestCase { syncCredentialsAdapter: syncCredentialsAdapter, appearancePreferences: appearancePreferences, managementDialogModel: managementDialogModel, - userAuthenticator: MockUserAuthenticator() + userAuthenticator: MockUserAuthenticator(), + syncPausedStateManager: pausedStateManager ) } override func tearDown() { ddgSyncing = nil syncPreferences = nil + pausedStateManager = nil tearDownDatabase() } @@ -147,86 +153,117 @@ final class SyncPreferencesTests: XCTestCase { XCTAssertTrue(ddgSyncing.disconnectCalled) } -} - -class MockDDGSyncing: DDGSyncing { - - let registeredDevices = [RegisteredDevice(id: "1", name: "Device 1", type: "desktop"), RegisteredDevice(id: "2", name: "Device 2", type: "mobile"), RegisteredDevice(id: "3", name: "Device 1", type: "desktop")] - var disconnectCalled = false - - var dataProvidersSource: DataProvidersSource? - - @Published var featureFlags: SyncFeatureFlags = .all - - var featureFlagsPublisher: AnyPublisher { - $featureFlags.eraseToAnyPublisher() - } + // MARK: - SYNC ERRORS + @MainActor + func test_WhenSyncPausedIsTrue_andChangePublished_isSyncPausedIsUpdated() async { + let expectation2 = XCTestExpectation(description: "isSyncPaused received the update") + let expectation1 = XCTestExpectation(description: "isSyncPaused published") + syncPreferences.$isSyncPaused + .dropFirst() + .sink { isPaused in + XCTAssertTrue(isPaused) + expectation2.fulfill() + } + .store(in: &cancellables) - @Published var authState: SyncAuthState = .inactive + Task { + pausedStateManager.isSyncPaused = true + pausedStateManager.isSyncPausedChangedPublisher.send() + expectation1.fulfill() + } - var authStatePublisher: AnyPublisher { - $authState.eraseToAnyPublisher() + await self.fulfillment(of: [expectation1, expectation2], timeout: 5.0) } - var account: SyncAccount? + @MainActor + func test_WhenSyncBookmarksPausedIsTrue_andChangePublished_isSyncBookmarksPausedIsUpdated() async { + let expectation2 = XCTestExpectation(description: "isSyncBookmarksPaused received the update") + let expectation1 = XCTestExpectation(description: "isSyncBookmarksPaused published") + syncPreferences.$isSyncBookmarksPaused + .dropFirst() + .sink { isPaused in + XCTAssertTrue(isPaused) + expectation2.fulfill() + } + .store(in: &cancellables) - var scheduler: Scheduling - - var syncDailyStats = SyncDailyStats(store: MockKeyValueStore()) - - @Published var isSyncInProgress: Bool + Task { + pausedStateManager.isSyncBookmarksPaused = true + pausedStateManager.isSyncPausedChangedPublisher.send() + expectation1.fulfill() + } - var isSyncInProgressPublisher: AnyPublisher { - $isSyncInProgress.eraseToAnyPublisher() + await self.fulfillment(of: [expectation1, expectation2], timeout: 5.0) } - init(dataProvidersSource: DataProvidersSource? = nil, authState: SyncAuthState, account: SyncAccount? = nil, scheduler: Scheduling = CapturingScheduler(), isSyncInProgress: Bool) { - self.dataProvidersSource = dataProvidersSource - self.authState = authState - self.account = account - self.scheduler = scheduler - self.isSyncInProgress = isSyncInProgress - } + @MainActor + func test_WhenSyncCredentialsPausedIsTrue_andChangePublished_isSyncCredentialsPausedIsUpdated() async { + let expectation2 = XCTestExpectation(description: "isSyncCredentialsPaused received the update") + let expectation1 = XCTestExpectation(description: "isSyncCredentialsPaused published") + syncPreferences.$isSyncCredentialsPaused + .dropFirst() + .sink { isPaused in + XCTAssertTrue(isPaused) + expectation2.fulfill() + } + .store(in: &cancellables) - func initializeIfNeeded() { - } + Task { + pausedStateManager.isSyncCredentialsPaused = true + pausedStateManager.isSyncPausedChangedPublisher.send() + expectation1.fulfill() + } - func createAccount(deviceName: String, deviceType: String) async throws { + await self.fulfillment(of: [expectation1, expectation2], timeout: 5.0) } - func login(_ recoveryKey: SyncCode.RecoveryKey, deviceName: String, deviceType: String) async throws -> [RegisteredDevice] { - return [] - } + @MainActor + func test_WhenSyncIsTurnedOff_ErrorHandlerSyncDidTurnOffCalled() async { + let expectation = XCTestExpectation(description: "Sync Turned off") - func remoteConnect() throws -> RemoteConnecting { - return MockRemoteConnecting() - } + Task { + syncPreferences.turnOffSync() + expectation.fulfill() + } - func transmitRecoveryKey(_ connectCode: SyncCode.ConnectCode) async throws { + await fulfillment(of: [expectation], timeout: 5.0) + XCTAssertTrue(pausedStateManager.syncDidTurnOffCalled) } - func disconnect() async throws { - disconnectCalled = true - } + @MainActor + func test_WhenAccountRemoved_ErrorHandlerSyncDidTurnOffCalled() async { + let expectation = XCTestExpectation(description: "Sync Turned off") - func disconnect(deviceId: String) async throws { - } + Task { + syncPreferences.deleteAccount() + expectation.fulfill() + } - func fetchDevices() async throws -> [RegisteredDevice] { - return registeredDevices + await fulfillment(of: [expectation], timeout: 5.0) + XCTAssertTrue(pausedStateManager.syncDidTurnOffCalled) } - func updateDeviceName(_ name: String) async throws -> [RegisteredDevice] { - return [] + func test_ErrorHandlerReturnsExpectedSyncBookmarksPausedMetadata() { + XCTAssertEqual(syncPreferences.syncBookmarksPausedTitle, MockSyncPausedStateManaging.syncBookmarksPausedData.title) + XCTAssertEqual(syncPreferences.syncBookmarksPausedMessage, MockSyncPausedStateManaging.syncBookmarksPausedData.description) + XCTAssertEqual(syncPreferences.syncBookmarksPausedButtonTitle, MockSyncPausedStateManaging.syncBookmarksPausedData.buttonTitle) + XCTAssertNotNil(syncPreferences.syncBookmarksPausedButtonAction) } - func deleteAccount() async throws { + func test_ErrorHandlerReturnsExpectedSyncCredentialsPausedMetadata() { + XCTAssertEqual(syncPreferences.syncCredentialsPausedTitle, MockSyncPausedStateManaging.syncCredentialsPausedData.title) + XCTAssertEqual(syncPreferences.syncCredentialsPausedMessage, MockSyncPausedStateManaging.syncCredentialsPausedData.description) + XCTAssertEqual(syncPreferences.syncCredentialsPausedButtonTitle, MockSyncPausedStateManaging.syncCredentialsPausedData.buttonTitle) + XCTAssertNotNil(syncPreferences.syncCredentialsPausedButtonAction) } - var serverEnvironment: ServerEnvironment = .production - - func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) { + func test_ErrorHandlerReturnsExpectedSyncIsPausedMetadata() { + XCTAssertEqual(syncPreferences.syncPausedTitle, MockSyncPausedStateManaging.syncIsPausedData.title) + XCTAssertEqual(syncPreferences.syncPausedMessage, MockSyncPausedStateManaging.syncIsPausedData.description) + XCTAssertEqual(syncPreferences.syncPausedButtonTitle, MockSyncPausedStateManaging.syncIsPausedData.buttonTitle) + XCTAssertNil(syncPreferences.syncPausedButtonAction) } + } class CapturingScheduler: Scheduling { @@ -259,29 +296,3 @@ struct MockRemoteConnecting: RemoteConnecting { func stopPolling() { } } - -struct MockPersistor: AppearancePreferencesPersistor { - - var homeButtonPosition: HomeButtonPosition = .hidden - - var showFullURL: Bool = false - - var showAutocompleteSuggestions: Bool = false - - var currentThemeName: String = "" - - var defaultPageZoom: CGFloat = 1.0 - - var favoritesDisplayMode: String? - - var isFavoriteVisible: Bool = true - - var isContinueSetUpVisible: Bool = true - - var isRecentActivityVisible: Bool = true - - var showBookmarksBar: Bool = false - - var bookmarksBarAppearance: BookmarksBarAppearance = .alwaysOn - -} From 0772b34f9657c6ac28fa146f065f83c4228a6ca7 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 10 May 2024 11:18:00 +0000 Subject: [PATCH 096/134] Bump version to 1.87.0 (187) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 9df09905e5..c6162a6ecf 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 186 +CURRENT_PROJECT_VERSION = 187 From 89d4dda0a6f433de09ed676f1b6b67550e42b1a2 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 10 May 2024 13:22:52 +0200 Subject: [PATCH 097/134] Add pixels to track Duck Player usage (#2760) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207208910013350/f Description: This change updates existing Duck Player pixels and adds a bunch of new pixels. --- .../View/PreferencesDuckPlayerView.swift | 9 +++ DuckDuckGo/Statistics/GeneralPixel.swift | 36 ++++++++-- .../DuckPlayerTabExtension.swift | 4 +- DuckDuckGo/YoutubePlayer/DuckPlayer.swift | 69 ++++++++++++++----- .../YoutubeOverlayUserScript.swift | 45 +++++++++--- .../YoutubePlayerUserScript.swift | 2 +- 6 files changed, 128 insertions(+), 37 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesDuckPlayerView.swift b/DuckDuckGo/Preferences/View/PreferencesDuckPlayerView.swift index 5d53b04bd7..d2549ade5c 100644 --- a/DuckDuckGo/Preferences/View/PreferencesDuckPlayerView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesDuckPlayerView.swift @@ -17,6 +17,7 @@ // import PreferencesViews +import PixelKit import SwiftUI import SwiftUIExtensions @@ -30,6 +31,14 @@ extension Preferences { model.duckPlayerMode } set: { newValue in model.duckPlayerMode = newValue + switch model.duckPlayerMode { + case .enabled: + PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysSettings) + case .alwaysAsk: + PixelKit.fire(GeneralPixel.duckPlayerSettingBackToDefault) + case .disabled: + PixelKit.fire(GeneralPixel.duckPlayerSettingNeverSettings) + } } } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index f162ae2590..85747d96f1 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -79,9 +79,17 @@ enum GeneralPixel: PixelKitEventV2 { case duckPlayerViewFromYoutubeAutomatic case duckPlayerViewFromSERP case duckPlayerViewFromOther - case duckPlayerSettingAlways - case duckPlayerSettingNever + case duckPlayerOverlayYoutubeImpressions + case duckPlayerOverlayYoutubeWatchHere + case duckPlayerSettingAlwaysDuckPlayer + case duckPlayerSettingAlwaysOverlaySERP + case duckPlayerSettingAlwaysOverlayYoutube + case duckPlayerSettingAlwaysSettings + case duckPlayerSettingNeverOverlaySERP + case duckPlayerSettingNeverOverlayYoutube + case duckPlayerSettingNeverSettings case duckPlayerSettingBackToDefault + case duckPlayerWatchOnYoutube // Dashboard case dashboardProtectionAllowlistAdd(triggerOrigin: String?) @@ -413,12 +421,28 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_duck-player_view-from_serp" case .duckPlayerViewFromOther: return "m_mac_duck-player_view-from_other" - case .duckPlayerSettingAlways: - return "m_mac_duck-player_setting_always" - case .duckPlayerSettingNever: - return "m_mac_duck-player_setting_never" + case .duckPlayerSettingAlwaysSettings: + return "m_mac_duck-player_setting_always_settings" + case .duckPlayerOverlayYoutubeImpressions: + return "m_mac_duck-player_overlay_youtube_impressions" + case .duckPlayerOverlayYoutubeWatchHere: + return "m_mac_duck-player_overlay_youtube_watch_here" + case .duckPlayerSettingAlwaysDuckPlayer: + return "m_mac_duck-player_setting_always_duck-player" + case .duckPlayerSettingAlwaysOverlaySERP: + return "m_mac_duck-player_setting_always_overlay_serp" + case .duckPlayerSettingAlwaysOverlayYoutube: + return "m_mac_duck-player_setting_always_overlay_youtube" + case .duckPlayerSettingNeverOverlaySERP: + return "m_mac_duck-player_setting_never_overlay_serp" + case .duckPlayerSettingNeverOverlayYoutube: + return "m_mac_duck-player_setting_never_overlay_youtube" + case .duckPlayerSettingNeverSettings: + return "m_mac_duck-player_setting_never_settings" case .duckPlayerSettingBackToDefault: return "m_mac_duck-player_setting_back-to-default" + case .duckPlayerWatchOnYoutube: + return "m_mac_duck-player_watch_on_youtube" case .dashboardProtectionAllowlistAdd: return "m_mac_mp_wla" diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift index c34ab6a88f..30c98f51a7 100644 --- a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift @@ -273,6 +273,7 @@ extension DuckPlayerTabExtension: NavigationResponder { // when currently displayed content is the Duck Player and loading a YouTube URL, don‘t override it if navigationAction.targetFrame?.url.isDuckPlayer == true, navigationAction.targetFrame?.url.youtubeVideoID == videoID { + PixelKit.fire(GeneralPixel.duckPlayerWatchOnYoutube) return .next // If this is a child tab of a Duck Player and it's loading a YouTube URL, don‘t override it @@ -312,7 +313,8 @@ extension DuckPlayerTabExtension: NavigationResponder { return } if navigation.url.isDuckPlayer { - PixelKit.fire(GeneralPixel.duckPlayerDailyUniqueView, frequency: .legacyDaily) + let setting = duckPlayer.mode == .enabled ? "always" : "default" + PixelKit.fire(GeneralPixel.duckPlayerDailyUniqueView, frequency: .legacyDaily, withAdditionalParameters: ["setting": setting]) } } diff --git a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift index abbd1fb71e..404ff6f265 100644 --- a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift +++ b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift @@ -109,16 +109,59 @@ final class DuckPlayer { // MARK: - Common Message Handlers - public func handleSetUserValues(params: Any, message: UserScriptMessage) -> Encodable? { - guard let userValues: UserValues = DecodableHelper.decode(from: params) else { - assertionFailure("YoutubeOverlayUserScript: expected JSON representation of UserValues") - return nil - } + // swiftlint:disable:next cyclomatic_complexity + public func handleSetUserValuesMessage( + from origin: YoutubeOverlayUserScript.MessageOrigin + ) -> (_ params: Any, _ message: UserScriptMessage) -> Encodable? { + + return { [weak self] params, _ -> Encodable? in + guard let self else { + return nil + } + guard let userValues: UserValues = DecodableHelper.decode(from: params) else { + assertionFailure("YoutubeOverlayUserScript: expected JSON representation of UserValues") + return nil + } - self.preferences.youtubeOverlayInteracted = userValues.overlayInteracted - self.preferences.duckPlayerMode = userValues.duckPlayerMode + let modeDidChange = self.preferences.duckPlayerMode != userValues.duckPlayerMode + let overlayDidInteract = !self.preferences.youtubeOverlayInteracted && userValues.overlayInteracted + + if modeDidChange { + self.preferences.duckPlayerMode = userValues.duckPlayerMode + if case .enabled = userValues.duckPlayerMode { + switch origin { + case .duckPlayer: + PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysDuckPlayer) + case .serpOverlay: + PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysOverlaySERP) + case .youtubeOverlay: + PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysOverlayYoutube) + } + } + } - return encodeUserValues() + if overlayDidInteract { + self.preferences.youtubeOverlayInteracted = userValues.overlayInteracted + + // If user checks "Remember my choice" and clicks "Watch here", we won't show + // the overlay anymore, but will keep presenting Dax logos (the mode stays at + // "alwaysAsk" which may be a bit counterintuitive, but it's the overlayInteracted + // flag that plays a role here). We want to track users opting in to not showing overlays, + // hence firing the pixel here. + if userValues.duckPlayerMode == .alwaysAsk { + switch origin { + case .serpOverlay: + PixelKit.fire(GeneralPixel.duckPlayerSettingNeverOverlaySERP) + case .youtubeOverlay: + PixelKit.fire(GeneralPixel.duckPlayerSettingNeverOverlayYoutube) + default: + break + } + } + } + + return self.encodeUserValues() + } } public func handleGetUserValues(params: Any, message: UserScriptMessage) -> Encodable? { @@ -150,16 +193,6 @@ final class DuckPlayer { modeCancellable = preferences.$duckPlayerMode .removeDuplicates() .dropFirst(1) - .handleEvents(receiveOutput: { mode in - switch mode { - case .enabled: - PixelKit.fire(GeneralPixel.duckPlayerSettingAlways) - case .alwaysAsk: - PixelKit.fire(GeneralPixel.duckPlayerSettingBackToDefault) - case .disabled: - PixelKit.fire(GeneralPixel.duckPlayerSettingNever) - } - }) .prepend(preferences.duckPlayerMode) .assign(to: \.mode, onWeaklyHeld: self) } else { diff --git a/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift b/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift index e5b711db73..dd61eea14e 100644 --- a/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift @@ -28,6 +28,21 @@ protocol YoutubeOverlayUserScriptDelegate: AnyObject { final class YoutubeOverlayUserScript: NSObject, Subfeature { + enum MessageOrigin { + case duckPlayer, serpOverlay, youtubeOverlay + + init?(url: URL) { + switch url.host { + case "duckduckgo.com": + self = .serpOverlay + case "www.youtube.com": + self = .youtubeOverlay + default: + return nil + } + } + } + let duckPlayerPreferences: DuckPlayerPreferences weak var broker: UserScriptMessageBroker? weak var delegate: YoutubeOverlayUserScriptDelegate? @@ -60,7 +75,11 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { switch MessageNames(rawValue: methodName) { case .setUserValues: - return DuckPlayer.shared.handleSetUserValues + guard let url = webView?.url, let origin = MessageOrigin(url: url) else { + assertionFailure("YoutubeOverlayUserScript: Unexpected message origin: \(String(describing: webView?.url))") + return nil + } + return DuckPlayer.shared.handleSetUserValuesMessage(from: origin) case .getUserValues: return DuckPlayer.shared.handleGetUserValues case .openDuckPlayer: @@ -111,20 +130,24 @@ extension YoutubeOverlayUserScript { return nil } let pixelName = parameters["pixelName"] as? String - if pixelName == "play.use" || pixelName == "play.do_not_use" { + + switch pixelName { + case "play.use": duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true - if pixelName == "play.use" { - PixelKit.fire(GeneralPixel.duckPlayerViewFromYoutubeViaMainOverlay) + PixelKit.fire(GeneralPixel.duckPlayerViewFromYoutubeViaMainOverlay) + // Temporary pixel for first time user uses Duck Player + if AppDelegate.isNewUser { + PixelKit.fire(GeneralPixel.watchInDuckPlayerInitial, frequency: .legacyInitial) } + case "play.do_not_use": + duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true + PixelKit.fire(GeneralPixel.duckPlayerOverlayYoutubeWatchHere) + case "overlay": + PixelKit.fire(GeneralPixel.duckPlayerOverlayYoutubeImpressions) + default: + break } - // Temporary pixel for first time user uses Duck Player - if !AppDelegate.isNewUser { - return nil - } - if pixelName == "play.use" { - PixelKit.fire(GeneralPixel.watchInDuckPlayerInitial, frequency: .legacyInitial) - } return nil } } diff --git a/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift b/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift index a3b19e9adf..0d4a7dd9b4 100644 --- a/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift @@ -52,7 +52,7 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { case .getUserValues: return DuckPlayer.shared.handleGetUserValues case .setUserValues: - return DuckPlayer.shared.handleSetUserValues + return DuckPlayer.shared.handleSetUserValuesMessage(from: .duckPlayer) default: assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)") return nil From 2619e1fa6664c6368cbafe8fd2cafaca797427f4 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 10 May 2024 15:12:52 +0200 Subject: [PATCH 098/134] Enable gzip compression for Sync PATCH payloads (#2723) Task/Issue URL: https://app.asana.com/0/0/1206919211758354/f Description: This change adds gzip compression to all Sync data PATCH requests. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 19 ++++++++++++++----- DuckDuckGo/Statistics/GeneralPixel.swift | 6 ++++++ DuckDuckGo/Sync/SyncErrorHandler.swift | 10 ++++++++++ DuckDuckGo/Sync/SyncSettingsAdapter.swift | 5 +++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 8 files changed, 39 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ac682a5f8c..8086840be4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12842,7 +12842,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.5; + version = 144.0.6; }; }; 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 0fabc33d55..c8835ebb9b 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" : "151737e0b690437a127cc6f1b9f443481cf2f645", - "version" : "144.0.5" + "revision" : "72be4e73360989af170399bc063fd5c628e1e84c", + "version" : "144.0.6" } }, { @@ -63,6 +63,15 @@ "version" : "2.3.0" } }, + { + "identity" : "gzipswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/1024jp/GzipSwift.git", + "state" : { + "revision" : "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", + "version" : "6.0.1" + } + }, { "identity" : "lottie-spm", "kind" : "remoteSourceControl", @@ -120,7 +129,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "46989693916f56d1186bd59ac15124caef896560", "version" : "1.3.1" @@ -138,7 +147,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" @@ -165,7 +174,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", "state" : { "revision" : "6c84fd19139414fc0edbf9673ade06e532a564f0", "version" : "2.0.0" diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 85747d96f1..90cb21ae2b 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -285,10 +285,13 @@ enum GeneralPixel: PixelKitEventV2 { case syncMetadataCouldNotLoadDatabase case syncBookmarksProviderInitializationFailed case syncBookmarksFailed + case syncBookmarksPatchCompressionFailed case syncCredentialsProviderInitializationFailed case syncCredentialsFailed + case syncCredentialsPatchCompressionFailed case syncSettingsFailed case syncSettingsMetadataUpdateFailed + case syncSettingsPatchCompressionFailed case syncSignupError(error: Error) case syncLoginError(error: Error) case syncLogoutError(error: Error) @@ -756,10 +759,13 @@ enum GeneralPixel: PixelKitEventV2 { case .syncMetadataCouldNotLoadDatabase: return "sync_metadata_could_not_load_database" case .syncBookmarksProviderInitializationFailed: return "sync_bookmarks_provider_initialization_failed" case .syncBookmarksFailed: return "sync_bookmarks_failed" + case .syncBookmarksPatchCompressionFailed: return "sync_bookmarks_patch_compression_failed" case .syncCredentialsProviderInitializationFailed: return "sync_credentials_provider_initialization_failed" case .syncCredentialsFailed: return "sync_credentials_failed" + case .syncCredentialsPatchCompressionFailed: return "sync_credentials_patch_compression_failed" case .syncSettingsFailed: return "sync_settings_failed" case .syncSettingsMetadataUpdateFailed: return "sync_settings_metadata_update_failed" + case .syncSettingsPatchCompressionFailed: return "sync_settings_patch_compression_failed" case .syncSignupError: return "sync_signup_error" case .syncLoginError: return "sync_login_error" case .syncLogoutError: return "sync_logout_error" diff --git a/DuckDuckGo/Sync/SyncErrorHandler.swift b/DuckDuckGo/Sync/SyncErrorHandler.swift index 5d26731e23..0af0fb7eb8 100644 --- a/DuckDuckGo/Sync/SyncErrorHandler.swift +++ b/DuckDuckGo/Sync/SyncErrorHandler.swift @@ -227,6 +227,16 @@ extension SyncErrorHandler: SyncErrorHandling { private func handleError(_ error: Error, modelType: ModelType) { switch error { + case SyncError.patchPayloadCompressionFailed(let errorCode): + let pixel: PixelKit.Event = { + switch modelType { + case .bookmarks: + return DebugEvent(GeneralPixel.syncBookmarksPatchCompressionFailed) + case .credentials: + return DebugEvent(GeneralPixel.syncCredentialsPatchCompressionFailed) + } + }() + PixelKit.fire(pixel, withAdditionalParameters: ["error": "\(errorCode)"]) case let syncError as SyncError: switch modelType { case .bookmarks: diff --git a/DuckDuckGo/Sync/SyncSettingsAdapter.swift b/DuckDuckGo/Sync/SyncSettingsAdapter.swift index f726673087..76cd9966ec 100644 --- a/DuckDuckGo/Sync/SyncSettingsAdapter.swift +++ b/DuckDuckGo/Sync/SyncSettingsAdapter.swift @@ -63,6 +63,11 @@ final class SyncSettingsAdapter { syncErrorCancellable = provider.syncErrorPublisher .sink { error in switch error { + case SyncError.patchPayloadCompressionFailed(let errorCode): + PixelKit.fire( + DebugEvent(GeneralPixel.syncSettingsPatchCompressionFailed), + withAdditionalParameters: ["error": "\(errorCode)"] + ) case let syncError as SyncError: PixelKit.fire(DebugEvent(GeneralPixel.syncSettingsFailed, error: syncError)) case let settingsMetadataError as SettingsSyncMetadataSaveError: diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index db44fe3e2c..5d9d7c8866 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: "144.0.5"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.6"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 19008a34fb..380fd19ea9 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.5"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.6"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 8355d9146a..fa2374c63d 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: "144.0.5"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.6"), .package(path: "../SwiftUIExtensions") ], targets: [ From 8d35dd075f9546e63136b2e6c91dc9f44622ddca Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 13 May 2024 10:07:15 +0000 Subject: [PATCH 099/134] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 40 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 224457323d..f256a7425d 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"6cbc7738304e5eaef20e98417f412fc9\"" - public static let embeddedDataSHA = "0fda36c6cb3a0f2bcd34562ad9d4bc8fe7dc2d21e029b87e323cb4992bfddb01" + public static let embeddedDataETag = "\"e1744d6617db82fc621d71131270b348\"" + public static let embeddedDataSHA = "60bbae2f81deb1d44f6d192d063f8798ac09d927db0decfd7d994568b614392d" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index cce44b20c4..0491e2e042 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1714746648846, + "version": 1715350028654, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -297,6 +297,9 @@ { "domain": "malmostadsteater.se" }, + { + "domain": "hertz.com" + }, { "domain": "marvel.com" }, @@ -312,7 +315,7 @@ ] }, "state": "enabled", - "hash": "7096d80d39c6c9a98d98ff6bde5644e0" + "hash": "cf3197bc061274e9c23a870dea493b9a" }, "autofill": { "exceptions": [ @@ -4903,10 +4906,10 @@ "url": "https://selfserve.decipherinc.com/survey/selfserve/32ab/240404?list=2", "firstDay": 5, "lastDay": 8, - "sharePercentage": 60 + "sharePercentage": 100 } }, - "hash": "8a5c9011216219c6576ec362acb51fd0" + "hash": "cbd62a1a63a829da0277ca045a311eb2" }, "nonTracking3pCookies": { "settings": { @@ -6060,6 +6063,12 @@ "domains": [ "" ] + }, + { + "rule": "st.dynamicyield.com/st", + "domains": [ + "harborfreight.com" + ] } ] }, @@ -6760,7 +6769,14 @@ { "rule": "grow.me/main.js", "domains": [ - "budgetbytes.com" + "budgetbytes.com", + "foodfornet.com" + ] + }, + { + "rule": "api.grow.me", + "domains": [ + "foodfornet.com" ] } ] @@ -7122,6 +7138,12 @@ "domains": [ "cosmicbook.news" ] + }, + { + "rule": "scripts.mediavine.com/tags/food-for-net.js", + "domains": [ + "foodfornet.com" + ] } ] }, @@ -7266,6 +7288,12 @@ "marlboro.com" ] }, + { + "rule": "whirlpool.tt.omtrdc.net/rest/v1/delivery", + "domains": [ + "kitchenaid.com" + ] + }, { "rule": "omtrdc.net", "domains": [ @@ -8345,7 +8373,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "8abdf819c66bc3e2391e900958ed0294" + "hash": "2dd3c945cc9811719be893edcd79682e" }, "trackingCookies1p": { "settings": { From ec1073593cca806a3cb2264aec997472d760f9e7 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 13 May 2024 10:07:15 +0000 Subject: [PATCH 100/134] Set marketing version to 1.88.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 6b9d59ea7d..94ced13f63 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.87.0 +MARKETING_VERSION = 1.88.0 From 73fb016bff97e728a7f65e3320e343f755ed0a35 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 13 May 2024 10:17:12 +0000 Subject: [PATCH 101/134] Bump version to 1.88.0 (188) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index c6162a6ecf..4e984defe0 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 187 +CURRENT_PROJECT_VERSION = 188 From a2de82d04fac26f916cacd37ad346bdc59137f23 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 14 May 2024 09:37:10 +1000 Subject: [PATCH 102/134] Alessandro/batch bugs wins feedback form copy and zoom icon (#2737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1207204140599545/f **Description**: This PR updates the copy for the "send feedback form” and the zoom icon in the more options menu --- .../Contents.json | 15 ++++++++ .../OptionsButtonMenuZoom.pdf | Bin 0 -> 1189 bytes DuckDuckGo/Common/Localizables/UserText.swift | 4 +- DuckDuckGo/Localizable.xcstrings | 36 +++++++++--------- .../NavigationBar/View/MoreOptionsMenu.swift | 2 +- 5 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/OptionsButtonMenuZoom.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/Contents.json new file mode 100644 index 0000000000..0cae675c3d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "OptionsButtonMenuZoom.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/OptionsButtonMenuZoom.pdf b/DuckDuckGo/Assets.xcassets/Images/OptionsButtonMenuZoom.imageset/OptionsButtonMenuZoom.pdf new file mode 100644 index 0000000000000000000000000000000000000000..55e29d010c25a513d79a8cd1e5e08d1f7424fceb GIT binary patch literal 1189 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f^q5;-#!sTp4RV{u4^Y+2_OHt zaeKoNDGMhPjwSCVUhtSRe|pF%&NFOPm)Gfk-2C~`s^$IX_t$NCu0Q{ty!^gh%fDWU zyX)_6*K=QR-Gj7=Ya1gHUN*Z;B4j+-fI=J0Ih#Ao|<=f68z`|tfrwev@!KH2-0>vypy#V=tw zC-MG8aA)UbHr>u$?lDZdD)Y|y=7yf0H)+YD54l-Ky-#^by=0DBb_OincYXqgwiNrUl}|Lo){3s|T4Zrh;^CoFe;;usPq%EXduVE~F#SjSuOAPY z6}i%1wVA1lyO_uxf8DF7DPUQB$j!KTp`E#hq>N_p$Bhy1yDXQzQ%U{8-;(Xb`6jxl zE$YA9Qm*&rE3W(Kn>vSW*=-d0?U2&O_%~OQ)-Rtj)#TEg(zG+T%vF`6ZTA?xKW6<| zI=jIj0+gnpi2|0oK*P(G{;AzeTE(%PG`i^;d`6a~)(Xr5!7E)P|s-Pc`m<~&N zzNsmhiB9KUggb*y4NkOT_`K3k4sl^JQ)DKFwpp@*KpI4Ha2ec8C z^kISliA6v$1yiUHQU(DD!84A&cV-IEsR|$;1S!Dman3IV>NUi0e=#JZzyiYz#Uzji zi{Tc690u{Yb7DziPJTM7){3IkG%f=Lb1sPYL8OA2sj0E40#FzV3=I^(EQLI{kTKAm zAb=1uGr>@0W(EuhR8#AOB6sJ1_!WyaAs91(DmR_BeAFi>>W!p3ocbvSARDE7&)66 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index d5d25eaeea..b7ace49675 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -484,7 +484,7 @@ struct UserText { static let browserFeedbackGeneralFeedback = NSLocalizedString("send.browser.feedback.general-feedback", value: "General feedback", comment: "Name of the option the user can chose to give general browser feedback") static let browserFeedbackSelectCategory = NSLocalizedString("send.browser.feedback.select-category", value: "Select a category", comment: "Title of the picker where the user can chose the category of the feedback they want ot send.") static let browserFeedbackThankYou = NSLocalizedString("send.browser.feedback.thankyou", value: "Thank you!", comment: "Thanks the user for sending feedback") - static let browserFeedbackFeedbackHelps = NSLocalizedString("send.browser.feedback.feedback-helps", value: "Your feedback will help us improve the DuckDuckGo app.", comment: "Text shown to the user when they provide feedback.") + static let browserFeedbackFeedbackHelps = NSLocalizedString("send.browser.feedback.feedback-helps", value: "Your feedback will help us improve the DuckDuckGo browser.", comment: "Text shown to the user when they provide feedback.") static let otherBookmarksImportedFolderTitle = NSLocalizedString("bookmarks.imported.other.folder.title", value: "Other bookmarks", comment: "Name of the \"Other bookmarks\" folder imported from other browser") static let mobileBookmarksImportedFolderTitle = NSLocalizedString("bookmarks.imported.mobile.folder.title", value: "Mobile bookmarks", comment: "Name of the \"Mobile bookmarks\" folder imported from other browser") @@ -686,7 +686,7 @@ struct UserText { static let sendFeedback = NSLocalizedString("preferences.about.send-feedback", value: "Send Feedback", comment: "Feedback button in the about preferences page") - static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo") + static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo browser version, and your macOS version.", comment: "Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo") static let feedbackBugDescription = NSLocalizedString("feedback.bug.description", value: "Please describe the problem in as much detail as possible:", comment: "Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo") static let feedbackFeatureRequestDescription = NSLocalizedString("feedback.feature.request.description", value: "What feature would you like to see?", comment: "Label in the feedback form for feature requests.") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 8eb357dd84..673ca12212 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -18262,55 +18262,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Berichte, die an DuckDuckGo gesendet werden, sind 100 % anonym und enthalten nur deine Nachricht, die DuckDuckGo-App-Version und deine macOS-Version." + "value" : "Berichte, die an DuckDuckGo gesendet werden, sind 100% anonym und enthalten nur deine Nachricht, die DuckDuckGo-Browser-Version und deine macOS-Version." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version." + "value" : "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo browser version, and your macOS version." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Los informes enviados a DuckDuckGo son 100 % anónimos y solo incluyen tu mensaje, la versión de la aplicación DuckDuckGo y tu versión de macOS." + "value" : "Los informes enviados a DuckDuckGo son 100% anónimos y solo incluyen tu mensaje, la versión de navegador de DuckDuckGo y tu versión de macOS." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les rapports envoyés à DuckDuckGo sont 100 % anonymes et n'incluent que votre message, la version de l'application DuckDuckGo et la version de votre macOS." + "value" : "Les rapports envoyés à DuckDuckGo sont 100% anonymes et n'incluent que votre message, la version du navigateur DuckDuckGo et la version de votre macOS." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "I report inviati a DuckDuckGo sono anonimi al 100% e includono solo il tuo messaggio, la versione dell'app DuckDuckGo e la tua versione macOS." + "value" : "I report inviati a DuckDuckGo sono anonimi al 100% e includono solo il tuo messaggio, la versione del browser DuckDuckGo e la tua versione macOS." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Rapporten die naar DuckDuckGo worden verzonden, zijn 100% anoniem en bevatten alleen je bericht, de versie van de DuckDuckGo-app en je versie van macOS." + "value" : "Rapporten die naar DuckDuckGo worden verzonden, zijn 100% anoniem en bevatten alleen je bericht, de versie van de DuckDuckGo-browser en je versie van macOS." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Raporty wysyłane do DuckDuckGo są stuprocentowo anonimowe i zawierają jedynie wiadomość, wersję aplikacji DuckDuckGo oraz wersję systemu macOS." + "value" : "Raporty wysyłane do DuckDuckGo są w 100% anonimowe i zawierają jedynie wiadomość, wersję przeglądarki DuckDuckGo oraz wersję systemu macOS." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Os relatórios enviados para a DuckDuckGo são 100% anónimos e incluem apenas a tua mensagem, a versão da aplicação DuckDuckGo e a versão do teu macOS." + "value" : "Os relatórios enviados para a DuckDuckGo são 100% anónimos e incluem apenas a tua mensagem, a versão do navegador DuckDuckGo e a versão do teu macOS." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отчеты, отправляемые в DuckDuckGo, на 100% анонимны. В отчет входит только написанное вами сообщение и сведения о версии приложения DuckDuckGo и операционной системы (macOS)." + "value" : "Отчеты, отправляемые в DuckDuckGo, на 100% анонимны. В отчет входит только написанное вами сообщение и сведения о версии браузера DuckDuckGo и операционной системы (macOS)." } } } @@ -48103,55 +48103,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dein Feedback hilft uns, die DuckDuckGo-App zu verbessern." + "value" : "Dein Feedback hilft uns, den DuckDuckGo-Browser zu verbessern." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Your feedback will help us improve the DuckDuckGo app." + "value" : "Your feedback will help us improve the DuckDuckGo browser." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Tus comentarios nos ayudarán a mejorar la aplicación DuckDuckGo." + "value" : "Tus comentarios nos ayudarán a mejorar el navegador DuckDuckGo." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Vos commentaires nous aideront à améliorer l'application DuckDuckGo." + "value" : "Vos commentaires nous aideront à améliorer le navigateur DuckDuckGo." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Il tuo feedback ci aiuterà a migliorare l'app DuckDuckGo." + "value" : "Il tuo feedback ci aiuterà a migliorare il browser DuckDuckGo." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Jouw feedback zal ons helpen de DuckDuckGo-app te verbeteren." + "value" : "Jouw feedback helpt ons de DuckDuckGo-browser te verbeteren." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Twoja opinia pomoże nam w ulepszaniu aplikacji DuckDuckGo." + "value" : "Twoja opinia pomoże nam ulepszyć przeglądarkę DuckDuckGo." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "O teu feedback ajuda-nos a melhorar a aplicação DuckDuckGo." + "value" : "O teu feedback ajuda-nos a melhorar o navegador DuckDuckGo." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш отзыв поможет нам улучшить приложение DuckDuckGo." + "value" : "Ваш ответ поможет браузеру DuckDuckGo стать еще лучше." } } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 1b48e04049..8adccabf28 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -87,7 +87,7 @@ final class MoreOptionsMenu: NSMenu { setupMenuItems() } - let zoomMenuItem = NSMenuItem(title: UserText.zoom, action: nil, keyEquivalent: "") + let zoomMenuItem = NSMenuItem(title: UserText.zoom, action: nil, keyEquivalent: "").withImage(.optionsButtonMenuZoom) private func setupMenuItems() { From 1cf47c4bd67a62b176335e42457bb056f9ad938d Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 14 May 2024 10:20:07 +1000 Subject: [PATCH 103/134] Fix a bug that caused a single pinned tabs to expand in full screen mode (#2749) Task/Issue URL: https://app.asana.com/0/1177771139624306/1205566351996167/f **Description**: This PR fixes the pinned tab width when the application is in full screen. Setting the maxWidth on the PinnedTabView seems to fix the issue. --- DuckDuckGo/PinnedTabs/View/PinnedTabsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabsView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabsView.swift index 8dbfd358ab..d2641b8554 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabsView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabsView.swift @@ -27,7 +27,7 @@ struct PinnedTabsView: View { ForEach(model.items) { item in PinnedTabView(model: item, showsHover: draggedTab == nil) .environmentObject(model) - .frame(maxHeight: PinnedTabView.Const.dimension) + .frame(maxWidth: PinnedTabView.Const.dimension, maxHeight: PinnedTabView.Const.dimension) } } .frame(minHeight: PinnedTabView.Const.dimension) From 765192bd93f86fc583e2728855e6f196837accda Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 14 May 2024 10:23:54 +1000 Subject: [PATCH 104/134] Fix address bar different focus state color when new tab vs new window (#2750) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207076354771782/f **Description**: This PR fixes the issue where the address bar border color displayed is wrong when a new window is opened. --- .../NavigationBar/View/AddressBarViewController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 7976bb9dc6..83f7dd9360 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -25,7 +25,7 @@ final class AddressBarViewController: NSViewController { @IBOutlet var addressBarTextField: AddressBarTextField! @IBOutlet var passiveTextField: NSTextField! @IBOutlet var inactiveBackgroundView: NSView! - @IBOutlet var activeBackgroundView: NSView! + @IBOutlet var activeBackgroundView: ColorView! @IBOutlet var activeOuterBorderView: NSView! @IBOutlet var activeBackgroundViewWithSuggestions: NSView! @IBOutlet var progressIndicator: LoadingProgressView! @@ -319,10 +319,8 @@ final class AddressBarViewController: NSViewController { let isKey = self.view.window?.isKeyWindow ?? false activeOuterBorderView.alphaValue = isKey && isFirstResponder && isHomePage ? 1 : 0 - activeOuterBorderView.layer?.backgroundColor = NSColor.controlAccentColor.withAlphaComponent(0.2).cgColor - activeBackgroundView.layer?.borderColor = NSColor.controlAccentColor.withAlphaComponent(0.8).cgColor activeOuterBorderView.layer?.backgroundColor = accentColor.withAlphaComponent(0.2).cgColor - activeBackgroundView.layer?.borderColor = accentColor.withAlphaComponent(0.8).cgColor + activeBackgroundView.borderColor = accentColor.withAlphaComponent(0.8) addressBarTextField.placeholderString = tabViewModel?.tab.content == .newtab ? UserText.addressBarPlaceholder : "" } From eb439a75992dc269c4725637648b4c06ddafe3a0 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 14 May 2024 19:55:33 +0600 Subject: [PATCH 105/134] Set Navigation Bar buttons pressed state when popovers are shown (#2778) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207311510824186/f --- DuckDuckGo.xcodeproj/project.pbxproj | 6 - .../View/AppKit/HoverTrackingArea.swift | 4 +- DuckDuckGo/Menus/MainMenuActions.swift | 6 +- .../AddressBarButtonsViewController.swift | 150 ++---------- .../View/AddressBarViewController.swift | 18 +- .../View/NavigationBar.storyboard | 14 +- .../View/NavigationBarPopovers.swift | 230 +++++++++++++++--- .../View/NavigationBarViewController.swift | 58 ++--- .../View/NetPPopoverManagerMock.swift | 8 +- ...etworkProtectionNavBarPopoverManager.swift | 9 +- .../View/PrivacyDashboard.storyboard | 22 -- .../View/PrivacyDashboardPopover.swift | 14 +- .../View/PrivacyDashboardViewController.swift | 22 +- .../View/AutofillPopoverPresenter.swift | 16 +- .../Mocks/MockAutofillPopoverPresenter.swift | 6 +- .../View/NavigationBarPopoversTests.swift | 4 +- 16 files changed, 300 insertions(+), 287 deletions(-) delete mode 100644 DuckDuckGo/PrivacyDashboard/View/PrivacyDashboard.storyboard diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8086840be4..9eeaa77716 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -690,7 +690,6 @@ 3706FCCC293F65D500E42796 /* TabBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC7B256C46AA007083E7 /* TabBar.storyboard */; }; 3706FCCD293F65D500E42796 /* shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396B2754D4E300B241FA /* shield-dot.json */; }; 3706FCD0293F65D500E42796 /* BookmarksBarCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BE53369286912D40019DBFD /* BookmarksBarCollectionViewItem.xib */; }; - 3706FCD1293F65D500E42796 /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; }; 3706FCD2293F65D500E42796 /* shield.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396A2754D4E200B241FA /* shield.json */; }; 3706FCD4293F65D500E42796 /* TabBarViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA7412B124D0B3AC00D22FE0 /* TabBarViewItem.xib */; }; 3706FCD6293F65D500E42796 /* httpsMobileV2FalsePositives.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B67742A255DBEB800025BD8 /* httpsMobileV2FalsePositives.json */; }; @@ -2442,7 +2441,6 @@ B6F9BDDD2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; - B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; }; B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; }; B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; }; BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */; }; @@ -3997,7 +3995,6 @@ B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfo.swift; sourceTree = ""; }; B6F9BDE32B45CD1900677B33 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; - B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; @@ -8036,7 +8033,6 @@ B6FA893B269C41ED00588ECD /* View */ = { isa = PBXGroup; children = ( - B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */, B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */, B63BDF7D27FDAA640072D75B /* PrivacyDashboardWebView.swift */, B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */, @@ -8867,7 +8863,6 @@ 3706FCCC293F65D500E42796 /* TabBar.storyboard in Resources */, 3706FCCD293F65D500E42796 /* shield-dot.json in Resources */, 3706FCD0293F65D500E42796 /* BookmarksBarCollectionViewItem.xib in Resources */, - 3706FCD1293F65D500E42796 /* PrivacyDashboard.storyboard in Resources */, 3706FCD2293F65D500E42796 /* shield.json in Resources */, 3706FCD4293F65D500E42796 /* TabBarViewItem.xib in Resources */, 3706FCD6293F65D500E42796 /* httpsMobileV2FalsePositives.json in Resources */, @@ -9061,7 +9056,6 @@ AA80EC79256C46AA007083E7 /* TabBar.storyboard in Resources */, AA34396D2754D4E300B241FA /* shield-dot.json in Resources */, 4BE5336B286912D40019DBFD /* BookmarksBarCollectionViewItem.xib in Resources */, - B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */, AA34396C2754D4E300B241FA /* shield.json in Resources */, AA7412B324D0B3AC00D22FE0 /* TabBarViewItem.xib in Resources */, 4B677435255DBEB800025BD8 /* httpsMobileV2FalsePositives.json in Resources */, diff --git a/DuckDuckGo/Common/View/AppKit/HoverTrackingArea.swift b/DuckDuckGo/Common/View/AppKit/HoverTrackingArea.swift index 718e7ea4c0..4d0be4f564 100644 --- a/DuckDuckGo/Common/View/AppKit/HoverTrackingArea.swift +++ b/DuckDuckGo/Common/View/AppKit/HoverTrackingArea.swift @@ -124,7 +124,9 @@ final class HoverTrackingArea: NSTrackingArea { mouseExited(event) } else { - updateLayer(animated: false) + // mouse-down: non-animated + // mouse-up: animated when mouse is outside + updateLayer(animated: !view.isMouseDown && !view.isMouseLocationInsideBounds()) } } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 884a6695d5..12094bbd16 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -122,11 +122,7 @@ extension AppDelegate { } @objc func openReportBrokenSite(_ sender: Any?) { - let storyboard = NSStoryboard(name: "PrivacyDashboard", bundle: nil) - let privacyDashboardViewController = storyboard.instantiateController(identifier: "PrivacyDashboardViewController") { coder in - PrivacyDashboardViewController(coder: coder, privacyInfo: nil, dashboardMode: .report) - } - + let privacyDashboardViewController = PrivacyDashboardViewController(privacyInfo: nil, dashboardMode: .report) privacyDashboardViewController.sizeDelegate = self let window = NSWindow(contentViewController: privacyDashboardViewController) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index f277f679b6..f2a57a1c01 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -34,16 +34,6 @@ final class AddressBarButtonsViewController: NSViewController { weak var delegate: AddressBarButtonsViewControllerDelegate? - private var bookmarkPopover: AddBookmarkPopover? - private func bookmarkPopoverCreatingIfNeeded() -> AddBookmarkPopover { - return bookmarkPopover ?? { - let popover = AddBookmarkPopover() - popover.delegate = self - self.bookmarkPopover = popover - return popover - }() - } - private var permissionAuthorizationPopover: PermissionAuthorizationPopover? private func permissionAuthorizationPopoverCreatingIfNeeded() -> PermissionAuthorizationPopover { return permissionAuthorizationPopover ?? { @@ -63,26 +53,6 @@ final class AddressBarButtonsViewController: NSViewController { }() } - private var _privacyDashboardPopover: PrivacyDashboardPopover? - private var privacyDashboardPopover: PrivacyDashboardPopover { - get { - if let result = _privacyDashboardPopover { - return result - } else { - let popover = PrivacyDashboardPopover() - popover.delegate = self - self.privacyDashboardPopover = popover - self.subscribePrivacyDashboardPendingUpdates(privacyDashboardPopover: popover) - return popover - } - } - set { - _privacyDashboardPopover = newValue - } - } - - @IBOutlet weak var privacyDashboardPositioningView: NSView! - @IBOutlet weak var privacyEntryPointButton: MouseOverAnimationButton! @IBOutlet weak var bookmarkButton: AddressBarButton! @IBOutlet weak var imageButtonWrapper: NSView! @@ -139,6 +109,7 @@ final class AddressBarButtonsViewController: NSViewController { private var tabCollectionViewModel: TabCollectionViewModel private var tabViewModel: TabViewModel? + private let popovers: NavigationBarPopovers private var bookmarkManager: BookmarkManager = LocalBookmarkManager.shared var controllerMode: AddressBarViewController.Mode? { @@ -167,13 +138,11 @@ final class AddressBarButtonsViewController: NSViewController { private var selectedTabViewModelCancellable: AnyCancellable? private var urlCancellable: AnyCancellable? private var bookmarkListCancellable: AnyCancellable? - private var privacyDashboadPendingUpdatesCancellable: AnyCancellable? private var effectiveAppearanceCancellable: AnyCancellable? private var permissionsCancellables = Set() private var trackerAnimationTriggerCancellable: AnyCancellable? private var privacyEntryPointIconUpdateCancellable: AnyCancellable? private var isMouseOverAnimationVisibleCancellable: AnyCancellable? - private var privacyInfoCancellable: AnyCancellable? private lazy var buttonsBadgeAnimator = NavigationBarBadgeAnimator() @@ -182,8 +151,10 @@ final class AddressBarButtonsViewController: NSViewController { } init?(coder: NSCoder, - tabCollectionViewModel: TabCollectionViewModel) { + tabCollectionViewModel: TabCollectionViewModel, + popovers: NavigationBarPopovers) { self.tabCollectionViewModel = tabCollectionViewModel + self.popovers = popovers super.init(coder: coder) } @@ -264,11 +235,7 @@ final class AddressBarButtonsViewController: NSViewController { } popupBlockedPopover?.close() - if privacyDashboardPopover.isShown { - closePrivacyDashboard() - } else { - openPrivacyDashboard() - } + popovers.togglePrivacyDashboardPopover(for: tabViewModel, from: privacyEntryPointButton) } private func updateBookmarkButtonVisibility() { @@ -284,7 +251,7 @@ final class AddressBarButtonsViewController: NSViewController { isUrlBookmarked = true } - return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) + return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || popovers.isEditBookmarkPopoverShown || isUrlBookmarked) } bookmarkButton.isShown = shouldShowBookmarkButton @@ -297,15 +264,11 @@ final class AddressBarButtonsViewController: NSViewController { return } - let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() - if !bookmarkPopover.isShown { - bookmarkButton.isShown = true - bookmarkPopover.isNew = result.isNew - bookmarkPopover.bookmark = bookmark - bookmarkPopover.show(positionedBelow: bookmarkButton) + if !popovers.isEditBookmarkPopoverShown { + popovers.showEditBookmarkPopover(with: bookmark, isNew: result.isNew, from: bookmarkButton, withDelegate: self) } else { updateBookmarkButtonVisibility() - bookmarkPopover.close() + popovers.closeEditBookmarkPopover() } } @@ -350,38 +313,12 @@ final class AddressBarButtonsViewController: NSViewController { } func closePrivacyDashboard() { - - // Prevent popover from being closed with Privacy Entry Point Button, while pending updates - guard let privacyDashboardViewController = privacyDashboardPopover.viewController, - privacyDashboardViewController.isPendingUpdates() == false else { - return - } - - privacyDashboardPopover.close() + popovers.closePrivacyDashboard() } func openPrivacyDashboard() { - guard let tabViewModel, - let privacyDashboardViewController = privacyDashboardPopover.viewController else { - return - } - - privacyDashboardViewController.updateTabViewModel(tabViewModel) - - let positioningViewInWindow = privacyDashboardPositioningView.convert(privacyDashboardPositioningView.bounds, to: view.window?.contentView) - privacyDashboardPopover.setPreferredMaxHeight(positioningViewInWindow.origin.y) - privacyDashboardPopover.delegate = self - privacyDashboardPopover.show(positionedBelow: privacyDashboardPositioningView) - - privacyEntryPointButton.state = .on - - privacyInfoCancellable = tabViewModel.tab.privacyInfoPublisher - .dropFirst() - .receive(on: DispatchQueue.main) - .sink { [weak privacyDashboardPopover, weak tabViewModel] _ in - guard privacyDashboardPopover?.isShown == true, let tabViewModel else { return } - privacyDashboardViewController.updateTabViewModel(tabViewModel) - } + guard let tabViewModel else { return } + popovers.openPrivacyDashboard(for: tabViewModel, from: privacyEntryPointButton) } func updateButtons() { @@ -661,32 +598,6 @@ final class AddressBarButtonsViewController: NSViewController { } } - private func subscribePrivacyDashboardPendingUpdates(privacyDashboardPopover: PrivacyDashboardPopover) { - privacyDashboadPendingUpdatesCancellable?.cancel() - guard NSApp.runType.requiresEnvironment else { return } - - guard let privacyDashboardViewController = privacyDashboardPopover.viewController else { - return - } - - privacyDashboadPendingUpdatesCancellable = privacyDashboardViewController.rulesUpdateObserver - .$pendingUpdates.dropFirst().receive(on: DispatchQueue.main).sink { [weak privacyDashboardPopover] _ in - let isPendingUpdate = privacyDashboardViewController.isPendingUpdates() - - // Prevent popover from being closed when clicking away, while pending updates - if isPendingUpdate { - privacyDashboardPopover?.behavior = .applicationDefined - } else { - privacyDashboardPopover?.close() -#if DEBUG - privacyDashboardPopover?.behavior = .semitransient -#else - privacyDashboardPopover?.behavior = .transient -#endif - } - } - } - private func updatePermissionButtons() { guard let tabViewModel else { return } @@ -997,46 +908,15 @@ extension AddressBarButtonsViewController: PermissionContextMenuDelegate { extension AddressBarButtonsViewController: NSPopoverDelegate { - func popoverShouldClose(_ popover: NSPopover) -> Bool { - switch popover { - case bookmarkPopover: - // fix popover reopening on next bookmarkButtonAction (on macOS 11) - DispatchQueue.main.async { [weak self] in - if let bookmarkPopover = self?.bookmarkPopover, bookmarkPopover.isShown { - bookmarkPopover.close() - } - } - return false - - default: - return true - } - } - - func popoverWillClose(_ notification: Notification) { - switch notification.object as? NSPopover { - case bookmarkPopover: - bookmarkPopover?.popoverWillClose() - - default: - break - } - } - func popoverDidClose(_ notification: Notification) { switch notification.object as? NSPopover { - case bookmarkPopover: - if bookmarkPopover?.isNew == true { + case popovers.bookmarkPopover: + if popovers.bookmarkPopover?.isNew == true { NotificationCenter.default.post(name: .bookmarkPromptShouldShow, object: nil) } updateBookmarkButtonVisibility() - bookmarkPopover = nil - case privacyDashboardPopover: - privacyEntryPointButton.state = .off - _privacyDashboardPopover = nil - default: - break + default: break } } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 83f7dd9360..51c3ca308a 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -33,6 +33,7 @@ final class AddressBarViewController: NSViewController { @IBOutlet var activeTextFieldMinXConstraint: NSLayoutConstraint! private static let defaultActiveTextFieldMinX: CGFloat = 40 + private let popovers: NavigationBarPopovers private(set) var addressBarButtonsViewController: AddressBarButtonsViewController? private let tabCollectionViewModel: TabCollectionViewModel @@ -81,8 +82,9 @@ final class AddressBarViewController: NSViewController { fatalError("AddressBarViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool) { + init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, popovers: NavigationBarPopovers) { self.tabCollectionViewModel = tabCollectionViewModel + self.popovers = popovers self.suggestionContainerViewModel = SuggestionContainerViewModel( isHomePage: tabViewModel?.tab.content == .newtab, isBurner: isBurner, @@ -174,7 +176,7 @@ final class AddressBarViewController: NSViewController { } @IBSegueAction func createAddressBarButtonsViewController(_ coder: NSCoder) -> AddressBarButtonsViewController? { - let controller = AddressBarButtonsViewController(coder: coder, tabCollectionViewModel: tabCollectionViewModel) + let controller = AddressBarButtonsViewController(coder: coder, tabCollectionViewModel: tabCollectionViewModel, popovers: popovers) self.addressBarButtonsViewController = controller controller?.delegate = self @@ -463,7 +465,7 @@ extension AddressBarViewController { func mouseDown(with event: NSEvent) -> NSEvent? { self.clickPoint = nil - guard event.window === self.view.window else { return event } + guard let window = self.view.window, event.window === window else { return event } if let point = self.view.mouseLocationInsideBounds(event.locationInWindow) { guard self.view.window?.firstResponder !== addressBarTextField.currentEditor(), @@ -483,8 +485,8 @@ extension AddressBarViewController { } } - } else if self.view.window?.isMainWindow == true { - self.clickPoint = event.locationInWindow + } else if window.isMainWindow { + self.clickPoint = window.convertPoint(toScreen: event.locationInWindow) } return event } @@ -493,10 +495,10 @@ extension AddressBarViewController { func mouseUp(with event: NSEvent) -> NSEvent? { // click (same position down+up) outside of the field: resign first responder - guard event.window === self.view.window, - self.view.window?.firstResponder === addressBarTextField.currentEditor(), + guard let window = self.view.window, event.window === window, + window.firstResponder === addressBarTextField.currentEditor(), let clickPoint, - clickPoint.distance(to: event.locationInWindow) <= Self.maxClickReleaseDistanceToResignFirstResponder else { + clickPoint.distance(to: window.convertPoint(toScreen: event.locationInWindow)) <= Self.maxClickReleaseDistanceToResignFirstResponder else { return event } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index a656539f29..ad0980e11b 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -1,7 +1,7 @@ - + @@ -474,7 +474,7 @@ - + @@ -575,13 +575,6 @@ - - - - - - - @@ -845,14 +838,12 @@ - - @@ -872,7 +863,6 @@ - diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift index 763cf56628..8480ad9298 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift @@ -32,10 +32,10 @@ protocol NetPPopoverManager: AnyObject { var ipcClient: NetworkProtectionIPCClient { get } var isShown: Bool { get } - func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) + func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover func close() - func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) + func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover? } extension PopoverPresenter { @@ -45,7 +45,7 @@ extension PopoverPresenter { } } -final class NavigationBarPopovers: PopoverPresenter { +final class NavigationBarPopovers: NSObject, PopoverPresenter { enum Constants { static let downloadsPopoverAutoHidingInterval: TimeInterval = 10 @@ -58,8 +58,17 @@ final class NavigationBarPopovers: PopoverPresenter { private(set) var autofillPopoverPresenter: AutofillPopoverPresenter private(set) var downloadsPopover: DownloadsPopover? + private var privacyDashboardPopover: PrivacyDashboardPopover? + private var privacyInfoCancellable: AnyCancellable? + private var privacyDashboadPendingUpdatesCancellable: AnyCancellable? + + private(set) var bookmarkPopover: AddBookmarkPopover? + private weak var bookmarkPopoverDelegate: NSPopoverDelegate? + private let networkProtectionPopoverManager: NetPPopoverManager + private var popoverIsShownCancellables = Set() + init(networkProtectionPopoverManager: NetPPopoverManager, autofillPopoverPresenter: AutofillPopoverPresenter) { self.networkProtectionPopoverManager = networkProtectionPopoverManager self.autofillPopoverPresenter = autofillPopoverPresenter @@ -96,46 +105,58 @@ final class NavigationBarPopovers: PopoverPresenter { bookmarkListPopover?.isShown ?? false } - func bookmarksButtonPressed(anchorView: NSView, popoverDelegate delegate: NSPopoverDelegate, tab: Tab?) { + var isEditBookmarkPopoverShown: Bool { + bookmarkPopover?.isShown ?? false + } + + func bookmarksButtonPressed(_ button: MouseOverButton, popoverDelegate delegate: NSPopoverDelegate, tab: Tab?) { if bookmarkListPopoverShown { bookmarkListPopover?.close() } else { - showBookmarkListPopover(usingView: anchorView, withDelegate: delegate, forTab: tab) + showBookmarkListPopover(from: button, withDelegate: delegate, forTab: tab) } } - func passwordManagementButtonPressed(usingView view: NSView, withDelegate delegate: NSPopoverDelegate) { - if autofillPopoverPresenter.popoverIsShown == true && view.window == autofillPopoverPresenter.popoverPresentingWindow { + func passwordManagementButtonPressed(_ button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) { + if autofillPopoverPresenter.popoverIsShown == true && button.window == autofillPopoverPresenter.popoverPresentingWindow { autofillPopoverPresenter.dismiss() } else { - showPasswordManagementPopover(selectedCategory: nil, usingView: view, withDelegate: delegate) + showPasswordManagementPopover(selectedCategory: nil, from: button, withDelegate: delegate) } } - func toggleNetworkProtectionPopover(usingView view: NSView, withDelegate delegate: NSPopoverDelegate) { - networkProtectionPopoverManager.toggle(positionedBelow: view, withDelegate: delegate) + func toggleNetworkProtectionPopover(from button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) { + if let popover = networkProtectionPopoverManager.toggle(positionedBelow: button, withDelegate: delegate) { + bindIsMouseDownState(of: button, to: popover) + } } - func toggleDownloadsPopover(usingView view: NSView, popoverDelegate: NSPopoverDelegate, downloadsDelegate: DownloadsViewControllerDelegate) { - + func toggleDownloadsPopover(from button: MouseOverButton, popoverDelegate: NSPopoverDelegate, downloadsDelegate: DownloadsViewControllerDelegate) { if downloadsPopover?.isShown ?? false { downloadsPopover?.close() return } guard closeTransientPopovers(), - view.window != nil - else { return } + button.window != nil else { return } let popover = DownloadsPopover() popover.delegate = popoverDelegate popover.viewController.delegate = downloadsDelegate downloadsPopover = popover - show(popover, positionedBelow: view) + show(popover, positionedBelow: button) + } + + func togglePrivacyDashboardPopover(for tabViewModel: TabViewModel?, from button: MouseOverButton) { + if privacyDashboardPopover?.isShown == true { + closePrivacyDashboard() + } else if let tabViewModel { + openPrivacyDashboard(for: tabViewModel, from: button) + } } private var downloadsPopoverTimer: Timer? - func showDownloadsPopoverAndAutoHide(usingView view: NSView, popoverDelegate: NSPopoverDelegate, downloadsDelegate: DownloadsViewControllerDelegate) { + func showDownloadsPopoverAndAutoHide(from button: MouseOverButton, popoverDelegate: NSPopoverDelegate, downloadsDelegate: DownloadsViewControllerDelegate) { let timerBlock: (Timer) -> Void = { [weak self] _ in self?.downloadsPopoverTimer?.invalidate() self?.downloadsPopoverTimer = nil @@ -146,7 +167,7 @@ final class NavigationBarPopovers: PopoverPresenter { } if !isDownloadsPopoverShown { - self.toggleDownloadsPopover(usingView: view, popoverDelegate: popoverDelegate, downloadsDelegate: downloadsDelegate) + self.toggleDownloadsPopover(from: button, popoverDelegate: popoverDelegate, downloadsDelegate: downloadsDelegate) downloadsPopoverTimer = Timer.scheduledTimer(withTimeInterval: Constants.downloadsPopoverAutoHidingInterval, repeats: false, @@ -175,10 +196,18 @@ final class NavigationBarPopovers: PopoverPresenter { networkProtectionPopoverManager.close() } + if bookmarkPopover?.isShown ?? false { + bookmarkPopover?.close() + } + + if privacyDashboardPopover?.isShown ?? false { + privacyDashboardPopover?.close() + } + return true } - func showBookmarkListPopover(usingView view: NSView, withDelegate delegate: NSPopoverDelegate, forTab tab: Tab?) { + func showBookmarkListPopover(from button: MouseOverButton, withDelegate delegate: NSPopoverDelegate, forTab tab: Tab?) { guard closeTransientPopovers() else { return } let popover = bookmarkListPopover ?? BookmarkListPopover() @@ -190,17 +219,91 @@ final class NavigationBarPopovers: PopoverPresenter { } LocalBookmarkManager.shared.requestSync() - show(popover, positionedBelow: view) + show(popover, positionedBelow: button) + } + + func showEditBookmarkPopover(with bookmark: Bookmark, isNew: Bool, from button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) { + guard closeTransientPopovers() else { return } + + let bookmarkPopover = AddBookmarkPopover() + bookmarkPopover.delegate = self + bookmarkPopover.isNew = isNew + bookmarkPopover.bookmark = bookmark + self.bookmarkPopover = bookmarkPopover + self.bookmarkPopoverDelegate = delegate + show(bookmarkPopover, positionedBelow: button) + } + + func closeEditBookmarkPopover() { + bookmarkPopover?.close() + } + + func openPrivacyDashboard(for tabViewModel: TabViewModel, from button: MouseOverButton) { + guard closeTransientPopovers() else { return } + + let popover = PrivacyDashboardPopover() + popover.delegate = self + self.privacyDashboardPopover = popover + self.subscribePrivacyDashboardPendingUpdates(for: popover) + + popover.viewController.updateTabViewModel(tabViewModel) + + let positioningRectInWindow = button.convert(button.bounds, to: button.window?.contentView) + popover.setPreferredMaxHeight(positioningRectInWindow.origin.y) + + show(popover, positionedBelow: button) + bindIsMouseDownState(of: button, to: popover) + + privacyInfoCancellable = tabViewModel.tab.privacyInfoPublisher + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak popover, weak tabViewModel] _ in + guard let popover, popover.isShown, let tabViewModel else { return } + popover.viewController.updateTabViewModel(tabViewModel) + } + } + + private func subscribePrivacyDashboardPendingUpdates(for privacyDashboardPopover: PrivacyDashboardPopover) { + privacyDashboadPendingUpdatesCancellable?.cancel() + guard NSApp.runType.requiresEnvironment else { return } + let privacyDashboardViewController = privacyDashboardPopover.viewController + + privacyDashboadPendingUpdatesCancellable = privacyDashboardViewController.rulesUpdateObserver + .$pendingUpdates.dropFirst().receive(on: DispatchQueue.main).sink { [weak privacyDashboardPopover] _ in + let isPendingUpdate = privacyDashboardViewController.isPendingUpdates() + + // Prevent popover from being closed when clicking away, while pending updates + if isPendingUpdate { + privacyDashboardPopover?.behavior = .applicationDefined + } else { + privacyDashboardPopover?.close() +#if DEBUG + privacyDashboardPopover?.behavior = .semitransient +#else + privacyDashboardPopover?.behavior = .transient +#endif + } + } + } + + func closePrivacyDashboard() { + // Prevent popover from being closed with Privacy Entry Point Button, while pending updates + guard let popover = privacyDashboardPopover, + !popover.viewController.isPendingUpdates() else { return } + + popover.close() } - func showPasswordManagementPopover(selectedCategory: SecureVaultSorting.Category?, usingView view: NSView, withDelegate delegate: NSPopoverDelegate) { + func showPasswordManagementPopover(selectedCategory: SecureVaultSorting.Category?, from button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) { guard closeTransientPopovers() else { return } - autofillPopoverPresenter.show(positionedBelow: view, withDomain: passwordManagementDomain, selectedCategory: selectedCategory) + let popover = autofillPopoverPresenter.show(positionedBelow: button, withDomain: passwordManagementDomain, selectedCategory: selectedCategory) + bindIsMouseDownState(of: button, to: popover) } - func showPasswordManagerPopover(selectedWebsiteAccount: SecureVaultModels.WebsiteAccount, usingView view: NSView, withDelegate delegate: NSPopoverDelegate) { - autofillPopoverPresenter.show(positionedBelow: view, withSelectedAccount: selectedWebsiteAccount) + func showPasswordManagerPopover(selectedWebsiteAccount: SecureVaultModels.WebsiteAccount, from button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) { + let popover = autofillPopoverPresenter.show(positionedBelow: button, withSelectedAccount: selectedWebsiteAccount) + bindIsMouseDownState(of: button, to: popover) } func hasAnySavePopoversVisible() -> Bool { @@ -269,19 +372,86 @@ final class NavigationBarPopovers: PopoverPresenter { show(popover, positionedBelow: view) } - func show(_ popover: NSPopover, positionedBelow view: NSView) { - view.isHidden = false + func show(_ popover: NSPopover, positionedBelow button: MouseOverButton) { + button.isHidden = false - popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) + popover.show(positionedBelow: button.bounds.insetFromLineOfDeath(flipped: button.isFlipped), in: button) + bindIsMouseDownState(of: button, to: popover) + } + + // keep button.isMouseDown ON while the popover is shown + func bindIsMouseDownState(of button: MouseOverButton, to popover: NSPopover) { + popoverIsShownCancellables.removeAll() + + button.publisher(for: \.isMouseDown).sink { [weak button, weak popover] isMouseDown in + guard let button, let popover else { return } + if !isMouseDown && popover.isShown { + button.isMouseDown = true + } + }.store(in: &popoverIsShownCancellables) + + popover.publisher(for: \.isShown).sink { [weak button] isShown in + guard let button else { return } + if isShown { + button.isMouseDown = true + } else { + button.isMouseDown = false + } + }.store(in: &popoverIsShownCancellables) } // MARK: - VPN - func showNetworkProtectionPopover( - positionedBelow view: NSView, - withDelegate delegate: NSPopoverDelegate) { - networkProtectionPopoverManager.show(positionedBelow: view, withDelegate: delegate) + func showNetworkProtectionPopover(positionedBelow button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) { + let popover = networkProtectionPopoverManager.show(positionedBelow: button, withDelegate: delegate) + bindIsMouseDownState(of: button, to: popover) + } +} + +extension NavigationBarPopovers: NSPopoverDelegate { + + func popoverShouldClose(_ popover: NSPopover) -> Bool { + switch popover { + case bookmarkPopover: + // fix popover reopening on next bookmarkButtonAction (on macOS 11) + DispatchQueue.main.async { [weak self] in + if let bookmarkPopover = self?.bookmarkPopover, bookmarkPopover.isShown { + bookmarkPopover.close() + } + } + return false + + default: + return true + } + } + + func popoverWillClose(_ notification: Notification) { + switch notification.object as? NSPopover { + case bookmarkPopover: + bookmarkPopoverDelegate?.popoverWillClose?(notification) + bookmarkPopover?.popoverWillClose() + + default: + break + } + } + + func popoverDidClose(_ notification: Notification) { + switch notification.object as? NSPopover { + case bookmarkPopover: + bookmarkPopoverDelegate?.popoverDidClose?(notification) + bookmarkPopover = nil + + case privacyDashboardPopover: + privacyDashboardPopover = nil + privacyInfoCancellable = nil + privacyDashboadPendingUpdatesCancellable = nil + + default: break + } } + } extension Notification.Name { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 6b81061e5e..4e438ad79f 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -198,7 +198,8 @@ final class NavigationBarViewController: NSViewController { @IBSegueAction func createAddressBarViewController(_ coder: NSCoder) -> AddressBarViewController? { guard let addressBarViewController = AddressBarViewController(coder: coder, tabCollectionViewModel: tabCollectionViewModel, - isBurner: isBurner) else { + isBurner: isBurner, + popovers: popovers) else { fatalError("NavigationBarViewController: Failed to init AddressBarViewController") } @@ -275,13 +276,11 @@ final class NavigationBarViewController: NSViewController { } @IBAction func bookmarksButtonAction(_ sender: NSButton) { - popovers.bookmarksButtonPressed(anchorView: bookmarkListButton, - popoverDelegate: self, - tab: tabCollectionViewModel.selectedTabViewModel?.tab) + popovers.bookmarksButtonPressed(bookmarkListButton, popoverDelegate: self, tab: tabCollectionViewModel.selectedTabViewModel?.tab) } @IBAction func passwordManagementButtonAction(_ sender: NSButton) { - popovers.passwordManagementButtonPressed(usingView: passwordManagementButton, withDelegate: self) + popovers.passwordManagementButtonPressed(passwordManagementButton, withDelegate: self) } @IBAction func networkProtectionButtonAction(_ sender: NSButton) { @@ -294,7 +293,7 @@ final class NavigationBarViewController: NSViewController { return } - popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) + popovers.toggleNetworkProtectionPopover(from: networkProtectionButton, withDelegate: networkProtectionButtonModel) } @IBAction func downloadsButtonAction(_ sender: NSButton) { @@ -466,19 +465,15 @@ final class NavigationBarViewController: NSViewController { setDownloadButtonHidingTimer() } - popovers.toggleDownloadsPopover(usingView: downloadsButton, popoverDelegate: self, downloadsDelegate: self) + popovers.toggleDownloadsPopover(from: downloadsButton, popoverDelegate: self, downloadsDelegate: self) } func showPasswordManagerPopover(selectedCategory: SecureVaultSorting.Category?) { - popovers.showPasswordManagementPopover(selectedCategory: selectedCategory, - usingView: passwordManagementButton, - withDelegate: self) + popovers.showPasswordManagementPopover(selectedCategory: selectedCategory, from: passwordManagementButton, withDelegate: self) } func showPasswordManagerPopover(selectedWebsiteAccount: SecureVaultModels.WebsiteAccount) { - popovers.showPasswordManagerPopover(selectedWebsiteAccount: selectedWebsiteAccount, - usingView: passwordManagementButton, - withDelegate: self) + popovers.showPasswordManagerPopover(selectedWebsiteAccount: selectedWebsiteAccount, from: passwordManagementButton, withDelegate: self) } private func setupNavigationButtonMenus() { @@ -630,7 +625,7 @@ final class NavigationBarViewController: NSViewController { !update.item.isBurner, WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window { - self.popovers.showDownloadsPopoverAndAutoHide(usingView: downloadsButton, + self.popovers.showDownloadsPopoverAndAutoHide(from: downloadsButton, popoverDelegate: self, downloadsDelegate: self) } else if update.item.isBurner { @@ -689,7 +684,7 @@ final class NavigationBarViewController: NSViewController { if LocalPinningManager.shared.isPinned(.autofill) { passwordManagementButton.isHidden = false } else { - passwordManagementButton.isHidden = !popovers.isPasswordManagementPopoverShown && !isAutoFillAutosaveMessageVisible + passwordManagementButton.isShown = popovers.isPasswordManagementPopoverShown || isAutoFillAutosaveMessageVisible } popovers.passwordManagementDomain = nil @@ -730,15 +725,15 @@ final class NavigationBarViewController: NSViewController { } private func updateDownloadsButton(updatingFromPinnedViewsNotification: Bool = false) { - let menu = NSMenu() - let title = LocalPinningManager.shared.shortcutTitle(for: .downloads) - menu.addItem(withTitle: title, action: #selector(toggleDownloadsPanelPinning(_:)), keyEquivalent: "") - - downloadsButton.menu = menu + downloadsButton.menu = NSMenu { + NSMenuItem(title: LocalPinningManager.shared.shortcutTitle(for: .downloads), + action: #selector(toggleDownloadsPanelPinning(_:)), + keyEquivalent: "") + } downloadsButton.toolTip = UserText.downloadsShortcutTooltip if LocalPinningManager.shared.isPinned(.downloads) { - downloadsButton.isHidden = false + downloadsButton.isShown = true return } @@ -746,21 +741,22 @@ final class NavigationBarViewController: NSViewController { downloadsButton.image = hasActiveDownloads ? .downloadsActive : .downloads let isTimerActive = downloadsButtonHidingTimer != nil - if popovers.isDownloadsPopoverShown { - downloadsButton.isHidden = false + downloadsButton.isShown = if popovers.isDownloadsPopoverShown { + true } else { - downloadsButton.isHidden = !(hasActiveDownloads || isTimerActive) + hasActiveDownloads || isTimerActive } - if !downloadsButton.isHidden { setDownloadButtonHidingTimer() } - downloadsButton.isMouseDown = popovers.isDownloadsPopoverShown + if downloadsButton.isShown { + setDownloadButtonHidingTimer() + } // If the user has selected Hide Downloads from the navigation bar context menu, and no downloads are active, then force it to be hidden // even if the timer is active. if updatingFromPinnedViewsNotification { if !LocalPinningManager.shared.isPinned(.downloads) { invalidateDownloadButtonHidingTimer() - downloadsButton.isHidden = !hasActiveDownloads + downloadsButton.isShown = hasActiveDownloads } } } @@ -991,9 +987,7 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { - popovers.showBookmarkListPopover(usingView: bookmarkListButton, - withDelegate: self, - forTab: tabCollectionViewModel.selectedTabViewModel?.tab) + popovers.showBookmarkListPopover(from: bookmarkListButton, withDelegate: self, forTab: tabCollectionViewModel.selectedTabViewModel?.tab) } func optionsButtonMenuRequestedBookmarkManagementInterface(_ menu: NSMenu) { @@ -1009,9 +1003,7 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } func optionsButtonMenuRequestedLoginsPopover(_ menu: NSMenu, selectedCategory: SecureVaultSorting.Category) { - popovers.showPasswordManagementPopover(selectedCategory: selectedCategory, - usingView: passwordManagementButton, - withDelegate: self) + popovers.showPasswordManagementPopover(selectedCategory: selectedCategory, from: passwordManagementButton, withDelegate: self) } func optionsButtonMenuRequestedNetworkProtectionPopover(_ menu: NSMenu) { diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index 2e4fdacb8d..033f795005 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -27,8 +27,12 @@ final class NetPPopoverManagerMock: NetPPopoverManager { var isShown: Bool { false } var ipcClient: NetworkProtectionIPCClient = IPCClientMock() - func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) {} - func show(positionedBelow view: NSView, withDelegate delegate: any NSPopoverDelegate) {} + func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover? { + return nil + } + func show(positionedBelow view: NSView, withDelegate delegate: any NSPopoverDelegate) -> NSPopover { + return NSPopover() + } func close() {} } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 14e266a11c..f16d3940bf 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -58,7 +58,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { } // swiftlint:disable:next function_body_length - func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { + func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover { let popover = { let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) @@ -125,6 +125,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { }() show(popover, positionedBelow: view) + return popover } private func show(_ popover: NSPopover, positionedBelow view: NSView) { @@ -133,12 +134,14 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) } - func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { + func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover? { if let networkProtectionPopover, networkProtectionPopover.isShown { networkProtectionPopover.close() self.networkProtectionPopover = nil + + return nil } else { - show(positionedBelow: view, withDelegate: delegate) + return show(positionedBelow: view, withDelegate: delegate) } } diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboard.storyboard b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboard.storyboard deleted file mode 100644 index 0c96c470e3..0000000000 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboard.storyboard +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift index 5566778ae2..346d23e39f 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift @@ -31,8 +31,8 @@ final class PrivacyDashboardPopover: NSPopover { return frame } - var viewController: PrivacyDashboardViewController? { - contentViewController as? PrivacyDashboardViewController + var viewController: PrivacyDashboardViewController { + (contentViewController as? PrivacyDashboardViewController)! } override init() { @@ -46,21 +46,17 @@ final class PrivacyDashboardPopover: NSPopover { } required init?(coder: NSCoder) { - fatalError("PrivacyDashboardPopover: Bad initializer") + fatalError("\(Self.self): Bad initializer") } private func setupContentController() { - let storyboard = NSStoryboard(name: "PrivacyDashboard", bundle: nil) - guard let controller = storyboard.instantiateController(withIdentifier: "PrivacyDashboardViewController") as? PrivacyDashboardViewController else { - assertionFailure("PrivacyDashboard is missing PrivacyDashboardViewController") - return - } + let controller = PrivacyDashboardViewController() controller.sizeDelegate = self contentViewController = controller } func setPreferredMaxHeight(_ height: CGFloat) { - viewController?.setPreferredMaxHeight(height - 40) // Account for popover arrow height + viewController.setPreferredMaxHeight(height - 40) // Account for popover arrow height } override func show(relativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge) { diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index 059652246d..f29cee0cdc 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -77,23 +77,18 @@ final class PrivacyDashboardViewController: NSViewController { var sizeDelegate: PrivacyDashboardViewControllerSizeDelegate? private weak var tabViewModel: TabViewModel? - required init?(coder: NSCoder, - privacyInfo: PrivacyInfo?, - dashboardMode: PrivacyDashboardMode, - privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager) { + init(privacyInfo: PrivacyInfo? = nil, + dashboardMode: PrivacyDashboardMode = .dashboard, + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager) { self.privacyDashboardController = PrivacyDashboardController(privacyInfo: privacyInfo, dashboardMode: dashboardMode, privacyConfigurationManager: privacyConfigurationManager, eventMapping: toggleReportEvents) - super.init(coder: coder) + super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { - self.privacyDashboardController = PrivacyDashboardController(privacyInfo: nil, - dashboardMode: .dashboard, - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - eventMapping: toggleReportEvents) - super.init(coder: coder) + fatalError("\(Self.self): Bad initializer") } public func updateTabViewModel(_ tabViewModel: TabViewModel) { @@ -107,9 +102,14 @@ final class PrivacyDashboardViewController: NSViewController { } } + override func loadView() { + view = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 489)) + initWebView() + } + public override func viewDidLoad() { super.viewDidLoad() - initWebView() + privacyDashboardController.setup(for: webView) privacyDashboardController.privacyDashboardNavigationDelegate = self privacyDashboardController.privacyDashboardDelegate = self diff --git a/DuckDuckGo/SecureVault/View/AutofillPopoverPresenter.swift b/DuckDuckGo/SecureVault/View/AutofillPopoverPresenter.swift index 871641ebb0..66308a7450 100644 --- a/DuckDuckGo/SecureVault/View/AutofillPopoverPresenter.swift +++ b/DuckDuckGo/SecureVault/View/AutofillPopoverPresenter.swift @@ -24,8 +24,8 @@ protocol AutofillPopoverPresenter { var popoverIsDirty: Bool { get } var popoverIsShown: Bool { get } var popoverPresentingWindow: NSWindow? { get } - func show(positionedBelow view: NSView, withDomain domain: String?, selectedCategory category: SecureVaultSorting.Category?) - func show(positionedBelow view: NSView, withSelectedAccount: SecureVaultModels.WebsiteAccount) + func show(positionedBelow view: NSView, withDomain domain: String?, selectedCategory category: SecureVaultSorting.Category?) -> NSPopover + func show(positionedBelow view: NSView, withSelectedAccount: SecureVaultModels.WebsiteAccount) -> NSPopover func dismiss() } @@ -55,12 +55,16 @@ final class DefaultAutofillPopoverPresenter: AutofillPopoverPresenter, PopoverPr } /// Note: Dismisses any previously displayed popover before showing a new one - func show(positionedBelow view: NSView, withDomain domain: String?, selectedCategory category: SecureVaultSorting.Category?) { - show(under: view, withDomain: domain).select(category: category) + func show(positionedBelow view: NSView, withDomain domain: String?, selectedCategory category: SecureVaultSorting.Category?) -> NSPopover { + let popover = show(under: view, withDomain: domain) + popover.select(category: category) + return popover } - func show(positionedBelow view: NSView, withSelectedAccount account: SecureVaultModels.WebsiteAccount) { - show(under: view, withDomain: nil).select(websiteAccount: account) + func show(positionedBelow view: NSView, withSelectedAccount account: SecureVaultModels.WebsiteAccount) -> NSPopover { + let popover = show(under: view, withDomain: nil) + popover.select(websiteAccount: account) + return popover } func dismiss() { diff --git a/UnitTests/NavigationBar/Mocks/MockAutofillPopoverPresenter.swift b/UnitTests/NavigationBar/Mocks/MockAutofillPopoverPresenter.swift index c25d96950e..682b6e8971 100644 --- a/UnitTests/NavigationBar/Mocks/MockAutofillPopoverPresenter.swift +++ b/UnitTests/NavigationBar/Mocks/MockAutofillPopoverPresenter.swift @@ -39,12 +39,14 @@ final class MockAutofillPopoverPresenter: AutofillPopoverPresenter { var popoverPresentingWindow: NSWindow? - func show(positionedBelow view: NSView, withDomain domain: String?, selectedCategory category: DuckDuckGo_Privacy_Browser.SecureVaultSorting.Category?) { + func show(positionedBelow view: NSView, withDomain domain: String?, selectedCategory category: DuckDuckGo_Privacy_Browser.SecureVaultSorting.Category?) -> NSPopover { didShowWithCategory = true + return NSPopover() } - func show(positionedBelow view: NSView, withSelectedAccount: BrowserServicesKit.SecureVaultModels.WebsiteAccount) { + func show(positionedBelow view: NSView, withSelectedAccount: BrowserServicesKit.SecureVaultModels.WebsiteAccount) -> NSPopover { didShowWithSelectedAccount = true + return NSPopover() } func dismiss() { diff --git a/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift b/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift index 091f73f4d9..5a0943ce29 100644 --- a/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift +++ b/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift @@ -79,7 +79,7 @@ final class NavigationBarPopoversTests: XCTestCase { func testShowsPasswordPopoverWithCategory() throws { // When - sut.showPasswordManagementPopover(selectedCategory: nil, usingView: NSView(), withDelegate: MockNSPopoverDelegate()) + sut.showPasswordManagementPopover(selectedCategory: nil, from: MouseOverButton(), withDelegate: MockNSPopoverDelegate()) // Then XCTAssertTrue(autofillPopoverPresenter.didShowWithCategory) @@ -90,7 +90,7 @@ final class NavigationBarPopoversTests: XCTestCase { let account = SecureVaultModels.WebsiteAccount(id: "") // When - sut.showPasswordManagerPopover(selectedWebsiteAccount: account, usingView: NSView(), withDelegate: MockNSPopoverDelegate()) + sut.showPasswordManagerPopover(selectedWebsiteAccount: account, from: MouseOverButton(), withDelegate: MockNSPopoverDelegate()) // Then XCTAssertTrue(autofillPopoverPresenter.didShowWithSelectedAccount) From 26d379a3c9aa301df3283c6824f06238ec570316 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 14 May 2024 20:18:31 +0600 Subject: [PATCH 106/134] Fix Mission Control detection; Cleanup WKFullScreenWindowController hack (#2768) Task/Issue URL: https://app.asana.com/0/1201037661562251/1207278354887235/f --- .../Extensions/NSWorkspaceExtension.swift | 44 +++++++++---- DuckDuckGo/Tab/Model/Tab.swift | 24 +------ .../Tab/View/WebViewContainerView.swift | 64 ++++++++----------- 3 files changed, 60 insertions(+), 72 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift b/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift index ebfca54a36..15bc5f3a68 100644 --- a/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift @@ -29,33 +29,32 @@ extension NSWorkspace { return bundle.displayName } + /// Detect if macOS Mission Control (three-finger swipe up to show the Spaces) is currently active static func isMissionControlActive() -> Bool { guard let visibleWindows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, CGWindowID(0)) as? [[CFString: Any]] else { assertionFailure("CGWindowListCopyWindowInfo doesn‘t work anymore") return false } - let allScreenSizes = NSScreen.screens.map(\.frame.size) - // Here‘s the trick: normally the Dock App only displays full-screen overlay windows drawing the Dock. // When the Mission Control is activated, the Dock presents multiple window tiles for each visible window // so here we filter out all the screen-sized windows and if the resulting list is not empty it may // mean that Mission Control is active. - let missionControlWindows = visibleWindows.filter { window in - windowName(window) == "Dock" && !allScreenSizes.contains(windowSize(window)) + let dockAppWindows = visibleWindows.filter { window in + window.ownerName == "Dock" } - - func windowName(_ dict: [CFString: Any]) -> String? { - dict[kCGWindowOwnerName] as? String + // filter out wallpaper windows + var missionControlWindows = dockAppWindows.filter { window in + window.name?.hasPrefix("Wallpaper") != true } - func windowSize(_ dict: [CFString: Any]) -> NSSize { - guard let bounds = dict[kCGWindowBounds] as? [String: NSNumber], - let width = bounds["Width"]?.intValue, - let height = bounds["Height"]?.intValue else { return .zero } - return NSSize(width: width, height: height) + // filter out the Dock drawing windows + for screen in NSScreen.screens { + if let idx = missionControlWindows.firstIndex(where: { window in window.size == screen.frame.size }) { + missionControlWindows.remove(at: idx) + } } - return missionControlWindows.count > allScreenSizes.count + return missionControlWindows.count > 0 } @available(macOS, obsoleted: 14.0, message: "This needs to be removed as it‘s no longer necessary.") @@ -79,3 +78,22 @@ extension NSWorkspace.OpenConfiguration { } } + +private extension [CFString: Any] { + + var name: String? { + self[kCGWindowName] as? String + } + + var ownerName: String? { + self[kCGWindowOwnerName] as? String + } + + var size: NSSize { + guard let bounds = self[kCGWindowBounds] as? [String: NSNumber], + let width = bounds["Width"]?.intValue, + let height = bounds["Height"]?.intValue else { return .zero } + return NSSize(width: width, height: height) + } + +} diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 1fd8c17493..a6fce323b1 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -333,36 +333,18 @@ protocol NewWindowPolicyDecisionMaker { } deinit { - cleanUpBeforeClosing(onDeinit: true, webView: webView, userContentController: userContentController) - } - - func cleanUpBeforeClosing() { - cleanUpBeforeClosing(onDeinit: false, webView: webView, userContentController: userContentController) - } - - @MainActor(unsafe) - private func cleanUpBeforeClosing(onDeinit: Bool, webView: WebView, userContentController: UserContentController?) { - let job = { [webView, userContentController] in + DispatchQueue.main.asyncOrNow { [webView, userContentController] in + // WebKit objects must be deallocated on the main thread webView.stopAllMedia(shouldStopLoading: true) userContentController?.cleanUpBeforeClosing() + #if DEBUG if case .normal = NSApp.runType { webView.assertObjectDeallocated(after: 4.0) } #endif } -#if DEBUG - if !onDeinit, case .normal = NSApp.runType { - // Tab should be deallocated shortly after burning - self.assertObjectDeallocated(after: 4.0) - } -#endif - guard Thread.isMainThread else { - DispatchQueue.main.async { job() } - return - } - job() } func stopAllMediaAndLoading() { diff --git a/DuckDuckGo/Tab/View/WebViewContainerView.swift b/DuckDuckGo/Tab/View/WebViewContainerView.swift index e75c81c627..722144ed57 100644 --- a/DuckDuckGo/Tab/View/WebViewContainerView.swift +++ b/DuckDuckGo/Tab/View/WebViewContainerView.swift @@ -51,7 +51,6 @@ final class WebViewContainerView: NSView { } private var blurViewIsHiddenCancellable: AnyCancellable? - private var fullScreenWindowWillCloseCancellable: AnyCancellable? private var cancellables = Set() override func didAddSubview(_ subview: NSView) { @@ -117,48 +116,37 @@ final class WebViewContainerView: NSView { .store(in: &cancellables) } - // fix a glitch scaling down Full Screen layer on next Full Screen activation - // after exiting Full Screen by dragging the window out in Mission Control - // (three-fingers-up swipe) - // see https://app.asana.com/0/1177771139624306/1204370242122745/f - private func observeFullScreenWindowWillExitFullScreen(_ fullScreenWindow: NSWindow) { - NotificationCenter.default.publisher(for: NSWindow.willExitFullScreenNotification, object: fullScreenWindow) - .sink { [weak self] _ in - guard let self else { return } - self.cancellables.removeAll() - - if NSWorkspace.isMissionControlActive() { - // closeAllMediaPresentations causes all Full Screen windows to be closed and removed from their WebViews - // (and reinstantiated the next time Full Screen is requested) - // this would slightly break UX in case multiple Full Screen windows are open but it fixes the bug - if #available(macOS 12.0, *) { - webView.closeAllMediaPresentations {} - } else { - webView.closeAllMediaPresentations() - } + /** - } - } - .store(in: &cancellables) + Fix a glitch breaking the Full Screen presentation on a repeated + Full Screen mode activation after dragging out of Mission Control Spaces. - // https://app.asana.com/0/72649045549333/1206959015087322/f - if #unavailable(macOS 14.4) { - fullScreenWindowWillCloseCancellable = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: fullScreenWindow) - .sink { [weak self] notification in - self?.fullScreenWindowWillCloseCancellable = nil - let fullScreenWindowController = (notification.object as? NSWindow)?.windowController - DispatchQueue.main.async { [weak fullScreenWindowController] in - guard let fullScreenWindowController else { return } - // just in case. - // if WKFullScreenWindowController receives `close()` the next time it‘s open it will crash because its _webView is nil - // https://errors.duckduckgo.com/organizations/ddg/issues/3411/?project=6&referrer=release-issue-stream - NSException.try { - fullScreenWindowController.setValue(NSView(), forKeyPath: #keyPath(webView)) - } + **Steps to reproduce:** + 1. Enter full screen video + 2. Open Mission Control (swipe three fingers up) + 3. Drag the full screen video out of the top panel in the Mission Control + 4. Enter full screen again - validate video opens in full screen + - The video would open in a shrinked (thumbnail) state without the fix + + - Note: The bug is actual for macOS 12 and above + + https://app.asana.com/0/1177771139624306/1204370242122745/f + */ + private func observeFullScreenWindowWillExitFullScreen(_ fullScreenWindow: NSWindow) { + if #available(macOS 12.0, *) { // works fine on Big Sur + NotificationCenter.default.publisher(for: NSWindow.willExitFullScreenNotification, object: fullScreenWindow) + .sink { [weak self] _ in + guard let self else { return } + self.cancellables.removeAll() + + if NSWorkspace.isMissionControlActive() { + // closeAllMediaPresentations causes all Full Screen windows to be closed and removed from their WebViews + // (and reinstantiated the next time Full Screen is requested) + webView.closeAllMediaPresentations {} } } + .store(in: &cancellables) } - } override func removeFromSuperview() { From 7633a8559e00a2fcf327bb0fb9b286777de935c5 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 14 May 2024 18:54:23 -0700 Subject: [PATCH 107/134] Update BSK for iOS RMF changes (#2772) Task/Issue URL: https://app.asana.com/0/414235014887631/1207234800675204/f Tech Design URL: CC: Description: Client PR for duckduckgo/BrowserServicesKit#809 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9eeaa77716..99de03bbf8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12836,7 +12836,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.6; + version = 145.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 c8835ebb9b..cfbe5f08f9 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" : "72be4e73360989af170399bc063fd5c628e1e84c", - "version" : "144.0.6" + "revision" : "6568d48c381042275c1936c6301a5100dea1c278", + "version" : "145.0.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "14b13d0c3db38f471ce4ba1ecb502ee1986c84d7", - "version" : "3.5.0" + "revision" : "25b8903191a40b21b09525085fe325ae3386092e", + "version" : "3.6.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5d9d7c8866..77c51492c6 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: "144.0.6"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 380fd19ea9..6bca821bc5 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.6"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index fa2374c63d..f8d1329f0f 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: "144.0.6"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From eff4f1fbd6cc59fc9a39184dd2e46bf9ae7a63c6 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 15 May 2024 08:50:16 +0200 Subject: [PATCH 108/134] Fire VPN uninstall pixels to track success rates and clean up VPN uninstallation code (#2742) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207231290677529/f **Description**: Cleans up VPN uninstallation code a bit, and fires pixels to track success metrics for the uninstallation procedure. --- DuckDuckGo.xcodeproj/project.pbxproj | 18 +- DuckDuckGo/Application/AppDelegate.swift | 12 +- DuckDuckGo/LoginItems/LoginItemsManager.swift | 3 + .../MainWindow/MainViewController.swift | 5 +- .../NetworkProtectionAppEvents.swift | 13 +- .../NetworkProtectionDebugMenu.swift | 13 +- .../NetworkProtectionDebugUtilities.swift | 16 +- ...etworkProtectionNavBarPopoverManager.swift | 8 +- ...rkProtectionSubscriptionEventHandler.swift | 14 +- .../Model/VPNPreferencesModel.swift | 2 +- .../NetworkProtectionFeatureDisabler.swift | 171 ------------ .../NetworkProtectionFeatureVisibility.swift | 24 +- DuckDuckGo/Waitlist/VPNUninstaller.swift | 258 ++++++++++++++++++ DuckDuckGoVPN/VPNUninstaller.swift | 1 - UnitTests/Menus/MoreOptionsMenuTests.swift | 5 +- .../VPNUninstallerTests.swift | 31 +++ 16 files changed, 366 insertions(+), 228 deletions(-) delete mode 100644 DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift create mode 100644 DuckDuckGo/Waitlist/VPNUninstaller.swift create mode 100644 UnitTests/NetworkProtection/VPNUninstallerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 99de03bbf8..c46cad6374 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1197,8 +1197,8 @@ 4B677433255DBEB800025BD8 /* httpsMobileV2Bloom.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B677428255DBEB800025BD8 /* httpsMobileV2Bloom.bin */; }; 4B677435255DBEB800025BD8 /* httpsMobileV2FalsePositives.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B67742A255DBEB800025BD8 /* httpsMobileV2FalsePositives.json */; }; 4B677442255DBEEA00025BD8 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.swift */; }; - 4B6785472AA8DE68008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */; }; - 4B6785482AA8DE69008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */; }; + 4B6785472AA8DE68008A5004 /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */; }; + 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */; }; 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; @@ -1501,6 +1501,8 @@ 7B37C7A52BAA32A50062546A /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 7B37C7A42BAA32A50062546A /* Subscription */; }; 7B430EA12A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */; }; + 7B4C5CF52BE51D640007A164 /* VPNUninstallerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */; }; + 7B4C5CF62BE51D640007A164 /* VPNUninstallerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */; }; 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; 7B4D8A222BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */; }; @@ -3105,7 +3107,7 @@ 4B67742A255DBEB800025BD8 /* httpsMobileV2FalsePositives.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = httpsMobileV2FalsePositives.json; sourceTree = ""; }; 4B677440255DBEEA00025BD8 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; 4B677454255DC18000025BD8 /* Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Bridging.h; sourceTree = ""; }; - 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureDisabler.swift; sourceTree = ""; }; + 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistThankYouPromptPresenter.swift; sourceTree = ""; }; 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "DuckDuckGo-ExampleCrash.ips"; sourceTree = ""; }; 4B70C00027B0793D000386ED /* CrashReportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportTests.swift; sourceTree = ""; }; @@ -3313,6 +3315,7 @@ 7B2E52242A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAgentNotificationsPresenter.swift; sourceTree = ""; }; 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarPopoverManager.swift; sourceTree = ""; }; 7B430EA02A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionSimulateFailureMenu.swift; sourceTree = ""; }; + 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstallerTests.swift; sourceTree = ""; }; 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 7B4CE8E626F02134009134B1 /* TabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarTests.swift; sourceTree = ""; }; 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; @@ -5425,7 +5428,7 @@ isa = PBXGroup; children = ( 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */, - 4B6785432AA8DE1F008A5004 /* NetworkProtectionFeatureDisabler.swift */, + 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */, 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */, 31F2D1FE2AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift */, 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */, @@ -5675,6 +5678,7 @@ 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, + 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -9950,7 +9954,7 @@ 3706FBE9293F65D500E42796 /* FireViewModel.swift in Sources */, B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, - 4B6785482AA8DE69008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, + 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, 3706FBED293F65D500E42796 /* TabCollection.swift in Sources */, B6C0BB6B29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, @@ -10291,6 +10295,7 @@ 3706FE0A293F661700E42796 /* UserAgentTests.swift in Sources */, 3706FE0B293F661700E42796 /* AVCaptureDeviceMock.swift in Sources */, 9FBD84572BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */, + 7B4C5CF62BE51D640007A164 /* VPNUninstallerTests.swift in Sources */, 3706FE0C293F661700E42796 /* GeolocationProviderMock.swift in Sources */, CBDD5DE429A6800300832877 /* MockConfigurationStore.swift in Sources */, 3706FE0D293F661700E42796 /* MainMenuTests.swift in Sources */, @@ -11124,7 +11129,7 @@ 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */, AAADFD06264AA282001555EA /* TimeIntervalExtension.swift in Sources */, - 4B6785472AA8DE68008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, + 4B6785472AA8DE68008A5004 /* VPNUninstaller.swift in Sources */, 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, @@ -11795,6 +11800,7 @@ 5681ED402BDB955100F59729 /* SyncCredentialsAdapterTests.swift in Sources */, 5681ED432BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */, 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */, + 7B4C5CF52BE51D640007A164 /* VPNUninstallerTests.swift in Sources */, 4B9DB05C2A983B55000927DB /* WaitlistViewModelTests.swift in Sources */, 1D1C36E329FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */, 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 0b2cc141b2..afbeaf6d23 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -37,6 +37,7 @@ import Lottie import NetworkProtection import Subscription +import NetworkProtectionIPC @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { @@ -86,7 +87,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { var privacyDashboardWindow: NSWindow? // Needs to be lazy as indirectly depends on AppDelegate - private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() + private lazy var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler = { + + let ipcClient = TunnelControllerIPCClient() + let tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) + let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) + + return NetworkProtectionSubscriptionEventHandler( + tunnelController: tunnelController, + vpnUninstaller: vpnUninstaller) + }() #if DBP private let dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler() diff --git a/DuckDuckGo/LoginItems/LoginItemsManager.swift b/DuckDuckGo/LoginItems/LoginItemsManager.swift index 076c75f2b8..4dd2f98fb5 100644 --- a/DuckDuckGo/LoginItems/LoginItemsManager.swift +++ b/DuckDuckGo/LoginItems/LoginItemsManager.swift @@ -22,7 +22,10 @@ import LoginItems import PixelKit protocol LoginItemsManaging { + func enableLoginItems(_ items: Set, log: OSLog) func throwingEnableLoginItems(_ items: Set, log: OSLog) throws + func disableLoginItems(_ items: Set) + func restartLoginItems(_ items: Set, log: OSLog) } /// Class to manage the login items for the VPN and DBP diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 118116eaad..113e525dca 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -72,8 +72,11 @@ final class MainViewController: NSViewController { let ipcClient = TunnelControllerIPCClient() ipcClient.register() + let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) - return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient, networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabler()) + return NetworkProtectionNavBarPopoverManager( + ipcClient: ipcClient, + vpnUninstaller: vpnUninstaller) }() let networkProtectionStatusReporter: NetworkProtectionStatusReporter = { var connectivityIssuesObserver: ConnectivityIssueObserver! diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index fcc9929d18..aaf0b2de0f 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -47,15 +47,15 @@ final class NetworkProtectionAppEvents { // MARK: - Feature Visibility private let featureVisibility: NetworkProtectionFeatureVisibility - private let featureDisabler: NetworkProtectionFeatureDisabling + private let uninstaller: VPNUninstalling private let defaults: UserDefaults init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), - featureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + uninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP) { self.defaults = defaults - self.featureDisabler = featureDisabler + self.uninstaller = uninstaller self.featureVisibility = featureVisibility } @@ -65,12 +65,7 @@ final class NetworkProtectionAppEvents { let loginItemsManager = LoginItemsManager() Task { @MainActor in - let disabled = await featureVisibility.disableIfUserHasNoAccess() - - guard !disabled else { - return - } - + await featureVisibility.disableIfUserHasNoAccess() restartNetworkProtectionIfVersionChanged(using: loginItemsManager) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 472b1f5275..0323060f84 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -181,7 +181,12 @@ final class NetworkProtectionDebugMenu: NSMenu { @objc func resetAllState(_ sender: Any?) { Task { @MainActor in guard case .alertFirstButtonReturn = await NSAlert.resetNetworkProtectionAlert().runModal() else { return } - await debugUtilities.resetAllState(keepAuthToken: false) + + do { + try await debugUtilities.resetAllState(keepAuthToken: false) + } catch { + os_log("Error in resetAllState: %{public}@", log: .networkProtection, error.localizedDescription) + } } } @@ -190,7 +195,11 @@ final class NetworkProtectionDebugMenu: NSMenu { @objc func resetAllKeepingInvite(_ sender: Any?) { Task { @MainActor in guard case .alertFirstButtonReturn = await NSAlert.resetNetworkProtectionAlert().runModal() else { return } - await debugUtilities.resetAllState(keepAuthToken: true) + do { + try await debugUtilities.resetAllState(keepAuthToken: true) + } catch { + os_log("Error in resetAllState: %{public}@", log: .networkProtection, error.localizedDescription) + } } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index 721f194c0d..a3b56f0970 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -30,7 +30,7 @@ import NetworkProtectionIPC final class NetworkProtectionDebugUtilities { private let ipcClient: TunnelControllerIPCClient - private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabler + private let vpnUninstaller: VPNUninstaller // MARK: - Login Items Management @@ -49,17 +49,13 @@ final class NetworkProtectionDebugUtilities { let ipcClient = TunnelControllerIPCClient() self.ipcClient = ipcClient - self.networkProtectionFeatureDisabler = NetworkProtectionFeatureDisabler(ipcClient: ipcClient) + self.vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) } // MARK: - Debug commands for the extension - func resetAllState(keepAuthToken: Bool) async { - let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) - - guard uninstalledSuccessfully else { - return - } + func resetAllState(keepAuthToken: Bool) async throws { + try await vpnUninstaller.uninstall(removeSystemExtension: true) settings.resetToDefaults() @@ -71,8 +67,8 @@ final class NetworkProtectionDebugUtilities { } func removeSystemExtensionAndAgents() async throws { - try await networkProtectionFeatureDisabler.removeSystemExtension() - networkProtectionFeatureDisabler.disableLoginItems() + try await vpnUninstaller.removeSystemExtension() + vpnUninstaller.disableLoginItems() } func sendTestNotificationRequest() async throws { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index f16d3940bf..82cd98ab95 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -45,12 +45,12 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { private var networkProtectionPopover: NetworkProtectionPopover? let ipcClient: NetworkProtectionIPCClient - let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling + let vpnUninstaller: VPNUninstalling init(ipcClient: TunnelControllerIPCClient, - networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling) { + vpnUninstaller: VPNUninstalling) { self.ipcClient = ipcClient - self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler + self.vpnUninstaller = vpnUninstaller } var isShown: Bool { @@ -116,7 +116,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { userDefaults: .netP, locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in - _ = await self?.networkProtectionFeatureDisabler.disable(uninstallSystemExtension: true) + _ = try? await self?.vpnUninstaller.uninstall(removeSystemExtension: true) }) popover.delegate = delegate diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 31795df3a3..39752c05bd 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -26,18 +26,22 @@ import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { private let accountManager: AccountManager + private let tunnelController: TunnelController private let networkProtectionTokenStorage: NetworkProtectionTokenStore - private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling + private let vpnUninstaller: VPNUninstalling private let userDefaults: UserDefaults private var cancellables = Set() init(accountManager: AccountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), + tunnelController: TunnelController, networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), - networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + vpnUninstaller: VPNUninstalling, userDefaults: UserDefaults = .netP) { + self.accountManager = accountManager + self.tunnelController = tunnelController self.networkProtectionTokenStorage = networkProtectionTokenStorage - self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler + self.vpnUninstaller = vpnUninstaller self.userDefaults = userDefaults subscribeToEntitlementChanges() @@ -84,7 +88,7 @@ final class NetworkProtectionSubscriptionEventHandler { if hasEntitlements { UserDefaults.netP.networkProtectionEntitlementsExpired = false } else { - networkProtectionFeatureDisabler.stop() + await tunnelController.stop() UserDefaults.netP.networkProtectionEntitlementsExpired = true } } @@ -106,7 +110,7 @@ final class NetworkProtectionSubscriptionEventHandler { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") Task { - await networkProtectionFeatureDisabler.disable(uninstallSystemExtension: false) + try? await vpnUninstaller.uninstall(removeSystemExtension: false) } } diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index c79ef7a8e2..1316d6cf85 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -109,7 +109,7 @@ final class VPNPreferencesModel: ObservableObject { switch response { case .OK: - await NetworkProtectionFeatureDisabler().disable(uninstallSystemExtension: true) + try? await VPNUninstaller().uninstall(removeSystemExtension: true) default: // intentional no-op break diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift deleted file mode 100644 index cddc3b1a9c..0000000000 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// NetworkProtectionFeatureDisabler.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 BrowserServicesKit -import Common -import NetworkExtension -import NetworkProtection -import NetworkProtectionIPC -import NetworkProtectionUI -import LoginItems -import SystemExtensions - -protocol NetworkProtectionFeatureDisabling { - /// - Returns: `true` if the uninstallation was completed. `false` if it was cancelled by the user or an error. - /// - @discardableResult - func disable(uninstallSystemExtension: Bool) async -> Bool - - func stop() -} - -final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling { - private let log: OSLog - private let loginItemsManager: LoginItemsManager - private let pinningManager: LocalPinningManager - private let settings: VPNSettings - private let userDefaults: UserDefaults - private let ipcClient: TunnelControllerIPCClient - - @MainActor - private var isDisabling = false - - init(loginItemsManager: LoginItemsManager = LoginItemsManager(), - pinningManager: LocalPinningManager = .shared, - userDefaults: UserDefaults = .netP, - settings: VPNSettings = .init(defaults: .netP), - ipcClient: TunnelControllerIPCClient = TunnelControllerIPCClient(), - log: OSLog = .networkProtection) { - - self.log = log - self.loginItemsManager = loginItemsManager - self.pinningManager = pinningManager - self.settings = settings - self.userDefaults = userDefaults - self.ipcClient = ipcClient - } - - @MainActor - private func canUninstall(includingSystemExtension: Bool) -> Bool { - !isDisabling && LoginItem.vpnMenu.status.isInstalled - } - - /// This method disables the VPN and clear all of its state. - /// - /// - Parameters: - /// - includeSystemExtension: Whether this method should uninstall the system extension. - /// - @MainActor - @discardableResult - func disable(uninstallSystemExtension: Bool) async -> Bool { - // We can do this optimistically as it has little if any impact. - unpinNetworkProtection() - - // To disable NetP we need the login item to be running - // This should be fine though as we'll disable them further down below - guard canUninstall(includingSystemExtension: uninstallSystemExtension) else { - return true - } - - isDisabling = true - - defer { - resetUserDefaults(uninstallSystemExtension: uninstallSystemExtension) - } - - enableLoginItems() - - // Allow some time for the login items to fully launch - try? await Task.sleep(interval: 0.5) - - if uninstallSystemExtension { - do { - try await removeSystemExtension() - } catch { - return false - } - } - - var attemptNumber = 1 - while attemptNumber <= 3 { - do { - try await removeVPNConfiguration() - break // Removal succeeded, break out of the while loop and continue with the rest of uninstallation - } catch { - print("Failed to remove VPN configuration, with error: \(error.localizedDescription)") - } - - attemptNumber += 1 - } - - // We want to give some time for the login item to reset state before disabling it - try? await Task.sleep(interval: 0.5) - disableLoginItems() - - notifyVPNUninstalled() - isDisabling = false - return true - } - - func stop() { - ipcClient.stop { _ in - // Intentional no-op - } - } - - private func enableLoginItems() { - loginItemsManager.enableLoginItems(LoginItemsManager.networkProtectionLoginItems, log: log) - } - - func disableLoginItems() { - loginItemsManager.disableLoginItems(LoginItemsManager.networkProtectionLoginItems) - } - - func removeSystemExtension() async throws { -#if NETP_SYSTEM_EXTENSION - try await ipcClient.debugCommand(.removeSystemExtension) -#endif - } - - private func unpinNetworkProtection() { - pinningManager.unpin(.networkProtection) - } - - private func removeVPNConfiguration() async throws { - // Remove the agent VPN configuration - try await ipcClient.debugCommand(.removeVPNConfiguration) - } - - private func resetUserDefaults(uninstallSystemExtension: Bool) { - settings.resetToDefaults() - - if uninstallSystemExtension { - userDefaults.networkProtectionOnboardingStatus = .default - } else { - userDefaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) - } - } - - private func notifyVPNUninstalled() { - // Wait a bit since the NetP button is likely being hidden - Task { - try? await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) - userDefaults.networkProtectionShouldShowVPNUninstalledMessage = true - } - } -} diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 6956ba22ae..753f6c3054 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -32,16 +32,14 @@ protocol NetworkProtectionFeatureVisibility { func canStartVPN() async throws -> Bool func isVPNVisible() -> Bool func shouldUninstallAutomatically() -> Bool - func disableForAllUsers() async - @discardableResult - func disableIfUserHasNoAccess() async -> Bool + func disableIfUserHasNoAccess() async var onboardStatusPublisher: AnyPublisher { get } } struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private static var subscriptionAuthTokenPrefix: String { "ddg:" } - private let featureDisabler: NetworkProtectionFeatureDisabling + private let vpnUninstaller: VPNUninstalling private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation private let privacyConfigurationManager: PrivacyConfigurationManaging @@ -52,13 +50,13 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), - featureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + vpnUninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP, log: OSLog = .networkProtection) { self.privacyConfigurationManager = privacyConfigurationManager self.networkProtectionFeatureActivation = networkProtectionFeatureActivation - self.featureDisabler = featureDisabler + self.vpnUninstaller = vpnUninstaller self.featureOverrides = featureOverrides self.defaults = defaults self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) @@ -123,19 +121,15 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { defaults.networkProtectionOnboardingStatusPublisher } - func disableForAllUsers() async { - await featureDisabler.disable(uninstallSystemExtension: false) - } - /// A method meant to be called safely from different places to disable the VPN if the user isn't meant to have access to it. /// - @discardableResult - func disableIfUserHasNoAccess() async -> Bool { + func disableIfUserHasNoAccess() async { guard shouldUninstallAutomatically() else { - return false + return } - await disableForAllUsers() - return true + /// There's not much to be done for this error here. + /// The uninstall call already fires pixels to allow us to track success rate and see the errors. + try? await vpnUninstaller.uninstall(removeSystemExtension: false) } } diff --git a/DuckDuckGo/Waitlist/VPNUninstaller.swift b/DuckDuckGo/Waitlist/VPNUninstaller.swift new file mode 100644 index 0000000000..7b95886092 --- /dev/null +++ b/DuckDuckGo/Waitlist/VPNUninstaller.swift @@ -0,0 +1,258 @@ +// +// VPNUninstaller.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 BrowserServicesKit +import Common +import LoginItems +import NetworkExtension +import NetworkProtection +import NetworkProtectionIPC +import NetworkProtectionUI +import PixelKit +import SystemExtensions + +protocol VPNUninstalling { + func uninstall(removeSystemExtension: Bool) async throws +} + +final class VPNUninstaller: VPNUninstalling { + + enum UninstallCancellationReason: String { + case alreadyUninstalling + case alreadyUninstalled + } + + enum UninstallError: CustomNSError { + case cancelled(reason: UninstallCancellationReason) + case runAgentError(_ error: Error) + case systemExtensionError(_ error: Error) + case vpnConfigurationError(_ error: Error) + + var errorCode: Int { + switch self { + case .cancelled: return 0 + case .runAgentError: return 1 + case .systemExtensionError: return 2 + case .vpnConfigurationError: return 3 + } + } + + var errorUserInfo: [String: Any] { + switch self { + case .cancelled(let reason): + return ["reason": reason.rawValue] + case .runAgentError(let error), + .systemExtensionError(let error), + .vpnConfigurationError(let error): + return [NSUnderlyingErrorKey: error as NSError] + } + } + } + + enum IPCUninstallAttempt: PixelKitEventV2 { + case begin + case cancelled(_ reason: UninstallCancellationReason) + case success + case failure(_ error: Error) + + var name: String { + switch self { + case .begin: + return "vpn_browser_uninstall_attempt_ipc" + + case .cancelled: + return "vpn_browser_uninstall_cancelled_ipc" + + case .success: + return "vpn_browser_uninstall_success_ipc" + + case .failure: + return "vpn_browser_uninstall_failure_ipc" + } + } + + var parameters: [String: String]? { + switch self { + case .begin, + .success, + .failure: + return nil + case .cancelled(let reason): + return ["reason": reason.rawValue] + } + } + + var error: Error? { + switch self { + case .begin, + .cancelled, + .success: + return nil + case .failure(let error): + return error + } + } + } + + private let log: OSLog + private let loginItemsManager: LoginItemsManaging + private let pinningManager: LocalPinningManager + private let settings: VPNSettings + private let userDefaults: UserDefaults + private let vpnMenuLoginItem: LoginItem + private let ipcClient: TunnelControllerIPCClient + private let pixelKit: PixelFiring? + + @MainActor + private var isDisabling = false + + init(loginItemsManager: LoginItemsManaging = LoginItemsManager(), + pinningManager: LocalPinningManager = .shared, + userDefaults: UserDefaults = .netP, + settings: VPNSettings = .init(defaults: .netP), + ipcClient: TunnelControllerIPCClient = TunnelControllerIPCClient(), + vpnMenuLoginItem: LoginItem = .vpnMenu, + pixelKit: PixelFiring? = PixelKit.shared, + log: OSLog = .networkProtection) { + + self.log = log + self.loginItemsManager = loginItemsManager + self.pinningManager = pinningManager + self.settings = settings + self.userDefaults = userDefaults + self.vpnMenuLoginItem = vpnMenuLoginItem + self.ipcClient = ipcClient + self.pixelKit = pixelKit + } + + /// This method disables the VPN and clear all of its state. + /// + /// - Parameters: + /// - includeSystemExtension: Whether this method should uninstall the system extension. + /// + @MainActor + func uninstall(removeSystemExtension: Bool) async throws { + pixelKit?.fire(IPCUninstallAttempt.begin) + + do { + // We can do this optimistically as it has little if any impact. + unpinNetworkProtection() + + guard !isDisabling else { + throw UninstallError.cancelled(reason: .alreadyUninstalling) + } + + guard vpnMenuLoginItem.status.isInstalled else { + throw UninstallError.cancelled(reason: .alreadyUninstalled) + } + + isDisabling = true + + defer { + resetUserDefaults(uninstallSystemExtension: removeSystemExtension) + } + + do { + try enableLoginItems() + } catch { + throw UninstallError.runAgentError(error) + } + + // Allow some time for the login items to fully launch + try? await Task.sleep(interval: 0.5) + + if removeSystemExtension { + do { + try await self.removeSystemExtension() + } catch { + throw UninstallError.systemExtensionError(error) + } + } + + var attemptNumber = 1 + while attemptNumber <= 3 { + do { + try await removeVPNConfiguration() + break // Removal succeeded, break out of the while loop and continue with the rest of uninstallation + } catch { + print("Failed to remove VPN configuration, with error: \(error.localizedDescription)") + + if attemptNumber == 3 { + throw UninstallError.vpnConfigurationError(error) + } + } + + attemptNumber += 1 + } + + // We want to give some time for the login item to reset state before disabling it + try? await Task.sleep(interval: 0.5) + disableLoginItems() + + notifyVPNUninstalled() + isDisabling = false + + pixelKit?.fire(IPCUninstallAttempt.success, frequency: .dailyAndCount) + } catch UninstallError.cancelled(let reason) { + pixelKit?.fire(IPCUninstallAttempt.cancelled(reason), frequency: .dailyAndCount) + } catch { + pixelKit?.fire(IPCUninstallAttempt.failure(error), frequency: .dailyAndCount) + } + } + + private func enableLoginItems() throws { + try loginItemsManager.throwingEnableLoginItems(LoginItemsManager.networkProtectionLoginItems, log: log) + } + + func disableLoginItems() { + loginItemsManager.disableLoginItems(LoginItemsManager.networkProtectionLoginItems) + } + + func removeSystemExtension() async throws { +#if NETP_SYSTEM_EXTENSION + try await ipcClient.debugCommand(.removeSystemExtension) +#endif + } + + private func unpinNetworkProtection() { + pinningManager.unpin(.networkProtection) + } + + private func removeVPNConfiguration() async throws { + // Remove the agent VPN configuration + try await ipcClient.debugCommand(.removeVPNConfiguration) + } + + private func resetUserDefaults(uninstallSystemExtension: Bool) { + settings.resetToDefaults() + + if uninstallSystemExtension { + userDefaults.networkProtectionOnboardingStatus = .default + } else { + userDefaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) + } + } + + private func notifyVPNUninstalled() { + // Wait a bit since the NetP button is likely being hidden + Task { + try? await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) + userDefaults.networkProtectionShouldShowVPNUninstalledMessage = true + } + } +} diff --git a/DuckDuckGoVPN/VPNUninstaller.swift b/DuckDuckGoVPN/VPNUninstaller.swift index 52593d9faa..6b2cabde84 100644 --- a/DuckDuckGoVPN/VPNUninstaller.swift +++ b/DuckDuckGoVPN/VPNUninstaller.swift @@ -54,7 +54,6 @@ final class VPNUninstaller: VPNUninstalling { } defaults.networkProtectionShouldShowVPNUninstalledMessage = true - exit(EXIT_SUCCESS) } } diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 6bda1607b7..e5ed58d0f8 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -143,6 +143,7 @@ final class MoreOptionsMenuTests: XCTestCase { } final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { + var onboardStatusPublisher: AnyPublisher { Just(.default).eraseToAnyPublisher() } @@ -175,7 +176,7 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility false } - func disableIfUserHasNoAccess() async -> Bool { - return false + func disableIfUserHasNoAccess() async { + // Intentional no-op } } diff --git a/UnitTests/NetworkProtection/VPNUninstallerTests.swift b/UnitTests/NetworkProtection/VPNUninstallerTests.swift new file mode 100644 index 0000000000..70876f000d --- /dev/null +++ b/UnitTests/NetworkProtection/VPNUninstallerTests.swift @@ -0,0 +1,31 @@ +// +// VPNUninstallerTests.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 +@testable import DuckDuckGo_Privacy_Browser +@testable import NetworkProtection + +final class VPNUninstallerTests: XCTestCase { + + func testUninstallWorks() async throws { + let uninstaller = VPNUninstaller() + + try await uninstaller.uninstall(removeSystemExtension: true) + } +} From 99da492dfb4c0b0a4e708a52433a82a0a446657c Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 15 May 2024 08:50:47 -0300 Subject: [PATCH 109/134] DBP: Fix identifer and type not being parsed on data brokers (#2780) --- .../Model/ExtractedProfile.swift | 2 ++ .../BrokerJSONCodableTests.swift | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 70a76305b3..aa6158bd24 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -24,6 +24,8 @@ struct ProfileSelector: Codable { let afterText: String? let beforeText: String? let separator: String? + let identifier: String? + let identifierType: String? } struct ExtractProfileSelectors: Codable, Sendable { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift index 9daf1612a4..7f77811b1c 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift @@ -79,7 +79,9 @@ final class BrokerJSONCodableTests: XCTestCase { "findElements": true }, "profileUrl": { - "selector": "a" + "selector": ".link-to-details", + "identifierType": "path", + "identifier": "https://www.advancedbackgroundchecks.com/${id}" } } } @@ -346,6 +348,18 @@ final class BrokerJSONCodableTests: XCTestCase { } } + func testVerecorJSONProfileURLSelector_isCorrectlyParsed() { + do { + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorWithURLJSONString.data(using: .utf8)!) + let scanStep = try broker.scanStep() + let extractAction = scanStep.actions.first(where: { $0.actionType == .extract })! as! ExtractAction + XCTAssertEqual(extractAction.profile.profileUrl?.identifierType, "path") + XCTAssertEqual(extractAction.profile.profileUrl?.identifier, "https://www.advancedbackgroundchecks.com/${id}") + } catch { + XCTFail("JSON string should be parsed correctly.") + } + } + // swiftlint:disable function_body_length func testParentSelector_isCorrectlyParsed() { let json = """ From 227170a15e24a4a3c60e57590b95ae6ef0881145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 15 May 2024 14:04:27 +0200 Subject: [PATCH 110/134] Fix encoding for broken site reporting pixel. (#2782) Task/Issue URL: https://app.asana.com/0/414235014887631/1207052048359801/f --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c46cad6374..3fb42836be 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12842,7 +12842,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.0.0; + version = 145.0.1; }; }; 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 cfbe5f08f9..6ff690f7b4 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" : "6568d48c381042275c1936c6301a5100dea1c278", - "version" : "145.0.0" + "revision" : "40b80f97bf55720d126399d9321f5374595207f0", + "version" : "145.0.1" } }, { @@ -174,7 +174,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "6c84fd19139414fc0edbf9673ade06e532a564f0", "version" : "2.0.0" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 77c51492c6..91ba9668df 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: "145.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6bca821bc5..3bf7fe95eb 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index f8d1329f0f..2544ca6c4f 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: "145.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 44c4dfeb8ec8e76f651a63fc89e06b20a2c13771 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 15 May 2024 18:52:46 +0600 Subject: [PATCH 111/134] Fix downloads button not appearing on download start (#2779) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207298392719906/f --- .../Services/DownloadListCoordinator.swift | 30 ++++- .../View/NavigationBarViewController.swift | 39 +++--- .../DownloadListCoordinatorTests.swift | 126 +++++++++--------- 3 files changed, 108 insertions(+), 87 deletions(-) diff --git a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift index 55763a2c5c..5364ca781a 100644 --- a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift +++ b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift @@ -57,7 +57,10 @@ final class DownloadListCoordinator { case removed case updated(oldValue: DownloadListItem) } - typealias Update = (kind: UpdateKind, item: DownloadListItem) + struct Update { + let kind: UpdateKind + let item: DownloadListItem + } private let updatesSubject = PassthroughSubject() let progress = Progress() @@ -143,7 +146,7 @@ final class DownloadListCoordinator { guard let destinationFilePresenter = try presenters.destination.get() else { throw FileAddError.noDestinationUrl } self.subscribeToPresenters((destination: destinationFilePresenter, tempFile: try? presenters.tempFile.get()), of: item) - self.updatesSubject.send((.added, item)) + self.updatesSubject.send(Update(kind: .added, item: item)) } // swiftlint:disable:next cyclomatic_complexity @@ -384,15 +387,15 @@ final class DownloadListCoordinator { case (.none, .none): break case (.none, .some(let item)): - self.updatesSubject.send((.added, item)) + self.updatesSubject.send(Update(kind: .added, item: item)) store.save(item) case (.some(let oldValue), .some(let item)): - self.updatesSubject.send((.updated(oldValue: oldValue), item)) + self.updatesSubject.send(Update(kind: .updated(oldValue: oldValue), item: item)) store.save(item) case (.some(let item), .none): item.progress?.cancel() if original != nil { - self.updatesSubject.send((.removed, item)) + self.updatesSubject.send(Update(kind: .removed, item: item)) } cleanupTempFiles(for: item) filePresenters[item.identifier] = nil @@ -451,6 +454,10 @@ final class DownloadListCoordinator { !downloadTaskCancellables.isEmpty } + var isEmpty: Bool { + items.isEmpty + } + @MainActor func downloads(sortedBy keyPath: KeyPath, ascending: Bool) -> [DownloadListItem] { return items.values.sorted { @@ -572,3 +579,16 @@ private extension DownloadListItem { } } + +extension DownloadListCoordinator.Update { + + var isDownloadCompletedUpdate: Bool { + if case .updated(let oldValue) = kind, + oldValue.progress != nil && item.progress == nil { + true + } else { + false + } + } + +} diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 4e438ad79f..1addc31915 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -607,34 +607,31 @@ final class NavigationBarViewController: NSViewController { } private func subscribeToDownloads() { + // show Downloads button on download completion for downloads started from non-Fire window downloadListCoordinator.updates .filter { update in // filter download completion events only - if case .updated(let oldValue) = update.kind, - oldValue.progress != nil && update.item.progress == nil { - return true - } else { - return false - } + !update.item.isBurner && update.isDownloadCompletedUpdate } - .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] update in - guard let self else { return } + .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + guard let self, !self.isDownloadsPopoverShown, + DownloadsPreferences.shared.shouldOpenPopupOnCompletion, + WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window else { return } - if DownloadsPreferences.shared.shouldOpenPopupOnCompletion, - !update.item.isBurner, - WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window { + self.popovers.showDownloadsPopoverAndAutoHide(from: downloadsButton, popoverDelegate: self, downloadsDelegate: self) + } + .store(in: &downloadsCancellables) - self.popovers.showDownloadsPopoverAndAutoHide(from: downloadsButton, - popoverDelegate: self, - downloadsDelegate: self) - } else if update.item.isBurner { - invalidateDownloadButtonHidingTimer() - } - updateDownloadsButton() + // update Downloads button visibility and state + downloadListCoordinator.updates + .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + self?.updateDownloadsButton() } .store(in: &downloadsCancellables) + // update Downloads button total progress indicator downloadListCoordinator.progress.publisher(for: \.totalUnitCount) .combineLatest(downloadListCoordinator.progress.publisher(for: \.completedUnitCount)) .map { (total, completed) -> Double? in @@ -739,6 +736,10 @@ final class NavigationBarViewController: NSViewController { let hasActiveDownloads = downloadListCoordinator.hasActiveDownloads downloadsButton.image = hasActiveDownloads ? .downloadsActive : .downloads + + if downloadListCoordinator.isEmpty { + invalidateDownloadButtonHidingTimer() + } let isTimerActive = downloadsButtonHidingTimer != nil downloadsButton.isShown = if popovers.isDownloadsPopoverShown { diff --git a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift index 8446b932b2..c318dcf867 100644 --- a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift +++ b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift @@ -74,10 +74,10 @@ final class DownloadListCoordinatorTests: XCTestCase { let e = expectation(description: "download added") var id: UUID! - let c = coordinator.updates.sink { kind, item in - if case .added = kind { + let c = coordinator.updates.sink { update in + if case .added = update.kind { e.fulfill() - id = item.identifier + id = update.item.identifier } } downloadManager.downloadAddedSubject.send(task) @@ -152,12 +152,12 @@ final class DownloadListCoordinatorTests: XCTestCase { } } - let c = coordinator.updates.sink { (kind, item) in - if case .added = kind { - expectations[item.identifier]!.fulfill() - } else if case .updated = kind { + let c = coordinator.updates.sink { update in + if case .added = update.kind { + expectations[update.item.identifier]!.fulfill() + } else if case .updated = update.kind { } else { - XCTFail("unexpected \(kind) \(item.fileName)") + XCTFail("unexpected \(update.kind) \(update.item.fileName)") } } @@ -183,17 +183,17 @@ final class DownloadListCoordinatorTests: XCTestCase { let e1 = expectation(description: "download added") let e2 = expectation(description: "download updated") - let c = coordinator.updates.sink { [coordinator] (kind, item) in - switch kind { + let c = coordinator.updates.sink { [coordinator] update in + switch update.kind { case .added: - XCTAssertEqual(item.progress, task.progress) + XCTAssertEqual(update.item.progress, task.progress) XCTAssertTrue(coordinator!.hasActiveDownloads) e1.fulfill() case .updated: - guard let tempUrlValue = item.tempURL else { return } - XCTAssertEqual(item.destinationURL, destURL) + guard let tempUrlValue = update.item.tempURL else { return } + XCTAssertEqual(update.item.destinationURL, destURL) XCTAssertEqual(tempUrlValue, tempURL) - XCTAssertEqual(item.fileName, destURL.lastPathComponent) + XCTAssertEqual(update.item.fileName, destURL.lastPathComponent) e2.fulfill() case .removed: XCTFail("unexpected .removed") @@ -217,14 +217,14 @@ final class DownloadListCoordinatorTests: XCTestCase { let taskCompleted = expectation(description: "item updated") var c: AnyCancellable! - c = coordinator.updates.sink { (kind, item) in - guard case .updated = kind, item.progress == nil else { return } + c = coordinator.updates.sink { update in + guard case .updated = update.kind, update.item.progress == nil else { return } taskCompleted.fulfill() - XCTAssertEqual(item.destinationURL, self.destURL) - XCTAssertNil(item.tempURL) - XCTAssertNil(item.progress) + XCTAssertEqual(update.item.destinationURL, self.destURL) + XCTAssertNil(update.item.tempURL) + XCTAssertNil(update.item.progress) c?.cancel() } @@ -245,10 +245,10 @@ final class DownloadListCoordinatorTests: XCTestCase { let (download, task, _) = setUpCoordinatorAndAddDownload(isBurner: true) let taskRemoved = expectation(description: "Task removed") - let c = coordinator.updates.sink { (kind, item) in - XCTAssertTrue(item.isBurner) + let c = coordinator.updates.sink { update in + XCTAssertTrue(update.item.isBurner) - if case .removed = kind { + if case .removed = update.kind { taskRemoved.fulfill() } } @@ -267,17 +267,17 @@ final class DownloadListCoordinatorTests: XCTestCase { let taskCompleted = expectation(description: "location updated") var c: AnyCancellable! - c = coordinator.updates.sink { (kind, item) in - if case .updated = kind { } else { - XCTFail("\(kind) is not .updated") + c = coordinator.updates.sink { update in + if case .updated = update.kind { } else { + XCTFail("\(update.kind) is not .updated") } - guard item.destinationURL != nil, item.tempURL != nil else { return } + guard update.item.destinationURL != nil, update.item.tempURL != nil else { return } - XCTAssertEqual(item.destinationURL, self.destURL) - XCTAssertEqual(item.tempURL, self.tempURL) - XCTAssertNil(item.progress) - XCTAssertEqual(item.error, FileDownloadError.failedToCompleteDownloadTask(underlyingError: TestError(), resumeData: .resumeData, isRetryable: true)) - XCTAssertEqual(item.error?.resumeData, .resumeData) + XCTAssertEqual(update.item.destinationURL, self.destURL) + XCTAssertEqual(update.item.tempURL, self.tempURL) + XCTAssertNil(update.item.progress) + XCTAssertEqual(update.item.error, FileDownloadError.failedToCompleteDownloadTask(underlyingError: TestError(), resumeData: .resumeData, isRetryable: true)) + XCTAssertEqual(update.item.error?.resumeData, .resumeData) taskCompleted.fulfill() c.cancel() } @@ -330,16 +330,16 @@ final class DownloadListCoordinatorTests: XCTestCase { } let itemUpdated = expectation(description: "item updated") - let c = coordinator.updates.sink { (kind, item) in - if case .updated = kind { } else { - XCTFail("\(kind) is not .updated") + let c = coordinator.updates.sink { update in + if case .updated = update.kind { } else { + XCTFail("\(update.kind) is not .updated") } itemUpdated.fulfill() - XCTAssertEqual(item.destinationURL, item.destinationURL) - XCTAssertEqual(item.tempURL, item.tempURL) - XCTAssertNotNil(item.progress) - XCTAssertNil(item.error) + XCTAssertEqual(update.item.destinationURL, item.destinationURL) + XCTAssertEqual(update.item.tempURL, item.tempURL) + XCTAssertNotNil(update.item.progress) + XCTAssertNil(update.item.error) } coordinator.restart(downloadWithIdentifier: DownloadListItem.testFailedItem.identifier) @@ -354,7 +354,7 @@ final class DownloadListCoordinatorTests: XCTestCase { func testWhenAddedDownloadRestartedWithResumeDataThenResumeIsCalled() throws { let (download, task, id) = setUpCoordinatorAndAddDownload() let taskFailed = expectation(description: "task failed") - let c1 = coordinator.updates.sink { _, _ in + let c1 = coordinator.updates.sink { _ in taskFailed.fulfill() } task.download(download.asWKDownload(), didFailWithError: TestError(), resumeData: .resumeData) @@ -390,16 +390,16 @@ final class DownloadListCoordinatorTests: XCTestCase { } let itemUpdated = expectation(description: "item updated") - let c = coordinator.updates.sink { (kind, item) in - if case .updated = kind { } else { - XCTFail("\(kind) is not .updated") + let c = coordinator.updates.sink { update in + if case .updated = update.kind { } else { + XCTFail("\(update.kind) is not .updated") } itemUpdated.fulfill() - XCTAssertEqual(item.destinationURL, item.destinationURL) - XCTAssertEqual(item.tempURL, item.tempURL) - XCTAssertNotNil(item.progress) - XCTAssertNil(item.error) + XCTAssertEqual(update.item.destinationURL, update.item.destinationURL) + XCTAssertEqual(update.item.tempURL, update.item.tempURL) + XCTAssertNotNil(update.item.progress) + XCTAssertNil(update.item.error) } coordinator.restart(downloadWithIdentifier: id) @@ -450,16 +450,16 @@ final class DownloadListCoordinatorTests: XCTestCase { } let itemUpdated = expectation(description: "item updated") - let c = coordinator.updates.sink { (kind, item) in - if case .updated = kind { } else { - XCTFail("\(kind) is not .updated") + let c = coordinator.updates.sink { update in + if case .updated = update.kind { } else { + XCTFail("\(update.kind) is not .updated") } itemUpdated.fulfill() - XCTAssertEqual(item.destinationURL, item.destinationURL) - XCTAssertEqual(item.tempURL, item.tempURL) - XCTAssertNotNil(item.progress) - XCTAssertNil(item.error) + XCTAssertEqual(update.item.destinationURL, item.destinationURL) + XCTAssertEqual(update.item.tempURL, item.tempURL) + XCTAssertNotNil(update.item.progress) + XCTAssertNil(update.item.error) } coordinator.restart(downloadWithIdentifier: DownloadListItem.testFailedItem.identifier) @@ -475,13 +475,13 @@ final class DownloadListCoordinatorTests: XCTestCase { let (download, _, id) = setUpCoordinatorAndAddDownload() let itemRemoved = expectation(description: "item removed") - let c = coordinator.updates.sink { (kind, item) in - if case .removed = kind { } else { - XCTFail("\(kind) is not .updated") + let c = coordinator.updates.sink { update in + if case .removed = update.kind { } else { + XCTFail("\(update.kind) is not .updated") } itemRemoved.fulfill() - XCTAssertEqual(item.identifier, id) + XCTAssertEqual(update.item.identifier, id) } let cancelCalled = expectation(description: "download cancelled") download.cancelBlock = { @@ -509,20 +509,20 @@ final class DownloadListCoordinatorTests: XCTestCase { let e1 = expectation(description: "download stopped") e1.expectedFulfillmentCount = 2 - var c = coordinator.updates.sink { (kind, item) in - guard case .updated = kind, item.progress == nil else { return } + var c = coordinator.updates.sink { update in + guard case .updated = update.kind, update.item.progress == nil else { return } e1.fulfill() - XCTAssertNotEqual(item.identifier, keptId) + XCTAssertNotEqual(update.item.identifier, keptId) } waitForExpectations(timeout: 1) let e2 = expectation(description: "item removed") e2.expectedFulfillmentCount = 2 - c = coordinator.updates.sink { (kind, item) in - guard case .removed = kind else { return } + c = coordinator.updates.sink { update in + guard case .removed = update.kind else { return } e2.fulfill() - XCTAssertNotEqual(item.identifier, keptId) + XCTAssertNotEqual(update.item.identifier, keptId) } coordinator.cleanupInactiveDownloads() From d6f9f50f264baa2616783af2abd5cfdd9a7d7e73 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 15 May 2024 11:11:23 -0300 Subject: [PATCH 112/134] DBP: Add sleep duration on pixels (#2771) --- .../DataBrokerRunCustomJSONViewModel.swift | 7 ++ .../DebugUI/DebugScanOperation.swift | 2 + .../Operations/DataBrokerOperation.swift | 5 +- .../DataBrokerOperationRunner.swift | 4 + .../Operations/OptOutOperation.swift | 3 + .../Operations/ScanOperation.swift | 3 + .../Pixels/DataBrokerProtectionPixels.swift | 13 +-- .../DataBrokerProtectionSleepObserver.swift | 82 +++++++++++++++++++ .../DataBrokerOperationActionTests.swift | 19 +++++ 9 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 6b5894d5e1..3cf0289b99 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -474,6 +474,13 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } } +final class FakeSleepObserver: SleepObserver { + + func totalSleepTime() -> TimeInterval { + return 0 + } +} + final class FakeStageDurationCalculator: StageDurationCalculator { var attemptId: UUID = UUID() var isManualScan: Bool = false diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift index 3d4e4b779c..bc6ff398d2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift @@ -70,6 +70,7 @@ final class DebugScanOperation: DataBrokerOperation { let cookieHandler: CookieHandler let pixelHandler: EventMapping var postLoadingSiteStartTime: Date? + let sleepObserver: SleepObserver private let fileManager = FileManager.default private let debugScanContentPath: String? @@ -101,6 +102,7 @@ final class DebugScanOperation: DataBrokerOperation { pixelHandler = EventMapping(mapping: { _, _, _, _ in // We do not need the pixel handler for the debug }) + self.sleepObserver = FakeSleepObserver() } func run(inputValue: Void, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 893ed9e350..d9bee8a71f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -34,6 +34,7 @@ protocol DataBrokerOperation: CCFCommunicationDelegate { var cookieHandler: CookieHandler { get } var stageCalculator: StageDurationCalculator { get } var pixelHandler: EventMapping { get } + var sleepObserver: SleepObserver { get } var webViewHandler: WebViewHandler? { get set } var actionsHandler: ActionsHandler? { get } @@ -202,7 +203,7 @@ extension DataBrokerOperation { if stageCalculator.isManualScan { let dataBrokerURL = self.query.dataBroker.url let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) - pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL, sleepDuration: sleepObserver.totalSleepTime())) } } @@ -210,7 +211,7 @@ extension DataBrokerOperation { if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { let dataBrokerURL = self.query.dataBroker.url let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) - pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL, sleepDuration: sleepObserver.totalSleepTime())) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift index 52a64c3fac..f8c2b98aca 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift @@ -87,6 +87,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { + let sleepObserver = DataBrokerProtectionSleepObserver(brokerProfileQueryData: profileQuery) let scan = ScanOperation( privacyConfig: privacyConfigManager, prefs: contentScopeProperties, @@ -95,6 +96,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { captchaService: captchaService, stageDurationCalculator: stageCalculator, pixelHandler: pixelHandler, + sleepObserver: sleepObserver, shouldRunNextStep: shouldRunNextStep ) return try await scan.run(inputValue: (), showWebView: showWebView) @@ -106,6 +108,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { + let sleepObserver = DataBrokerProtectionSleepObserver(brokerProfileQueryData: profileQuery) let optOut = OptOutOperation( privacyConfig: privacyConfigManager, prefs: contentScopeProperties, @@ -114,6 +117,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { captchaService: captchaService, stageCalculator: stageCalculator, pixelHandler: pixelHandler, + sleepObserver: sleepObserver, shouldRunNextStep: shouldRunNextStep ) try await optOut.run(inputValue: extractedProfile, showWebView: showWebView) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift index 0a957a62f8..9f48f0faa1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift @@ -42,6 +42,7 @@ final class OptOutOperation: DataBrokerOperation { let clickAwaitTime: TimeInterval let pixelHandler: EventMapping var postLoadingSiteStartTime: Date? + let sleepObserver: SleepObserver // Captcha is a third-party resource that sometimes takes more time to load // if we are not able to get the captcha information. We will try to run the action again @@ -60,6 +61,7 @@ final class OptOutOperation: DataBrokerOperation { clickAwaitTime: TimeInterval = 40, stageCalculator: StageDurationCalculator, pixelHandler: EventMapping, + sleepObserver: SleepObserver, shouldRunNextStep: @escaping () -> Bool ) { self.privacyConfig = privacyConfig @@ -73,6 +75,7 @@ final class OptOutOperation: DataBrokerOperation { self.clickAwaitTime = clickAwaitTime self.cookieHandler = cookieHandler self.pixelHandler = pixelHandler + self.sleepObserver = sleepObserver } func run(inputValue: ExtractedProfile, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index f7bb9de995..964c7cdf69 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -43,6 +43,7 @@ final class ScanOperation: DataBrokerOperation { let clickAwaitTime: TimeInterval let pixelHandler: EventMapping var postLoadingSiteStartTime: Date? + let sleepObserver: SleepObserver init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, @@ -54,6 +55,7 @@ final class ScanOperation: DataBrokerOperation { clickAwaitTime: TimeInterval = 0, stageDurationCalculator: StageDurationCalculator, pixelHandler: EventMapping, + sleepObserver: SleepObserver, shouldRunNextStep: @escaping () -> Bool ) { self.privacyConfig = privacyConfig @@ -67,6 +69,7 @@ final class ScanOperation: DataBrokerOperation { self.clickAwaitTime = clickAwaitTime self.cookieHandler = cookieHandler self.pixelHandler = pixelHandler + self.sleepObserver = sleepObserver } func run(inputValue: InputValue, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad7f3cd61d..c9edeedfc4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -69,6 +69,7 @@ public enum DataBrokerProtectionPixels { static let profileQueries = "profile_queries" static let hasError = "has_error" static let brokerURL = "broker_url" + static let sleepDuration = "sleep_duration" } case error(error: DataBrokerProtectionError, dataBroker: String) @@ -177,8 +178,8 @@ public enum DataBrokerProtectionPixels { // Initial scans pixels // https://app.asana.com/0/1204006570077678/1206981742767458/f case initialScanTotalDuration(duration: Double, profileQueries: Int) - case initialScanSiteLoadDuration(duration: Double, hasError: Bool, brokerURL: String) - case initialScanPostLoadingDuration(duration: Double, hasError: Bool, brokerURL: String) + case initialScanSiteLoadDuration(duration: Double, hasError: Bool, brokerURL: String, sleepDuration: Double) + case initialScanPostLoadingDuration(duration: Double, hasError: Bool, brokerURL: String, sleepDuration: Double) case initialScanPreStartDuration(duration: Double) } @@ -437,10 +438,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent { Consts.backendServiceCallSite: backendServiceCallSite.rawValue] case .initialScanTotalDuration(let duration, let profileQueries): return [Consts.durationInMs: String(duration), Consts.profileQueries: String(profileQueries)] - case .initialScanSiteLoadDuration(let duration, let hasError, let brokerURL): - return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL] - case .initialScanPostLoadingDuration(let duration, let hasError, let brokerURL): - return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL] + case .initialScanSiteLoadDuration(let duration, let hasError, let brokerURL, let sleepDuration): + return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL, Consts.sleepDuration: String(sleepDuration)] + case .initialScanPostLoadingDuration(let duration, let hasError, let brokerURL, let sleepDuration): + return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL, Consts.sleepDuration: String(sleepDuration)] case .initialScanPreStartDuration(let duration): return [Consts.durationInMs: String(duration)] } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift new file mode 100644 index 0000000000..a38a78ec29 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift @@ -0,0 +1,82 @@ +// +// DataBrokerProtectionSleepObserver.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 Cocoa +import Common + +protocol SleepObserver { + func totalSleepTime() -> TimeInterval +} +/// This class purpose is to measure from the background agent how much time the operations +/// are working while the computer is asleep. +/// This will help us gather metrics around what happen to WebViews when the computer is sleeping. +/// +/// https://app.asana.com/0/1204006570077678/1207278682082256/f +final class DataBrokerProtectionSleepObserver: SleepObserver { + private var startSleepTime: Date? + private var endTime: TimeInterval? + private let brokerProfileQueryData: BrokerProfileQueryData + + init(brokerProfileQueryData: BrokerProfileQueryData) { + self.brokerProfileQueryData = brokerProfileQueryData + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(willSleepNotification(_:)), name: NSWorkspace.willSleepNotification, object: nil) + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) + } + + deinit { + os_log(.debug, log: .dataBrokerProtection, "SleepObserver: Deinit %{public}s %{public}s %{public}s", brokerProfileQueryData.dataBroker.name, brokerProfileQueryData.profileQuery.firstName, brokerProfileQueryData.profileQuery.city) + NotificationCenter.default.removeObserver(self) + } + + func totalSleepTime() -> TimeInterval { + guard let totalSleepTime = self.endTime else { + return 0 + } + + os_log(.debug, log: .dataBrokerProtection, "SleepObserver: Total Sleep time more than zero: %{public}s", String(totalSleepTime)) + + 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) + 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) + guard let startSleepTime = self.startSleepTime else { + return + } + + if let endTime = self.endTime { + // This scenario can happen if during the scan the computer goes to sleep more than once. + let currentSleepIterationTime = Date().timeIntervalSince(startSleepTime).toMs + self.endTime = endTime + currentSleepIterationTime + } else { + endTime = Date().timeIntervalSince(startSleepTime).toMs + } + } +} + +extension TimeInterval { + var toMs: TimeInterval { + (self * 1000).rounded(.towardZero) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index cdbb776170..2b46c2b05f 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -49,6 +49,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -74,6 +75,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -106,6 +108,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -136,6 +139,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -159,6 +163,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) emailService.shouldThrow = true @@ -188,6 +193,7 @@ final class DataBrokerOperationActionTests: XCTestCase { clickAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -208,6 +214,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -230,6 +237,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -253,6 +261,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) let actionsHandler = ActionsHandler(step: step) @@ -283,6 +292,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -306,6 +316,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.retriesCountOnError = 0 @@ -330,6 +341,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -349,6 +361,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) sut.webViewHandler = webViewHandler @@ -370,6 +383,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: mockStageCalculator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -390,6 +404,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: mockStageCalculator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -410,6 +425,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: mockStageCalculator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -430,6 +446,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: mockStageCalculator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -450,6 +467,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) @@ -472,6 +490,7 @@ final class DataBrokerOperationActionTests: XCTestCase { operationAwaitTime: 0, stageCalculator: stageCalulator, pixelHandler: pixelHandler, + sleepObserver: FakeSleepObserver(), shouldRunNextStep: { true } ) From 29dc71858662250a17380a39f54a42094fd22058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 15 May 2024 16:36:00 +0200 Subject: [PATCH 113/134] Update BSK (#2784) Task/Issue URL: https://app.asana.com/0/1205237866452338/1207324536994064/f **Description**: It updates BSK, where I reverted to using old encoding for params! --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3fb42836be..d9207b1de2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12842,7 +12842,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.0.1; + version = 145.0.2; }; }; 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 6ff690f7b4..4f1d6c03e6 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" : "40b80f97bf55720d126399d9321f5374595207f0", - "version" : "145.0.1" + "revision" : "6e77cb1df92d64724ba7bbb3f85947c820b3f1d1", + "version" : "145.0.2" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 91ba9668df..5968e9dc4a 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: "145.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.2"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 3bf7fe95eb..e0fa074267 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.2"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 2544ca6c4f..c87ae12edf 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: "145.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.2"), .package(path: "../SwiftUIExtensions") ], targets: [ From 0815921366f6e79ff9b839b5aec4d97557d3d01a Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Wed, 15 May 2024 10:31:00 -0500 Subject: [PATCH 114/134] Bump BSK Version (#2766) Task/Issue URL: https://app.asana.com/0/0/1207281817761492/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. --- ###### 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 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d9207b1de2..22e32f5cf3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12842,7 +12842,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.0.2; + version = 145.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 4f1d6c03e6..2cf0cd36d6 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" : "6e77cb1df92d64724ba7bbb3f85947c820b3f1d1", - "version" : "145.0.2" + "revision" : "739e7a89f1ddf18d2fe83e94a83892e248f60668", + "version" : "145.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "1bb3bc5eb565735051f342a87b5405d4374876c7", - "version" : "5.12.0" + "revision" : "bb8e7e62104ed6506c7bfd3ef7aa4aca3686ed4f", + "version" : "5.15.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5968e9dc4a..6b7ae3d3a1 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: "145.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index e0fa074267..2879531843 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index c87ae12edf..2e126269fa 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: "145.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From be5fa66540f1e66ab9a11b28282532df672ce874 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 16 May 2024 11:10:24 +1000 Subject: [PATCH 115/134] Subscription attribution (#2761) Task/Issue URL: https://app.asana.com/0/1199230911884351/1205406454505225/f **Description**: This PR adds the attribution pixels when a Privacy Pro subscription is purchased. --- DuckDuckGo.xcodeproj/project.pbxproj | 62 ++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Attribution/AttributionPixelHandler.swift | 97 ++++++++++++++ .../Common/Extensions/URLExtension.swift | 1 + .../InstallationAttributionPixelHandler.swift | 63 ++------- .../Statistics/ATB/StatisticsLoader.swift | 4 +- DuckDuckGo/Statistics/PrivacyProPixel.swift | 2 + .../SubscriptionAttributionPixelHandler.swift | 46 +++++++ .../SubscriptionRedirectManager.swift | 66 ++++++++++ .../RedirectNavigationResponder.swift | 23 +--- .../SubscriptionPagesUserScript.swift | 18 ++- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Pixel/PixelCapturedParameters.swift | 31 +++++ ...allationAttributionPixelHandlerTests.swift | 49 +++---- .../Mock/MockAttributionOriginProvider.swift | 1 + .../Mock/MockAttributionsPixelHandler.swift | 2 +- ...criptionAttributionPixelHandlerTests.swift | 124 ++++++++++++++++++ .../SubscriptionRedirectManagerTests.swift | 61 +++++++++ 20 files changed, 551 insertions(+), 109 deletions(-) create mode 100644 DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift create mode 100644 DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift create mode 100644 DuckDuckGo/Subscription/SubscriptionRedirectManager.swift create mode 100644 UnitTests/Common/Pixel/PixelCapturedParameters.swift create mode 100644 UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift create mode 100644 UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 22e32f5cf3..220f975915 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1714,6 +1714,10 @@ 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; + 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; + 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; + 9F0660782BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */; }; + 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; @@ -1748,6 +1752,14 @@ 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; + 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; + 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; + 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; + 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; + 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; @@ -3475,6 +3487,8 @@ 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; + 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandlerTests.swift; sourceTree = ""; }; + 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelCapturedParameters.swift; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelMock.swift; sourceTree = ""; }; @@ -3492,6 +3506,10 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; + 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionPixelHandler.swift; sourceTree = ""; }; + 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; + 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; + 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManagerTests.swift; sourceTree = ""; }; 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; @@ -4516,6 +4534,8 @@ isa = PBXGroup; children = ( 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, + 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */, + 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */, ); path = Subscription; sourceTree = ""; @@ -6253,6 +6273,7 @@ B683097A274DCFE3004B46BB /* Database */, AAEC74B92642E66600C2EFBC /* Extensions */, 4BA1A6CE258BF58C00F6F690 /* FileSystem */, + 9F0660752BECC7E700B8EEF1 /* Pixel */, B6AE74322609AFBB005B9B1A /* Progress */, B698E5032908011E00A746A8 /* AppKitPrivateMethodsAvailabilityTests.swift */, B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */, @@ -6304,6 +6325,14 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F0660752BECC7E700B8EEF1 /* Pixel */ = { + isa = PBXGroup; + children = ( + 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */, + ); + path = Pixel; + sourceTree = ""; + }; 9F0FFFB62BCCAE80007C87DD /* Mocks */ = { isa = PBXGroup; children = ( @@ -6314,6 +6343,23 @@ path = Mocks; sourceTree = ""; }; + 9F64345F2BEC82A000D2D8A0 /* Attribution */ = { + isa = PBXGroup; + children = ( + 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */, + ); + path = Attribution; + sourceTree = ""; + }; + 9F64346E2BECB9FB00D2D8A0 /* Subscriptions */ = { + isa = PBXGroup; + children = ( + 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */, + 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */, + ); + path = Subscriptions; + sourceTree = ""; + }; 9F872D9B2B9058B000138637 /* Extensions */ = { isa = PBXGroup; children = ( @@ -6636,6 +6682,7 @@ 858A798626A99D9000A75A42 /* SecureVault */, B6DA440F2616C0F200DD1EC2 /* Statistics */, AA63744E24C9BB4A00AB2AC4 /* Suggestions */, + 9F64346E2BECB9FB00D2D8A0 /* Subscriptions */, AA92ACAE24EFE1F5005F41C9 /* Tab */, AAC9C01224CAFBB700AD1325 /* TabBar */, B6CA4822298CDC0B0067ECCE /* TabExtensionsTests */, @@ -6901,6 +6948,7 @@ AA86491324D831B9001BABEE /* Common */ = { isa = PBXGroup; children = ( + 9F64345F2BEC82A000D2D8A0 /* Attribution */, 4B67743D255DBEEA00025BD8 /* Database */, AADC60E92493B305008F8EF7 /* Extensions */, 4BA1A691258B06F600F6F690 /* FileSystem */, @@ -9520,6 +9568,7 @@ 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, + 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9955,6 +10004,7 @@ B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, + 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, 3706FBED293F65D500E42796 /* TabCollection.swift in Sources */, B6C0BB6B29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, @@ -10063,6 +10113,7 @@ 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 */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, @@ -10328,6 +10379,7 @@ 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */, 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */, + 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, 3706FE20293F661700E42796 /* CLLocationManagerMock.swift in Sources */, B6656E0E2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, B6C843DB2BA1CAB6006FDEC3 /* FilePresenterTests.swift in Sources */, @@ -10343,6 +10395,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, @@ -10496,6 +10549,7 @@ 5681ED442BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */, B60C6F8529B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, 567DA94629E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, + 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, 3706FE82293F661700E42796 /* MockStatisticsStore.swift in Sources */, 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, @@ -10984,6 +11038,7 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, + 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, @@ -11093,6 +11148,7 @@ B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, + 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, @@ -11150,6 +11206,7 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, + 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11593,6 +11650,7 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.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 */, @@ -11719,6 +11777,7 @@ 1D8C2FED2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, + 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */, @@ -11754,6 +11813,7 @@ B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */, 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, 1D8C2FF02B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */, + 9F0660782BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, 1D3B1AC429378953006F4388 /* BWResponseTests.swift in Sources */, B693955F26F1C17F0015B914 /* DownloadListCoordinatorTests.swift in Sources */, 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, @@ -12842,7 +12902,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.1.0; + version = 145.2.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 2cf0cd36d6..415ca34871 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" : "739e7a89f1ddf18d2fe83e94a83892e248f60668", - "version" : "145.1.0" + "revision" : "c69a664b58cb351ccb73aa548726854a2861c0ed", + "version" : "145.2.0" } }, { diff --git a/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift b/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift new file mode 100644 index 0000000000..0a21678f79 --- /dev/null +++ b/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift @@ -0,0 +1,97 @@ +// +// AttributionPixelHandler.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 + +// A type that send pixels that needs attributions parameters. +protocol AttributionPixelHandler { + func fireAttributionPixel( + event: PixelKit.Event, + frequency: PixelKit.Frequency, + origin: String?, + additionalParameters: [String: String]? + ) +} + +final class GenericAttributionPixelHandler: AttributionPixelHandler { + enum Parameters { + static let origin = "origin" + static let locale = "locale" + } + + private let fireRequest: FireRequest + private let locale: Locale + + /// Creates an instance with the specified fire request, origin provider and locale. + /// - Parameters: + /// - fireRequest: A function for sending the Pixel request. + /// - locale: The locale of the device. + init( + fireRequest: @escaping FireRequest = PixelKit.fire, + locale: Locale = .current + ) { + self.fireRequest = fireRequest + self.locale = locale + } + + func fireAttributionPixel( + event: PixelKit.Event, + frequency: PixelKit.Frequency, + origin: String?, + additionalParameters: [String: String]? + ) { + fireRequest( + event, + frequency, + [:], + self.parameters(additionalParameters, withOrigin: origin, locale: locale.identifier), + nil, + nil, + true, { _, _ in } + ) + } +} + +// MARK: - Parameter + +private extension GenericAttributionPixelHandler { + func parameters(_ parameters: [String: String]?, withOrigin origin: String?, locale: String) -> [String: String] { + var parameters = parameters ?? [:] + parameters[Self.Parameters.locale] = locale + if let origin { + parameters[Self.Parameters.origin] = origin + } + return parameters + } +} + +// MARK: - FireRequest + +extension GenericAttributionPixelHandler { + typealias FireRequest = ( + _ event: PixelKit.Event, + _ frequency: PixelKit.Frequency, + _ headers: [String: String], + _ parameters: [String: String]?, + _ error: Error?, + _ allowedQueryReservedCharacters: CharacterSet?, + _ includeAppVersionParameter: Bool, + _ onComplete: @escaping (Bool, Error?) -> Void + ) -> Void +} diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 07683d29cc..db79eac20f 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -581,4 +581,5 @@ extension URL { return false } } + } diff --git a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift index b26f411d9c..b9d05691e7 100644 --- a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift +++ b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift @@ -20,72 +20,29 @@ import Foundation import PixelKit /// A type that handles Pixels for acquisition attributions. -protocol AttributionsPixelHandler: AnyObject { +protocol InstallationAttributionsPixelHandler: AnyObject { /// Fire the Pixel to track the App install. func fireInstallationAttributionPixel() } -final class InstallationAttributionPixelHandler: AttributionsPixelHandler { - enum Parameters { - static let origin = "origin" - static let locale = "locale" - } - - private let fireRequest: FireRequest +final class AppInstallationAttributionPixelHandler: InstallationAttributionsPixelHandler { private let originProvider: AttributionOriginProvider - private let locale: Locale + private let decoratedAttributionPixelHandler: AttributionPixelHandler - /// Creates an instance with the specified fire request, origin provider and locale. - /// - Parameters: - /// - fireRequest: A function for sending the Pixel request. - /// - originProvider: A provider for the origin used to track the acquisition funnel. - /// - locale: The locale of the device. init( - fireRequest: @escaping FireRequest = PixelKit.fire, originProvider: AttributionOriginProvider = AttributionOriginFileProvider(), - locale: Locale = .current + attributionPixelHandler: AttributionPixelHandler = GenericAttributionPixelHandler() ) { - self.fireRequest = fireRequest self.originProvider = originProvider - self.locale = locale + decoratedAttributionPixelHandler = attributionPixelHandler } func fireInstallationAttributionPixel() { - fireRequest( - GeneralPixel.installationAttribution, - .legacyInitial, - [:], - additionalParameters(origin: originProvider.origin, locale: locale.identifier), - nil, - nil, - true, { _, _ in } + decoratedAttributionPixelHandler.fireAttributionPixel( + event: GeneralPixel.installationAttribution, + frequency: .legacyInitial, + origin: originProvider.origin, + additionalParameters: nil ) } } - -// MARK: - Parameter - -private extension InstallationAttributionPixelHandler { - func additionalParameters(origin: String?, locale: String) -> [String: String] { - var dictionary = [Self.Parameters.locale: locale] - if let origin { - dictionary[Self.Parameters.origin] = origin - } - return dictionary - } -} - -// MARK: - FireRequest - -extension InstallationAttributionPixelHandler { - typealias FireRequest = ( - _ event: PixelKit.Event, - _ frequency: PixelKit.Frequency, - _ headers: [String: String], - _ parameters: [String: String]?, - _ error: Error?, - _ allowedQueryReservedCharacters: CharacterSet?, - _ includeAppVersionParameter: Bool, - _ onComplete: @escaping (Bool, Error?) -> Void - ) -> Void -} diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 744f4b6071..84219ba446 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -30,14 +30,14 @@ final class StatisticsLoader { private let statisticsStore: StatisticsStore private let emailManager: EmailManager - private let attributionPixelHandler: AttributionsPixelHandler + private let attributionPixelHandler: InstallationAttributionsPixelHandler private let parser = AtbParser() private var isAppRetentionRequestInProgress = false init( statisticsStore: StatisticsStore = LocalStatisticsStore(), emailManager: EmailManager = EmailManager(), - attributionPixelHandler: AttributionsPixelHandler = InstallationAttributionPixelHandler() + attributionPixelHandler: InstallationAttributionsPixelHandler = AppInstallationAttributionPixelHandler() ) { self.statisticsStore = statisticsStore self.emailManager = emailManager diff --git a/DuckDuckGo/Statistics/PrivacyProPixel.swift b/DuckDuckGo/Statistics/PrivacyProPixel.swift index d50f06656a..a4093228f6 100644 --- a/DuckDuckGo/Statistics/PrivacyProPixel.swift +++ b/DuckDuckGo/Statistics/PrivacyProPixel.swift @@ -65,6 +65,7 @@ enum PrivacyProPixel: PixelKitEventV2 { case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval case privacyProPurchaseStripeSuccess + case privacyProSuccessfulSubscriptionAttribution // Web pixels case privacyProOfferMonthlyPriceClick case privacyProOfferYearlyPriceClick @@ -110,6 +111,7 @@ enum PrivacyProPixel: PixelKitEventV2 { case .privacyProSubscriptionManagementPlanBilling: return "m_mac_\(appDistribution)_privacy-pro_settings_change-plan-or-billing_click" case .privacyProSubscriptionManagementRemoval: return "m_mac_\(appDistribution)_privacy-pro_settings_remove-from-device_click" case .privacyProPurchaseStripeSuccess: return "m_mac_\(appDistribution)_privacy-pro_app_subscription-purchase_stripe_success" + case .privacyProSuccessfulSubscriptionAttribution: return "m_mac_\(appDistribution)_subscribe" // Web case .privacyProOfferMonthlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_monthly-price_click" case .privacyProOfferYearlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_yearly-price_click" diff --git a/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift b/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift new file mode 100644 index 0000000000..a6361d0427 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift @@ -0,0 +1,46 @@ +// +// SubscriptionAttributionPixelHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription + +protocol SubscriptionAttributionPixelHandler: AnyObject { + var origin: String? { get set } + func fireSuccessfulSubscriptionAttributionPixel() +} + +// MARK: - SubscriptionAttributionPixelHandler + +final class PrivacyProSubscriptionAttributionPixelHandler: SubscriptionAttributionPixelHandler { + var origin: String? + private let decoratedAttributionPixelHandler: AttributionPixelHandler + + init(attributionPixelHandler: AttributionPixelHandler = GenericAttributionPixelHandler()) { + decoratedAttributionPixelHandler = attributionPixelHandler + } + + func fireSuccessfulSubscriptionAttributionPixel() { + decoratedAttributionPixelHandler.fireAttributionPixel( + event: PrivacyProPixel.privacyProSuccessfulSubscriptionAttribution, + frequency: .standard, + origin: origin, + additionalParameters: nil + ) + } + +} diff --git a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift new file mode 100644 index 0000000000..66e6aefa04 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift @@ -0,0 +1,66 @@ +// +// SubscriptionRedirectManager.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription +import BrowserServicesKit + +protocol SubscriptionRedirectManager: AnyObject { + func redirectURL(for url: URL) -> URL? +} + +final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { + private let featureAvailabiltyProvider: () -> Bool + + init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable) { + self.featureAvailabiltyProvider = featureAvailabiltyProvider + } + + func redirectURL(for url: URL) -> URL? { + guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } + + if url.pathComponents == URL.privacyPro.pathComponents { + let isFeatureAvailable = featureAvailabiltyProvider() + let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts + // Redirect the `/pro` URL to `/subscriptions` URL. If there are any query items in the original URL it appends to the `/subscriptions` URL. + return isPurchasePageRedirectActive ? URL.subscriptionBaseURL.addingQueryItems(from: url) : nil + } + + return nil + } + +} + +private extension URL { + + func addingQueryItems(from url: URL) -> URL { + // If the origin value is of type "do+something" appending the percentEncodedQueryItem crashes the browser as + is replaced by a space. + // Perform encoding on the value to avoid the crash. + guard let queryItems = url.getQueryItems()? + .compactMap({ queryItem -> URLQueryItem? in + guard let value = queryItem.value else { return nil } + let encodedValue = value.percentEncoded(withAllowedCharacters: .urlQueryParameterAllowed) + return URLQueryItem(name: queryItem.name, value: encodedValue) + }) + else { return self } + + return self.appending(percentEncodedQueryItems: queryItems) + } + +} diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index d06cb5f3d3..81f400302c 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -18,13 +18,17 @@ import Navigation import Foundation -import Subscription -import BrowserServicesKit struct RedirectNavigationResponder: NavigationResponder { + private let redirectManager: SubscriptionRedirectManager + + init(redirectManager: SubscriptionRedirectManager = PrivacyProSubscriptionRedirectManager()) { + self.redirectManager = redirectManager + } + func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? { - guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectURL(for: navigationAction.url) else { return .next } + guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectManager.redirectURL(for: navigationAction.url) else { return .next } return .redirect(mainFrame) { navigator in var request = navigationAction.request @@ -33,17 +37,4 @@ struct RedirectNavigationResponder: NavigationResponder { } } - private func redirectURL(for url: URL) -> URL? { - guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } - - if url.pathComponents == URL.privacyPro.pathComponents { - let isFeatureAvailable = DefaultSubscriptionFeatureAvailability().isFeatureAvailable - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false - let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts - - return isPurchasePageRedirectActive ? URL.subscriptionBaseURL : nil - } - - return nil - } } diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index 4d9b38724b..f0e7525820 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -90,6 +90,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.broker = broker } + private let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + + init(subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { + self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + } + struct Handlers { static let getSubscription = "getSubscription" static let setSubscription = "setSubscription" @@ -208,7 +214,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // swiftlint:disable:next function_body_length cyclomatic_complexity func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) struct SubscriptionSelection: Decodable { let id: String @@ -216,6 +221,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let message = original + // Extract the origin from the webview URL to use for attribution pixel. + subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) + if SubscriptionPurchaseEnvironment.current == .appStore { if #available(macOS 12.0, *) { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController @@ -287,6 +295,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { os_log(.info, log: .subscription, "[Purchase] Purchase complete") PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .unique) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure(let error): switch error { @@ -420,6 +429,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.dismiss(progressViewController) PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() return [String: String]() // cannot be nil, the web app expect something back before redirecting the user to the final page } @@ -477,6 +487,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { broker.push(method: method.rawValue, params: params, for: self, into: webView) } + + @MainActor + private func originFrom(originalMessage: WKScriptMessage) -> String? { + let url = originalMessage.webView?.url + return url?.getParameter(named: AttributionParameter.origin) + } } extension MainWindowController { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 6b7ae3d3a1..a5b462fc1d 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: "145.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.2.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 2879531843..1088392164 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.2.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 2e126269fa..a8431e0cfe 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: "145.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.2.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Common/Pixel/PixelCapturedParameters.swift b/UnitTests/Common/Pixel/PixelCapturedParameters.swift new file mode 100644 index 0000000000..b73c5b38af --- /dev/null +++ b/UnitTests/Common/Pixel/PixelCapturedParameters.swift @@ -0,0 +1,31 @@ +// +// PixelCapturedParameters.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 + +struct PixelCapturedParameters { + var event: PixelKit.Event? + var frequency: PixelKit.Frequency = .standard + var headers: [String: String] = [:] + var parameters: [String: String]? + var error: Error? + var reservedCharacters: CharacterSet? + var includeAppVersion: Bool? + var onComplete: (Bool, Error?) -> Void = { _, _ in } +} diff --git a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift index 0d217cf769..df475b229b 100644 --- a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift +++ b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift @@ -21,13 +21,13 @@ import PixelKit @testable import DuckDuckGo_Privacy_Browser final class InstallationAttributionPixelHandlerTests: XCTestCase { - private var sut: InstallationAttributionPixelHandler! - private var capturedParams: CapturedParameters! - private var fireRequest: InstallationAttributionPixelHandler.FireRequest! + private var sut: AppInstallationAttributionPixelHandler! + private var capturedParams: PixelCapturedParameters! + private var fireRequest: GenericAttributionPixelHandler.FireRequest! override func setUpWithError() throws { try super.setUpWithError() - capturedParams = CapturedParameters() + capturedParams = PixelCapturedParameters() fireRequest = { event, frequency, headers, parameters, error, reservedCharacters, includeAppVersion, onComplete in self.capturedParams.event = event self.capturedParams.frequency = frequency @@ -49,7 +49,8 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { func testWhenPixelFiresThenNameIsSetToM_Mac_Install() { // GIVEN - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() @@ -61,13 +62,14 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { func testWhenPixelFiresThenLanguageCodeIsSet() { // GIVEN let locale = Locale(identifier: "hu-HU") - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: locale) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "hu-HU") + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "hu-HU") } func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { @@ -75,14 +77,15 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { let origin = "app_search" let locale = Locale(identifier: "en-US") let originProvider = MockAttributionOriginProvider(origin: origin) - sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: originProvider, attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin], origin) - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") } func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { @@ -90,19 +93,20 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { let origin: String? = nil let locale = Locale(identifier: "en-US") let originProvider = MockAttributionOriginProvider(origin: origin) - sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) - + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: originProvider, attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertNil(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin]) - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + XCTAssertNil(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") } func testWhenPixelFiresThenAddAppVersionIsTrueAndFrequencyIsLegacyInitial() { // GIVEN - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() @@ -112,18 +116,3 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { XCTAssertEqual(capturedParams.frequency, .legacyInitial) } } - -extension InstallationAttributionPixelHandlerTests { - - struct CapturedParameters { - var event: PixelKit.Event? - var frequency: PixelKit.Frequency = .standard - var headers: [String: String] = [:] - var parameters: [String: String]? - var error: Error? - var reservedCharacters: CharacterSet? - var includeAppVersion: Bool? - var onComplete: (Bool, Error?) -> Void = { _, _ in } - } - -} diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift index 05e91bff49..bd07f8f56d 100644 --- a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription @testable import DuckDuckGo_Privacy_Browser final class MockAttributionOriginProvider: AttributionOriginProvider { diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift index a32ee60a30..cc6aa508a1 100644 --- a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift @@ -19,7 +19,7 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser -final class MockAttributionsPixelHandler: AttributionsPixelHandler { +final class MockAttributionsPixelHandler: InstallationAttributionsPixelHandler { private(set) var fireInstallationAttributionPixelCount = 0 private(set) var didCallFireInstallationAttributionPixel = false diff --git a/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift b/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift new file mode 100644 index 0000000000..8bc003fe07 --- /dev/null +++ b/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift @@ -0,0 +1,124 @@ +// +// SubscriptionAttributionPixelHandlerTests.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 PixelKit +@testable import DuckDuckGo_Privacy_Browser + +final class SubscriptionAttributionPixelHandlerTests: XCTestCase { + private var sut: PrivacyProSubscriptionAttributionPixelHandler! + private var capturedParams: PixelCapturedParameters! + private var fireRequest: GenericAttributionPixelHandler.FireRequest! + + override func setUpWithError() throws { + try super.setUpWithError() + capturedParams = PixelCapturedParameters() + fireRequest = { event, frequency, headers, parameters, error, reservedCharacters, includeAppVersion, onComplete in + self.capturedParams.event = event + self.capturedParams.frequency = frequency + self.capturedParams.headers = headers + self.capturedParams.parameters = parameters + self.capturedParams.error = error + self.capturedParams.reservedCharacters = reservedCharacters + self.capturedParams.includeAppVersion = includeAppVersion + self.capturedParams.onComplete = onComplete + } + } + + override func tearDownWithError() throws { + capturedParams = nil + fireRequest = nil + sut = nil + try super.tearDownWithError() + } + + func testWhenPixelFiresThenNameIsSetToM_Mac_DistributionType_Subscribe() { + // GIVEN + #if APPSTORE + let expectedPixelName = "m_mac_store_subscribe" + #else + let expectedPixelName = "m_mac_direct_subscribe" + #endif + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.event?.name, expectedPixelName) + } + + func testWhenPixelFiresThenLanguageCodeIsSet() { + // GIVEN + let locale = Locale(identifier: "hu-HU") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "hu-HU") + } + + func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { + // GIVEN + let origin = "app_search" + let locale = Locale(identifier: "en-US") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + sut.origin = origin + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { + // GIVEN + let origin: String? = nil + let locale = Locale(identifier: "en-US") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + sut.origin = origin + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertNil(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresThenAddAppVersionIsTrueAndFrequencyIsStandard() { + // GIVEN + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.includeAppVersion, true) + XCTAssertEqual(capturedParams.frequency, .standard) + } +} diff --git a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift new file mode 100644 index 0000000000..814abc7a42 --- /dev/null +++ b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift @@ -0,0 +1,61 @@ +// +// SubscriptionRedirectManagerTests.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 Subscription +@testable import DuckDuckGo_Privacy_Browser + +final class SubscriptionRedirectManagerTests: XCTestCase { + private var sut: PrivacyProSubscriptionRedirectManager! + + override func setUpWithError() throws { + try super.setUpWithError() + sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true) + SubscriptionPurchaseEnvironment.canPurchase = true + } + + override func tearDownWithError() throws { + sut = nil + try super.tearDownWithError() + } + + func testWhenURLIsPrivacyProAndHasOriginQueryParameterThenRedirectToSubscriptionBaseURLAndAppendQueryParameter() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro?origin=test")) + let expectedURL = URL.subscriptionBaseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) + + // WHEN + let result = sut.redirectURL(for: url) + + // THEN + XCTAssertEqual(result, expectedURL) + } + + func testWhenURLIsPrivacyProAndDoesNotHaveOriginQueryParameterThenRedirectToSubscriptionBaseURL() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro")) + let expectedURL = URL.subscriptionBaseURL + + // WHEN + let result = sut.redirectURL(for: url) + + // THEN + XCTAssertEqual(result, expectedURL) + } + +} From b601c00fc1c42bed45809e702ec31c997b97ee68 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 16 May 2024 11:53:18 +1000 Subject: [PATCH 116/134] Web Inspector focus for new tabs (#2775) Task/Issue URL: https://app.asana.com/0/1177771139624306/1204065533298601/f **Description**: Fix an issue that causes the web inspector to gain focus when a new tab is open. --- DuckDuckGo/Tab/View/WebView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Tab/View/WebView.swift b/DuckDuckGo/Tab/View/WebView.swift index 7de19a7c13..8c9cd2d1f2 100644 --- a/DuckDuckGo/Tab/View/WebView.swift +++ b/DuckDuckGo/Tab/View/WebView.swift @@ -38,6 +38,12 @@ final class WebView: WKWebView { private var isLoadingObserver: Any? + private var shouldShowWebInspector: Bool { + // When a new tab is open, we don't want the web inspector to be active on screen and gain focus. + // When a new tab is open the other tab views are removed from the window, hence, we should not show the web inspector. + isInspectorShown && window != nil + } + override func addTrackingArea(_ trackingArea: NSTrackingArea) { /// disable mouseEntered/mouseMoved/mouseExited events passing to Web View while it‘s loading /// see https://app.asana.com/0/1177771139624306/1206990108527681/f @@ -163,8 +169,8 @@ final class WebView: WKWebView { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() - if self.isInspectorShown { - self.openDeveloperTools() + if shouldShowWebInspector { + openDeveloperTools() } } From 767ae9d1f774df7d73a5a4d3eb2c1a756d2a0365 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 16 May 2024 12:07:25 +1000 Subject: [PATCH 117/134] Update Bookmark this page to Delete bookmark when url is already bookmarked (#2774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1203352129640292/f **Description**: This PR changes the behaviour by showing “Delete bookmark” if a URL is bookmarked on the Tab contextual menu. --- .../View/AddBookmarkPopoverView.swift | 2 +- DuckDuckGo/Common/Localizables/UserText.swift | 1 + DuckDuckGo/Localizable.xcstrings | 60 +++++++++++++++++ .../Model/PinnedTabsViewModel.swift | 18 +++++- .../PinnedTabs/View/PinnedTabView.swift | 21 ++++-- .../TabBar/View/TabBarViewController.swift | 64 +++++++++++++++++-- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 18 +++++- .../HomePage/Mocks/MockBookmarkManager.swift | 3 +- .../PinnedTabs/PinnedTabsViewModelTests.swift | 34 +++++++++- .../TabBar/View/MockTabViewItemDelegate.swift | 11 ++++ .../TabBar/View/TabBarViewItemTests.swift | 51 ++++++++++++++- 11 files changed, 263 insertions(+), 20 deletions(-) diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index 1faa240398..bcef29a4c5 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -48,7 +48,7 @@ struct AddBookmarkPopoverView: View { selectedFolder: $model.selectedFolder, isURLFieldHidden: true, addFolderAction: model.addFolderButtonAction, - otherActionTitle: UserText.remove, + otherActionTitle: UserText.delete, isOtherActionDisabled: false, otherAction: model.removeButtonAction, defaultActionTitle: UserText.done, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b7ace49675..a3ee25f1f8 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -29,6 +29,7 @@ struct UserText { static let cancel = NSLocalizedString("cancel", value: "Cancel", comment: "Cancel button") static let notNow = NSLocalizedString("notnow", value: "Not Now", comment: "Not Now button") static let remove = NSLocalizedString("generic.remove.button", value: "Remove", comment: "Label of a button that allows the user to remove an item") + static let delete = NSLocalizedString("generic.delete.button", value: "Delete", comment: "Label of a button that allows the user to delete an item") static let discard = NSLocalizedString("generic.discard.button", value: "Discard", comment: "Label of a button that allows the user discard an action/change") static let neverForThisSite = NSLocalizedString("never.for.this.site", value: "Never Ask for This Site", comment: "Never ask to save login credentials for this site button") static let open = NSLocalizedString("open", value: "Open", comment: "Open button") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 673ca12212..caa202fc24 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -20715,6 +20715,66 @@ } } }, + "generic.delete.button" : { + "comment" : "Label of a button that allows the user to delete an item", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Delete" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elimina" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijderen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить" + } + } + } + }, "generic.discard.button" : { "comment" : "Label of a button that allows the user discard an action/change", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 7324862828..5c77dfdf96 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -83,10 +83,15 @@ final class PinnedTabsViewModel: ObservableObject { // MARK: - - init(collection: TabCollection, fireproofDomains: FireproofDomains = .shared) { + init( + collection: TabCollection, + fireproofDomains: FireproofDomains = .shared, + bookmarkManager: BookmarkManager = LocalBookmarkManager.shared + ) { tabsDidReorderPublisher = tabsDidReorderSubject.eraseToAnyPublisher() contextMenuActionPublisher = contextMenuActionSubject.eraseToAnyPublisher() self.fireproofDomains = fireproofDomains + self.bookmarkManager = bookmarkManager tabsCancellable = collection.$tabs.assign(to: \.items, onWeaklyHeld: self) dragMovesWindowCancellable = $items @@ -100,6 +105,7 @@ final class PinnedTabsViewModel: ObservableObject { private var tabsCancellable: AnyCancellable? private var dragMovesWindowCancellable: AnyCancellable? private var fireproofDomains: FireproofDomains + private var bookmarkManager: BookmarkManager private func updateItemsWithoutSeparator() { var items = Set() @@ -136,6 +142,7 @@ extension PinnedTabsViewModel { case unpin(Int) case duplicate(Int) case bookmark(Tab) + case removeBookmark(Tab) case fireproof(Tab) case removeFireproofing(Tab) case close(Int) @@ -179,10 +186,19 @@ extension PinnedTabsViewModel { contextMenuActionSubject.send(.close(index)) } + func isPinnedTabBookmarked(_ tab: Tab) -> Bool { + guard let url = tab.url else { return false } + return bookmarkManager.isUrlBookmarked(url: url) + } + func bookmark(_ tab: Tab) { contextMenuActionSubject.send(.bookmark(tab)) } + func removeBookmark(_ tab: Tab) { + contextMenuActionSubject.send(.removeBookmark(tab)) + } + func fireproof(_ tab: Tab) { contextMenuActionSubject.send(.fireproof(tab)) } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index c9a0c87f8b..7bc278f80e 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -89,11 +89,7 @@ struct PinnedTabView: View { collectionModel?.unpin(model) } Divider() - Button(UserText.bookmarkThisPage) { [weak collectionModel, weak model] in - guard let model = model else { return } - collectionModel?.bookmark(model) - } - + bookmarkAction fireproofAction Divider() switch collectionModel.audioStateView { @@ -127,6 +123,21 @@ struct PinnedTabView: View { } } } + + @ViewBuilder + private var bookmarkAction: some View { + if collectionModel.isPinnedTabBookmarked(model) { + Button(UserText.deleteBookmark) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.removeBookmark(model) + } + } else { + Button(UserText.bookmarkThisPage) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.bookmark(model) + } + } + } } private struct BorderView: View { diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index b2d4be517a..f1e18cea7d 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -23,6 +23,8 @@ import Lottie import SwiftUI import WebKit +// swiftlint:disable file_length + final class TabBarViewController: NSViewController { enum HorizontalSpace: CGFloat { @@ -309,6 +311,12 @@ final class TabBarViewController: NSViewController { 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") + return + } + deleteBookmark(with: url) case let .fireproof(tab): fireproof(tab) case let .removeFireproofing(tab): @@ -773,6 +781,14 @@ 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) + return + } + bookmarkManager.remove(bookmark: bookmark) + } + 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) @@ -790,8 +806,25 @@ extension TabBarViewController: TabCollectionViewModelDelegate { FireproofDomains.shared.remove(domain: host) } + + // MARK: - TabViewItem + + func urlAndTitle(for tabBarViewItem: TabBarViewItem) -> (url: URL, title: String)? { + guard + let indexPath = collectionView.indexPath(for: tabBarViewItem), + 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) + return nil + } + + return (url, tabViewModel.title) + } } +// MARK: - NSCollectionViewDelegateFlowLayout + extension TabBarViewController: NSCollectionViewDelegateFlowLayout { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { @@ -801,6 +834,8 @@ extension TabBarViewController: NSCollectionViewDelegateFlowLayout { } +// MARK: - NSCollectionViewDataSource + extension TabBarViewController: NSCollectionViewDataSource { func numberOfSections(in collectionView: NSCollectionView) -> Int { @@ -854,6 +889,8 @@ extension TabBarViewController: NSCollectionViewDataSource { } } +// MARK: - NSCollectionViewDelegate + extension TabBarViewController: NSCollectionViewDelegate { func collectionView(_ collectionView: NSCollectionView, @@ -978,6 +1015,8 @@ extension TabBarViewController: NSCollectionViewDelegate { } +// MARK: - TabBarViewItemDelegate + extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, isMouseOver: Bool) { @@ -1038,15 +1077,22 @@ extension TabBarViewController: TabBarViewItemDelegate { return tabCollectionViewModel.tabViewModel(at: indexPath.item)?.tab.content.canBeBookmarked ?? false } + func tabBarViewItemIsAlreadyBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { + guard let url = urlAndTitle(for: tabBarViewItem)?.url else { return false } + + return bookmarkManager.isUrlBookmarked(url: url) + } + func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) { - guard let indexPath = collectionView.indexPath(for: tabBarViewItem), - 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) - return - } + guard let (url, title) = urlAndTitle(for: tabBarViewItem) else { return } + + bookmarkTab(with: url, title: title) + } + + func tabBarViewItemRemoveBookmarkAction(_ tabBarViewItem: TabBarViewItem) { + guard let url = urlAndTitle(for: tabBarViewItem)?.url else { return } - bookmarkTab(with: url, title: tabViewModel.title) + deleteBookmark(with: url) } func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { @@ -1180,6 +1226,8 @@ extension TabBarViewController: TabBarViewItemDelegate { } +// MARK: - TabBarViewItemPasteboardWriter + final class TabBarViewItemPasteboardWriter: NSObject, NSPasteboardWriting { static let utiInternalType = NSPasteboard.PasteboardType(rawValue: "com.duckduckgo.tab.internal") @@ -1193,3 +1241,5 @@ final class TabBarViewItemPasteboardWriter: NSObject, NSPasteboardWriting { } } + +// swiftlint:enable file_length diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index 18042eb73f..4acca3216c 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -33,6 +33,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool + func tabBarViewItemIsAlreadyBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) @@ -43,6 +44,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemDuplicateAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemPinAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) + func tabBarViewItemRemoveBookmarkAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewBurnerWindowAction(_ tabBarViewItem: TabBarViewItem) @@ -199,6 +201,10 @@ final class TabBarViewItem: NSCollectionViewItem { delegate?.tabBarViewItemBookmarkThisPageAction(self) } + @objc func removeFromBookmarksAction(_ sender: Any) { + delegate?.tabBarViewItemRemoveBookmarkAction(self) + } + @objc func bookmarkAllOpenTabsAction(_ sender: Any) { delegate?.tabBarViewItemBookmarkAllOpenTabsAction(self) } @@ -502,7 +508,11 @@ extension TabBarViewItem: NSMenuDelegate { // Section 2 addFireproofMenuItem(to: menu) - addBookmarkMenuItem(to: menu) + if let delegate, delegate.tabBarViewItemIsAlreadyBookmarked(self) { + removeBookmarkMenuItem(to: menu) + } else { + addBookmarkMenuItem(to: menu) + } menu.addItem(.separator()) // Section 3 @@ -538,6 +548,12 @@ extension TabBarViewItem: NSMenuDelegate { menu.addItem(bookmarkMenuItem) } + private func removeBookmarkMenuItem(to menu: NSMenu) { + let bookmarkMenuItem = NSMenuItem(title: UserText.deleteBookmark, action: #selector(removeFromBookmarksAction(_:)), keyEquivalent: "") + bookmarkMenuItem.target = self + menu.addItem(bookmarkMenuItem) + } + private func addBookmarkAllTabsMenuItem(to menu: NSMenu) { let bookmarkMenuItem = NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(bookmarkAllOpenTabsAction(_:)), keyEquivalent: "") bookmarkMenuItem.target = self diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index d1fafb18be..cf0c87be4d 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -25,8 +25,9 @@ class MockBookmarkManager: BookmarkManager { return false } + var isUrlBookmarked = false func isUrlBookmarked(url: URL) -> Bool { - return false + return isUrlBookmarked } func allHosts() -> Set { diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index c9f063d54f..f54376f4df 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -31,6 +31,7 @@ class PinnedTabsViewModelTests: XCTestCase { var model: PinnedTabsViewModel! var collection: TabCollection! + var bookmarkManagerMock: MockBookmarkManager! override func setUpWithError() throws { try super.setUpWithError() @@ -41,7 +42,8 @@ class PinnedTabsViewModelTests: XCTestCase { Tab(content: .url("http://d.com".url!, source: .link)), Tab(content: .url("http://e.com".url!, source: .link)) ]) - model = PinnedTabsViewModel(collection: collection) + bookmarkManagerMock = .init() + model = PinnedTabsViewModel(collection: collection, bookmarkManager: bookmarkManagerMock) } func testInitialState() throws { @@ -150,10 +152,11 @@ class PinnedTabsViewModelTests: XCTestCase { model.removeFireproofing(tabB) model.close(tabA) model.muteOrUmute(tabB) + model.removeBookmark(tabA) cancellable.cancel() - XCTAssertEqual(events.count, 7) + XCTAssertEqual(events.count, 8) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], @@ -161,13 +164,38 @@ class PinnedTabsViewModelTests: XCTestCase { case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], case .close(0) = events[5], - case .muteOrUnmute(tabB) = events[6] + case .muteOrUnmute(tabB) = events[6], + case .removeBookmark(tabA) = events[7] else { XCTFail("Incorrect context menu action") return } } + func testWhenIsPinnedTabBookmarkedCalledAndURLIsBookmarkedThenReturnTrue() { + // GIVEN + bookmarkManagerMock.isUrlBookmarked = true + let tab = Tab(content: .url(URL.duckDuckGo, source: .link)) + + // WHEN + let result = model.isPinnedTabBookmarked(tab) + + // THEN + XCTAssertTrue(result) + } + + func testWhenIsPinnedTabBookmarkedCalledAndURLIsNotBookmarkedThenReturnFalse() { + // GIVEN + bookmarkManagerMock.isUrlBookmarked = false + let tab = Tab(content: .url(URL.duckDuckGo, source: .link)) + + // WHEN + let result = model.isPinnedTabBookmarked(tab) + + // THEN + XCTAssertFalse(result) + } + } private extension Array where Element == Tab { diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 63b4a7fc97..680f840d9c 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -27,7 +27,10 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheLeft = false var hasItemsToTheRight = false var audioState: WKWebView.AudioState? + var isTabBarItemAlreadyBookmarked = false + private(set) var tabBarViewItemBookmarkThisPageActionCalled = false + private(set) var tabBarViewItemRemoveBookmarkActionCalled = false private(set) var tabBarViewItemBookmarkAllOpenTabsActionCalled = false func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -74,8 +77,16 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { mockedCurrentTab?.content.canBeBookmarked ?? true } + func tabBarViewItemIsAlreadyBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { + isTabBarItemAlreadyBookmarked + } + func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { + tabBarViewItemBookmarkThisPageActionCalled = true + } + func tabBarViewItemRemoveBookmarkAction(_ tabBarViewItem: TabBarViewItem) { + tabBarViewItemRemoveBookmarkActionCalled = true } func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 19505467d6..975116e54b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -46,7 +46,6 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 1)?.title, UserText.pinTab) XCTAssertTrue(menu.item(at: 2)?.isSeparatorItem ?? false) XCTAssertEqual(menu.item(at: 3)?.title, UserText.fireproofSite) - XCTAssertEqual(menu.item(at: 4)?.title, UserText.bookmarkThisPage) XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) XCTAssertEqual(menu.item(at: 6)?.title, UserText.bookmarkAllTabs) XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) @@ -82,6 +81,28 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertTrue(menu.item(at: 3)?.isSeparatorItem ?? false) } + func testWhenURLIsNotBookmarkedThenBookmarkThisPageIsShown() { + // GIVEN + delegate.isTabBarItemAlreadyBookmarked = false + + // WHEN + tabBarViewItem.menuNeedsUpdate(menu) + + // THEN + XCTAssertEqual(menu.item(at: 4)?.title, UserText.bookmarkThisPage) + } + + func testWhenURLIsBookmarkedThenDeleteBookmarkIsShown() { + // GIVEN + delegate.isTabBarItemAlreadyBookmarked = true + + // WHEN + tabBarViewItem.menuNeedsUpdate(menu) + + // THEN + XCTAssertEqual(menu.item(at: 4)?.title, UserText.deleteBookmark) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) @@ -259,4 +280,32 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertTrue(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) } + func testWhenClickingOnBookmarkThisPageThenTheActionDelegateIsNotified() throws { + // GIVEN + delegate.isTabBarItemAlreadyBookmarked = false + tabBarViewItem.menuNeedsUpdate(menu) + let index = try XCTUnwrap(menu.indexOfItem(withTitle: UserText.bookmarkThisPage)) + XCTAssertFalse(delegate.tabBarViewItemBookmarkThisPageActionCalled) + + // WHEN + menu.performActionForItem(at: index) + + // THEN + XCTAssertTrue(delegate.tabBarViewItemBookmarkThisPageActionCalled) + } + + func testWhenClickingOnDeleteBookmarkThenTheActionDelegateIsNotified() throws { + // GIVEN + delegate.isTabBarItemAlreadyBookmarked = true + tabBarViewItem.menuNeedsUpdate(menu) + let index = try XCTUnwrap(menu.indexOfItem(withTitle: UserText.deleteBookmark)) + XCTAssertFalse(delegate.tabBarViewItemRemoveBookmarkActionCalled) + + // WHEN + menu.performActionForItem(at: index) + + // THEN + XCTAssertTrue(delegate.tabBarViewItemRemoveBookmarkActionCalled) + } + } From 7c85aa0fea24d3c0196b9a42b3b7dd8d1e6ec124 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 16 May 2024 12:58:38 +0200 Subject: [PATCH 118/134] Add controller VPN uninstall pixels (#2781) Task/Issue URL: https://app.asana.com/0/1206580121312550/1207324076451291/f iOS PR: https://github.com/duckduckgo/iOS/pull/2857 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/822 ## Description Adds VPN uninstall pixels to the controller. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../NetworkProtectionDebugUtilities.swift | 4 +- .../NetworkProtectionTunnelController.swift | 4 +- DuckDuckGo/Waitlist/VPNUninstaller.swift | 56 ++++---- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 84 +++++++----- .../TunnelControllerIPCService.swift | 18 ++- DuckDuckGoVPN/VPNUninstaller.swift | 126 +++++++++++++++--- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../TunnelControllerIPCClient.swift | 4 +- .../TunnelControllerIPCServer.swift | 14 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 13 files changed, 218 insertions(+), 104 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 220f975915..8c42e67d5c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12902,7 +12902,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.2.0; + version = 145.3.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 415ca34871..6ed1817498 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" : "c69a664b58cb351ccb73aa548726854a2861c0ed", - "version" : "145.2.0" + "revision" : "b901a47ad442356b0953ffdb768ea1fae47fe912", + "version" : "145.3.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index a3b56f0970..cf810d9225 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -72,10 +72,10 @@ final class NetworkProtectionDebugUtilities { } func sendTestNotificationRequest() async throws { - try await ipcClient.debugCommand(.sendTestNotification) + try await ipcClient.command(.sendTestNotification) } func expireRegistrationKeyNow() async throws { - try await ipcClient.debugCommand(.expireRegistrationKey) + try await ipcClient.command(.expireRegistrationKey) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index ed20ffef1d..9bf6853f9a 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -294,13 +294,13 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Debug Command support - func relay(_ command: DebugCommand) async throws { + func relay(_ command: VPNCommand) async throws { guard await isConnected, let session = await session else { return } - let errorMessage: ExtensionMessageString? = try await session.sendProviderRequest(.debugCommand(command)) + let errorMessage: ExtensionMessageString? = try await session.sendProviderRequest(.command(command)) if let errorMessage { throw TunnelFailureError(errorDescription: errorMessage.value) } diff --git a/DuckDuckGo/Waitlist/VPNUninstaller.swift b/DuckDuckGo/Waitlist/VPNUninstaller.swift index 7b95886092..0de31fcf6a 100644 --- a/DuckDuckGo/Waitlist/VPNUninstaller.swift +++ b/DuckDuckGo/Waitlist/VPNUninstaller.swift @@ -35,6 +35,11 @@ final class VPNUninstaller: VPNUninstalling { enum UninstallCancellationReason: String { case alreadyUninstalling case alreadyUninstalled + case sysexInstallationCancelled + + /// The user was asked for login / pwd or touchID and cancelled + /// + case sysexInstallationRequiresAuthorization } enum UninstallError: CustomNSError { @@ -42,6 +47,7 @@ final class VPNUninstaller: VPNUninstalling { case runAgentError(_ error: Error) case systemExtensionError(_ error: Error) case vpnConfigurationError(_ error: Error) + case uninstallError(_ error: Error) var errorCode: Int { switch self { @@ -49,6 +55,7 @@ final class VPNUninstaller: VPNUninstalling { case .runAgentError: return 1 case .systemExtensionError: return 2 case .vpnConfigurationError: return 3 + case .uninstallError: return 4 } } @@ -58,7 +65,8 @@ final class VPNUninstaller: VPNUninstalling { return ["reason": reason.rawValue] case .runAgentError(let error), .systemExtensionError(let error), - .vpnConfigurationError(let error): + .vpnConfigurationError(let error), + .uninstallError(let error): return [NSUnderlyingErrorKey: error as NSError] } } @@ -174,30 +182,20 @@ final class VPNUninstaller: VPNUninstalling { } // Allow some time for the login items to fully launch - try? await Task.sleep(interval: 0.5) - if removeSystemExtension { - do { - try await self.removeSystemExtension() - } catch { - throw UninstallError.systemExtensionError(error) - } - } - - var attemptNumber = 1 - while attemptNumber <= 3 { - do { - try await removeVPNConfiguration() - break // Removal succeeded, break out of the while loop and continue with the rest of uninstallation - } catch { - print("Failed to remove VPN configuration, with error: \(error.localizedDescription)") - - if attemptNumber == 3 { - throw UninstallError.vpnConfigurationError(error) - } + do { + try await ipcClient.command(.uninstallVPN) + } catch { + print("Failed to uninstall VPN, with error: \(error.localizedDescription)") + + switch error { + case OSSystemExtensionError.requestCanceled: + throw UninstallError.cancelled(reason: .sysexInstallationCancelled) + case OSSystemExtensionError.authorizationRequired: + throw UninstallError.cancelled(reason: .sysexInstallationRequiresAuthorization) + default: + throw UninstallError.uninstallError(error) } - - attemptNumber += 1 } // We want to give some time for the login item to reset state before disabling it @@ -225,7 +223,11 @@ final class VPNUninstaller: VPNUninstalling { func removeSystemExtension() async throws { #if NETP_SYSTEM_EXTENSION - try await ipcClient.debugCommand(.removeSystemExtension) + do { + try await ipcClient.command(.removeSystemExtension) + } catch { + throw UninstallError.systemExtensionError(error) + } #endif } @@ -235,7 +237,11 @@ final class VPNUninstaller: VPNUninstalling { private func removeVPNConfiguration() async throws { // Remove the agent VPN configuration - try await ipcClient.debugCommand(.removeVPNConfiguration) + do { + try await ipcClient.command(.removeVPNConfiguration) + } catch { + throw UninstallError.vpnConfigurationError(error) + } } private func resetUserDefaults(uninstallSystemExtension: Bool) { diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index b19518730d..3f277460e5 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -43,6 +43,8 @@ final class DuckDuckGoVPNApplication: NSApplication { } super.init() + + setupPixelKit() self.delegate = _delegate #if DEBUG @@ -59,6 +61,41 @@ final class DuckDuckGoVPNApplication: NSApplication { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + @MainActor + private func setupPixelKit() { + let dryRun: Bool + +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + let pixelSource: String + +#if NETP_SYSTEM_EXTENSION + pixelSource = "vpnAgent" +#else + pixelSource = "vpnAgentAppStore" +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: pixelSource, + defaultHeaders: [:], + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } + } } @main @@ -160,6 +197,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private lazy var tunnelControllerIPCService: TunnelControllerIPCService = { let ipcServer = TunnelControllerIPCService( tunnelController: tunnelController, + uninstaller: vpnUninstaller, networkExtensionController: networkExtensionController, statusReporter: statusReporter) ipcServer.activate() @@ -204,8 +242,11 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { VPNAppEventsHandler(tunnelController: tunnelController) }() + @MainActor private lazy var vpnUninstaller: VPNUninstaller = { - VPNUninstaller(networkExtensionController: networkExtensionController, vpnConfigurationManager: VPNConfigurationManager()) + VPNUninstaller( + tunnelController: tunnelController, + networkExtensionController: networkExtensionController) }() /// The status bar NetworkProtection menu @@ -262,7 +303,14 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { locationFormatter: DefaultVPNLocationFormatter(), uninstallHandler: { [weak self] in guard let self else { return } - await self.vpnUninstaller.uninstall(includingSystemExtension: true) + + do { + try await self.vpnUninstaller.uninstall(includingSystemExtension: true) + exit(EXIT_SUCCESS) + } catch { + // Intentional no-op: we already track VPN uninstallation failures using + // pixels within the vpn uninstaller. + } } ) } @@ -285,38 +333,6 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { _ = tunnelControllerIPCService _ = vpnProxyLauncher - let dryRun: Bool - -#if DEBUG - dryRun = true -#else - dryRun = false -#endif - - let pixelSource: String - -#if NETP_SYSTEM_EXTENSION - pixelSource = "vpnAgent" -#else - pixelSource = "vpnAgentAppStore" -#endif - - PixelKit.setUp(dryRun: dryRun, - appVersion: AppVersion.shared.versionNumber, - source: pixelSource, - defaultHeaders: [:], - defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in - - let url = URL.pixelUrl(forPixelNamed: pixelName) - let apiHeaders = APIRequest.Headers(additionalHeaders: headers) // workaround - Pixel class should really handle APIRequest.Headers by itself - let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) - let request = APIRequest(configuration: configuration) - - request.fetch { _, error in - onComplete(error == nil, error) - } - } - vpnAppEventsHandler.appDidFinishLaunching() let launchInformation = LoginItemLaunchInformation(agentBundleID: Bundle.main.bundleIdentifier!, defaults: .netP) diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index b88c3f20b8..2b23d6c658 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -31,17 +31,20 @@ import NetworkProtectionUI final class TunnelControllerIPCService { private let tunnelController: NetworkProtectionTunnelController private let networkExtensionController: NetworkExtensionController + private let uninstaller: VPNUninstalling private let server: NetworkProtectionIPC.TunnelControllerIPCServer private let statusReporter: NetworkProtectionStatusReporter private var cancellables = Set() private let defaults: UserDefaults init(tunnelController: NetworkProtectionTunnelController, + uninstaller: VPNUninstalling, networkExtensionController: NetworkExtensionController, statusReporter: NetworkProtectionStatusReporter, defaults: UserDefaults = .netP) { self.tunnelController = tunnelController + self.uninstaller = uninstaller self.networkExtensionController = networkExtensionController server = .init(machServiceName: Bundle.main.bundleIdentifier!) self.statusReporter = statusReporter @@ -144,15 +147,12 @@ extension TunnelControllerIPCService: IPCServerInterface { try? await networkExtensionController.deactivateSystemExtension() } - func debugCommand(_ command: DebugCommand) async throws { + func command(_ command: VPNCommand) async throws { try await tunnelController.relay(command) switch command { case .removeSystemExtension: -#if NETP_SYSTEM_EXTENSION - try await networkExtensionController.deactivateSystemExtension() - defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) -#endif + try await uninstaller.removeSystemExtension() case .expireRegistrationKey: // Intentional no-op: handled by the extension break @@ -160,11 +160,9 @@ extension TunnelControllerIPCService: IPCServerInterface { // Intentional no-op: handled by the extension break case .removeVPNConfiguration: - await VPNConfigurationManager().removeVPNConfiguration() - - if defaults.networkProtectionOnboardingStatus == .completed { - defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) - } + try await uninstaller.removeVPNConfiguration() + case .uninstallVPN: + try await uninstaller.uninstall(includingSystemExtension: true) case .disableConnectOnDemandAndShutDown: // Not implemented on macOS yet break diff --git a/DuckDuckGoVPN/VPNUninstaller.swift b/DuckDuckGoVPN/VPNUninstaller.swift index 6b2cabde84..2d675fbee4 100644 --- a/DuckDuckGoVPN/VPNUninstaller.swift +++ b/DuckDuckGoVPN/VPNUninstaller.swift @@ -19,41 +19,135 @@ import Foundation import NetworkProtection import NetworkProtectionIPC +import PixelKit +import SystemExtensions protocol VPNUninstalling { - func uninstall(includingSystemExtension: Bool) async + func uninstall(includingSystemExtension: Bool) async throws + func removeSystemExtension() async throws + func removeVPNConfiguration() async throws } +@MainActor final class VPNUninstaller: VPNUninstalling { - let networkExtensionController: NetworkExtensionController - let vpnConfiguration: VPNConfigurationManager - let defaults: UserDefaults - init(networkExtensionController: NetworkExtensionController, vpnConfigurationManager: VPNConfigurationManager, defaults: UserDefaults = .netP) { + private let tunnelController: NetworkProtectionTunnelController + private let networkExtensionController: NetworkExtensionController + private let defaults: UserDefaults + private let pixelKit: PixelFiring? + + init(tunnelController: NetworkProtectionTunnelController, + networkExtensionController: NetworkExtensionController, + defaults: UserDefaults = .netP, + pixelKit: PixelFiring? = PixelKit.shared) { + + self.tunnelController = tunnelController self.networkExtensionController = networkExtensionController - self.vpnConfiguration = vpnConfigurationManager self.defaults = defaults + self.pixelKit = pixelKit } - func uninstall(includingSystemExtension: Bool) async { -#if NETP_SYSTEM_EXTENSION - if includingSystemExtension { - do { - try await networkExtensionController.deactivateSystemExtension() - defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) - } catch { + func uninstall(includingSystemExtension: Bool) async throws { + pixelKit?.fire(VPNUninstallAttempt.begin) + + do { + try await removeSystemExtension() + try await removeVPNConfiguration() + if defaults.networkProtectionOnboardingStatus == .completed { + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) } + + defaults.networkProtectionShouldShowVPNUninstalledMessage = true + pixelKit?.fire(VPNUninstallAttempt.success, frequency: .dailyAndCount) + } catch { + switch error { + case OSSystemExtensionError.requestCanceled: + pixelKit?.fire(VPNUninstallAttempt.cancelled(.sysexInstallationCancelled), frequency: .dailyAndCount) + case OSSystemExtensionError.authorizationRequired: + pixelKit?.fire(VPNUninstallAttempt.cancelled(.sysexInstallationRequiresAuthorization), frequency: .dailyAndCount) + default: + pixelKit?.fire(VPNUninstallAttempt.failure(error), frequency: .dailyAndCount) + } + + throw error } + } + + func removeSystemExtension() async throws { +#if NETP_SYSTEM_EXTENSION + await tunnelController.stop() + try await networkExtensionController.deactivateSystemExtension() + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) #endif + } + + func removeVPNConfiguration() async throws { + await tunnelController.stop() - await vpnConfiguration.removeVPNConfiguration() + guard let manager = await tunnelController.manager else { + // Nothing to remove, this is fine + return + } + + try await manager.removeFromPreferences() if defaults.networkProtectionOnboardingStatus == .completed { defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) } + } +} - defaults.networkProtectionShouldShowVPNUninstalledMessage = true - exit(EXIT_SUCCESS) +// MARK: - VPNUninstallAttempt + +extension VPNUninstaller { + enum UninstallCancellationReason: String { + case sysexInstallationCancelled + case sysexInstallationRequiresAuthorization + } + + enum VPNUninstallAttempt: PixelKitEventV2 { + case begin + case cancelled(_ reason: UninstallCancellationReason) + case success + case failure(_ error: Error) + + var name: String { + switch self { + case .begin: + return "vpn_controller_uninstall_attempt" + + case .cancelled: + return "vpn_controller_uninstall_cancelled" + + case .success: + return "vpn_controller_uninstall_success" + + case .failure: + return "vpn_controller_uninstall_failure" + } + } + + var parameters: [String: String]? { + switch self { + case .begin, + .success, + .failure: + return nil + case .cancelled(let reason): + return ["reason": reason.rawValue] + } + } + + var error: Error? { + switch self { + case .begin, + .cancelled, + .success: + return nil + case .failure(let error): + return error + } + } } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index a5b462fc1d..b202597a0a 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: "145.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 1088392164..c3ca3e5042 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index a3aafbbe11..58e060f78a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -178,14 +178,14 @@ extension TunnelControllerIPCClient: IPCServerInterface { }, xpcReplyErrorHandler: completion) } - public func debugCommand(_ command: DebugCommand) async throws { + public func command(_ command: VPNCommand) async throws { guard let payload = try? JSONEncoder().encode(command) else { return } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in xpc.execute(call: { server in - server.debugCommand(payload) { error in + server.command(payload) { error in if let error { continuation.resume(throwing: error) } else { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index ef5a8d015a..e1103a3592 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -49,9 +49,9 @@ public protocol IPCServerInterface: AnyObject { /// func fetchLastError(completion: @escaping (Error?) -> Void) - /// Debug commands + /// Commands /// - func debugCommand(_ command: DebugCommand) async throws + func command(_ command: VPNCommand) async throws } /// This protocol describes the server-side XPC interface. @@ -79,9 +79,9 @@ protocol XPCServerInterface { /// func fetchLastError(completion: @escaping (Error?) -> Void) - /// Debug commands + /// Commands /// - func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) + func command(_ payload: Data, completion: @escaping (Error?) -> Void) } public final class TunnelControllerIPCServer { @@ -186,15 +186,15 @@ extension TunnelControllerIPCServer: XPCServerInterface { serverDelegate?.fetchLastError(completion: completion) } - func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) { - guard let command = try? JSONDecoder().decode(DebugCommand.self, from: payload) else { + func command(_ payload: Data, completion: @escaping (Error?) -> Void) { + guard let command = try? JSONDecoder().decode(VPNCommand.self, from: payload) else { completion(IPCError.cannotDecodeDebugCommand) return } Task { do { - try await serverDelegate?.debugCommand(command) + try await serverDelegate?.command(command) completion(nil) } catch { completion(error) diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index a8431e0cfe..e272dea26f 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: "145.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 07ede788aa732521eba78748d7920659400fbb60 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 16 May 2024 17:27:14 +0600 Subject: [PATCH 119/134] Fix wkobject deinit from background thread (#2769) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207299387361575/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/817 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift | 10 ++++++++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8c42e67d5c..08c7eacafb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12902,7 +12902,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.3.0; + version = 145.3.1; }; }; 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 6ed1817498..3f7a21fb3e 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" : "b901a47ad442356b0953ffdb768ea1fae47fe912", - "version" : "145.3.0" + "revision" : "9e64d694224e1e38b46b1a00656ad9654f941ad3", + "version" : "145.3.1" } }, { diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift index db56c4d535..1d219e0c32 100644 --- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift +++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift @@ -567,12 +567,18 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable } deinit { - @MainActor(unsafe) - func performRegardlessOfMainThread() { + DispatchQueue.main.asyncOrNow { [download] in + // WebKit objects must be deallocated on the main thread + withExtendedLifetime(download) {} + } + +#if DEBUG + @MainActor(unsafe) func performRegardlessOfMainThread() { os_log(.debug, log: log, ".deinit") assert(state.isCompleted, "FileDownloadTask is deallocated without finish(with:) been called") } performRegardlessOfMainThread() +#endif } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index b202597a0a..f164f07811 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: "145.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index c3ca3e5042..d666a56945 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index e272dea26f..b2ddce187b 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: "145.3.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 701cdbcf2091eb4a3008ea9e6c79ba19d86986f9 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 17 May 2024 09:18:55 +1000 Subject: [PATCH 120/134] Search bar clickable area is too small (#2787) Task/Issue URL: https://app.asana.com/0/1177771139624306/1203899219213416/f **Description**: This PR fixes the clickable area of the AddressBar by intercepting right-clicks in the `AddressBarViewController` and forwarding them to the `AddressBarTextField` if necessary. --- .../View/AddressBarViewController.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 51c3ca308a..42c1ce7869 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -461,6 +461,10 @@ extension AddressBarViewController { guard let self else { return event } return self.mouseUp(with: event) }.store(in: &eventMonitorCancellables) + NSEvent.addLocalCancellableMonitor(forEventsMatching: .rightMouseDown) { [weak self] event in + guard let self else { return event } + return self.rightMouseDown(with: event) + }.store(in: &eventMonitorCancellables) } func mouseDown(with event: NSEvent) -> NSEvent? { @@ -491,6 +495,22 @@ extension AddressBarViewController { return event } + func rightMouseDown(with event: NSEvent) -> NSEvent? { + guard event.window === self.view.window else { return event } + // Convert the point to view system + let pointInView = view.convert(event.locationInWindow, from: nil) + + // If the view where the touch occurred is outside the AddressBar forward the event + guard let viewWithinAddressBar = view.hitTest(pointInView) else { return event } + + // If the farthest view of the point location is a NSButton or LottieAnimationView don't show contextual menu + guard viewWithinAddressBar.shouldShowArrowCursor == false else { return nil } + + // The event location is not a button so we can forward the event to the textfield + addressBarTextField.rightMouseDown(with: event) + return nil + } + private static let maxClickReleaseDistanceToResignFirstResponder: CGFloat = 4 func mouseUp(with event: NSEvent) -> NSEvent? { From 5864a4adfd6e494b114d07a16aa1e161026577b2 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 17 May 2024 11:41:06 +0200 Subject: [PATCH 121/134] Fix a division-by-zero crash in the VPN code (#2785) Task/Issue URL: https://app.asana.com/0/1203137811378537/1207299387361586/f iOS PR: https://github.com/duckduckgo/iOS/pull/2862 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/823 ## Description Fixes a crash caused by a division by zero. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8086840be4..df96f2f78a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12842,7 +12842,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 144.0.6; + version = "144.0.7-3"; }; }; 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 c8835ebb9b..cb1323b9af 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" : "72be4e73360989af170399bc063fd5c628e1e84c", - "version" : "144.0.6" + "revision" : "ada5f68970f098b3230dbd80a25cd048a606ac12", + "version" : "144.0.7-3" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "14b13d0c3db38f471ce4ba1ecb502ee1986c84d7", - "version" : "3.5.0" + "revision" : "25b8903191a40b21b09525085fe325ae3386092e", + "version" : "3.6.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5d9d7c8866..ca45e2f839 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: "144.0.6"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 380fd19ea9..5c1a60bab9 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.6"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index fa2374c63d..6f7d2da22e 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: "144.0.6"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), .package(path: "../SwiftUIExtensions") ], targets: [ From e8897076a83232543427e19e2dad34decc7b5645 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 17 May 2024 20:17:15 +1000 Subject: [PATCH 122/134] Fix "Details" copy alignment for Fire pop-up (#2739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1205409896979119/f **Description**: Fixed the alignment for the “Details” button in the `Fire` Popover. --- DuckDuckGo/Common/Localizables/UserText.swift | 1 - .../Fire/View/FirePopoverViewController.swift | 20 +++---- DuckDuckGo/Localizable.xcstrings | 60 ------------------- 3 files changed, 10 insertions(+), 71 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index a3ee25f1f8..f5a8f3b421 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -332,7 +332,6 @@ struct UserText { static let currentWindow = NSLocalizedString("fire.currentWindow", value: "All sites visited in current window", comment: "Configuration option for fire button") static let allDataDescription = NSLocalizedString("fire.all-data.description", value: "Clear all tabs and related site data", comment: "Description of the 'All Data' configuration option for the fire button") static let currentWindowDescription = NSLocalizedString("fire.current-window.description", value: "Clear current window and related site data", comment: "Description of the 'Current Window' configuration option for the fire button") - static let selectedDomainsDescription = NSLocalizedString("fire.selected-domains.description", value: "Clear data only for selected domains", comment: "Description of the 'Current Window' configuration option for the fire button") static let selectSiteToClear = NSLocalizedString("fire.select-site-to-clear", value: "Select a site to clear its data.", comment: "Info label in the fire button popover") static func activeTabsInfo(tabs: Int, sites: Int) -> String { let localized = NSLocalizedString("fire.active-tabs-info", diff --git a/DuckDuckGo/Fire/View/FirePopoverViewController.swift b/DuckDuckGo/Fire/View/FirePopoverViewController.swift index 72708e500d..53b1dc91b2 100644 --- a/DuckDuckGo/Fire/View/FirePopoverViewController.swift +++ b/DuckDuckGo/Fire/View/FirePopoverViewController.swift @@ -108,7 +108,7 @@ final class FirePopoverViewController: NSViewController { setUpStrings() updateClearButtonAppearance() setupOptionsButton() - updateCloseDetailsButton() + setupOpenCloseDetailsButton() updateWarningWrapperView() subscribeToViewModel() @@ -125,7 +125,6 @@ final class FirePopoverViewController: NSViewController { openFireWindowsTitleLabel.stringValue = UserText.fireDialogFireWindowTitle fireWindowDescriptionLabel.stringValue = UserText.fireDialogFireWindowDescription closeTabsLabel.stringValue = UserText.fireDialogCloseTabs - openDetailsButton.title = UserText.details closeBurnerWindowButton.title = UserText.fireDialogBurnWindowButton clearButton.title = UserText.clear cancelButton.title = UserText.cancel @@ -137,7 +136,6 @@ final class FirePopoverViewController: NSViewController { return } firePopoverViewModel.clearingOption = clearingOption - updateCloseDetailsButton() updateWarningWrapperView() } @@ -221,13 +219,16 @@ final class FirePopoverViewController: NSViewController { button.attributedTitle = attrTitle } - private func updateCloseDetailsButton() { - guard firePopoverViewModel.areAllSelected else { - closeDetailsButton.title = " \(UserText.selectedDomainsDescription)" - return - } + private func setupOpenCloseDetailsButton() { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.firstLineHeadIndent = 15 + let title = NSMutableAttributedString(string: UserText.fireDialogDetails) + title.addAttributes([.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: title.length)) - closeDetailsButton.title = " \(UserText.fireDialogDetails)" + openDetailsButton.attributedTitle = title + openDetailsButton.alignment = .left + closeDetailsButton.attributedTitle = title + closeDetailsButton.alignment = .left } private func updateWarningWrapperView() { @@ -288,7 +289,6 @@ final class FirePopoverViewController: NSViewController { let selectionIndexPaths = Set(selected.map {IndexPath(item: $0, section: self.firePopoverViewModel.selectableSectionIndex)}) self.collectionView.selectionIndexPaths = selectionIndexPaths self.updateInfoLabel() - self.updateCloseDetailsButton() } } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index caa202fc24..8382341179 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -19935,66 +19935,6 @@ } } }, - "fire.selected-domains.description" : { - "comment" : "Description of the 'Current Window' configuration option for the fire button", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Daten nur für ausgewählte Domains löschen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Clear data only for selected domains" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar datos solo para los dominios seleccionados" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "N'effacer les données que des domaines sélectionnés" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancella i dati solo per i domini selezionati" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alleen gegevens wissen voor de geselecteerde domeinen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wyczyść dane dotyczące tylko wybranych domen" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limpar dados apenas para domínios selecionados" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Функция Fire Button сотрет только данные выбранных сайтов" - } - } - } - }, "fireproof" : { "comment" : "Fireproof button", "extractionState" : "extracted_with_value", From 0a060e9873aef76c0581d5a4a407861bf8e4a728 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 17 May 2024 21:21:05 +1000 Subject: [PATCH 123/134] Cherry pick Subscription Attribution changes (#2790) Task/Issue URL: https://app.asana.com/0/1199230911884351/1205406454505225/f **Description**: Cherry pick the changes from this [PR](https://github.com/duckduckgo/macos-browser/pull/2761) --- DuckDuckGo.xcodeproj/project.pbxproj | 62 ++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Attribution/AttributionPixelHandler.swift | 97 ++++++++++++++ .../Common/Extensions/URLExtension.swift | 1 + .../InstallationAttributionPixelHandler.swift | 63 ++------- .../Statistics/ATB/StatisticsLoader.swift | 4 +- DuckDuckGo/Statistics/PrivacyProPixel.swift | 2 + .../SubscriptionAttributionPixelHandler.swift | 46 +++++++ .../SubscriptionRedirectManager.swift | 66 ++++++++++ .../RedirectNavigationResponder.swift | 23 +--- .../SubscriptionPagesUserScript.swift | 18 ++- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Pixel/PixelCapturedParameters.swift | 31 +++++ ...allationAttributionPixelHandlerTests.swift | 49 +++---- .../Mock/MockAttributionOriginProvider.swift | 1 + .../Mock/MockAttributionsPixelHandler.swift | 2 +- ...criptionAttributionPixelHandlerTests.swift | 124 ++++++++++++++++++ .../SubscriptionRedirectManagerTests.swift | 61 +++++++++ 20 files changed, 551 insertions(+), 109 deletions(-) create mode 100644 DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift create mode 100644 DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift create mode 100644 DuckDuckGo/Subscription/SubscriptionRedirectManager.swift create mode 100644 UnitTests/Common/Pixel/PixelCapturedParameters.swift create mode 100644 UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift create mode 100644 UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index df96f2f78a..aa6ee329fc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1713,6 +1713,10 @@ 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; + 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; + 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; + 9F0660782BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */; }; + 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F0FFFB42BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */; }; @@ -1747,6 +1751,14 @@ 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; + 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; + 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; + 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; + 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; + 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; @@ -3474,6 +3486,8 @@ 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; + 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandlerTests.swift; sourceTree = ""; }; + 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelCapturedParameters.swift; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F0FFFB72BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelMock.swift; sourceTree = ""; }; @@ -3491,6 +3505,10 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; + 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionPixelHandler.swift; sourceTree = ""; }; + 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; + 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; + 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManagerTests.swift; sourceTree = ""; }; 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; @@ -4516,6 +4534,8 @@ isa = PBXGroup; children = ( 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, + 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */, + 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */, ); path = Subscription; sourceTree = ""; @@ -6252,6 +6272,7 @@ B683097A274DCFE3004B46BB /* Database */, AAEC74B92642E66600C2EFBC /* Extensions */, 4BA1A6CE258BF58C00F6F690 /* FileSystem */, + 9F0660752BECC7E700B8EEF1 /* Pixel */, B6AE74322609AFBB005B9B1A /* Progress */, B698E5032908011E00A746A8 /* AppKitPrivateMethodsAvailabilityTests.swift */, B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */, @@ -6303,6 +6324,14 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F0660752BECC7E700B8EEF1 /* Pixel */ = { + isa = PBXGroup; + children = ( + 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */, + ); + path = Pixel; + sourceTree = ""; + }; 9F0FFFB62BCCAE80007C87DD /* Mocks */ = { isa = PBXGroup; children = ( @@ -6313,6 +6342,23 @@ path = Mocks; sourceTree = ""; }; + 9F64345F2BEC82A000D2D8A0 /* Attribution */ = { + isa = PBXGroup; + children = ( + 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */, + ); + path = Attribution; + sourceTree = ""; + }; + 9F64346E2BECB9FB00D2D8A0 /* Subscriptions */ = { + isa = PBXGroup; + children = ( + 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */, + 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */, + ); + path = Subscriptions; + sourceTree = ""; + }; 9F872D9B2B9058B000138637 /* Extensions */ = { isa = PBXGroup; children = ( @@ -6635,6 +6681,7 @@ 858A798626A99D9000A75A42 /* SecureVault */, B6DA440F2616C0F200DD1EC2 /* Statistics */, AA63744E24C9BB4A00AB2AC4 /* Suggestions */, + 9F64346E2BECB9FB00D2D8A0 /* Subscriptions */, AA92ACAE24EFE1F5005F41C9 /* Tab */, AAC9C01224CAFBB700AD1325 /* TabBar */, B6CA4822298CDC0B0067ECCE /* TabExtensionsTests */, @@ -6900,6 +6947,7 @@ AA86491324D831B9001BABEE /* Common */ = { isa = PBXGroup; children = ( + 9F64345F2BEC82A000D2D8A0 /* Attribution */, 4B67743D255DBEEA00025BD8 /* Database */, AADC60E92493B305008F8EF7 /* Extensions */, 4BA1A691258B06F600F6F690 /* FileSystem */, @@ -9522,6 +9570,7 @@ 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, + 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9957,6 +10006,7 @@ B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, + 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, 3706FBED293F65D500E42796 /* TabCollection.swift in Sources */, B6C0BB6B29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, @@ -10065,6 +10115,7 @@ 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 */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, @@ -10329,6 +10380,7 @@ 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 3706FE1F293F661700E42796 /* AppStateChangePublisherTests.swift in Sources */, 9FA5A0B12BC9039300153786 /* BookmarkFolderStoreMock.swift in Sources */, + 9F0660792BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, 3706FE20293F661700E42796 /* CLLocationManagerMock.swift in Sources */, B6656E0E2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, B6C843DB2BA1CAB6006FDEC3 /* FilePresenterTests.swift in Sources */, @@ -10344,6 +10396,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, @@ -10497,6 +10550,7 @@ 5681ED442BDBA5F900F59729 /* SyncBookmarksAdapterTests.swift in Sources */, B60C6F8529B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, 567DA94629E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, + 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, 3706FE82293F661700E42796 /* MockStatisticsStore.swift in Sources */, 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, @@ -10985,6 +11039,7 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, + 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, @@ -11094,6 +11149,7 @@ B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, + 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, @@ -11151,6 +11207,7 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, + 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11594,6 +11651,7 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.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 */, @@ -11720,6 +11778,7 @@ 1D8C2FED2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, + 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */, @@ -11755,6 +11814,7 @@ B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */, 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, 1D8C2FF02B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */, + 9F0660782BECC81C00B8EEF1 /* PixelCapturedParameters.swift in Sources */, 1D3B1AC429378953006F4388 /* BWResponseTests.swift in Sources */, B693955F26F1C17F0015B914 /* DownloadListCoordinatorTests.swift in Sources */, 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */, @@ -12842,7 +12902,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = "144.0.7-3"; + version = "144.0.7-4"; }; }; 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 cb1323b9af..e9bb970531 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" : "ada5f68970f098b3230dbd80a25cd048a606ac12", - "version" : "144.0.7-3" + "revision" : "43db1d59455246547fc4ea3998f07751dfa77166", + "version" : "144.0.7-4" } }, { diff --git a/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift b/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift new file mode 100644 index 0000000000..0a21678f79 --- /dev/null +++ b/DuckDuckGo/Common/Attribution/AttributionPixelHandler.swift @@ -0,0 +1,97 @@ +// +// AttributionPixelHandler.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 + +// A type that send pixels that needs attributions parameters. +protocol AttributionPixelHandler { + func fireAttributionPixel( + event: PixelKit.Event, + frequency: PixelKit.Frequency, + origin: String?, + additionalParameters: [String: String]? + ) +} + +final class GenericAttributionPixelHandler: AttributionPixelHandler { + enum Parameters { + static let origin = "origin" + static let locale = "locale" + } + + private let fireRequest: FireRequest + private let locale: Locale + + /// Creates an instance with the specified fire request, origin provider and locale. + /// - Parameters: + /// - fireRequest: A function for sending the Pixel request. + /// - locale: The locale of the device. + init( + fireRequest: @escaping FireRequest = PixelKit.fire, + locale: Locale = .current + ) { + self.fireRequest = fireRequest + self.locale = locale + } + + func fireAttributionPixel( + event: PixelKit.Event, + frequency: PixelKit.Frequency, + origin: String?, + additionalParameters: [String: String]? + ) { + fireRequest( + event, + frequency, + [:], + self.parameters(additionalParameters, withOrigin: origin, locale: locale.identifier), + nil, + nil, + true, { _, _ in } + ) + } +} + +// MARK: - Parameter + +private extension GenericAttributionPixelHandler { + func parameters(_ parameters: [String: String]?, withOrigin origin: String?, locale: String) -> [String: String] { + var parameters = parameters ?? [:] + parameters[Self.Parameters.locale] = locale + if let origin { + parameters[Self.Parameters.origin] = origin + } + return parameters + } +} + +// MARK: - FireRequest + +extension GenericAttributionPixelHandler { + typealias FireRequest = ( + _ event: PixelKit.Event, + _ frequency: PixelKit.Frequency, + _ headers: [String: String], + _ parameters: [String: String]?, + _ error: Error?, + _ allowedQueryReservedCharacters: CharacterSet?, + _ includeAppVersionParameter: Bool, + _ onComplete: @escaping (Bool, Error?) -> Void + ) -> Void +} diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 07683d29cc..db79eac20f 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -581,4 +581,5 @@ extension URL { return false } } + } diff --git a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift index b26f411d9c..b9d05691e7 100644 --- a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift +++ b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift @@ -20,72 +20,29 @@ import Foundation import PixelKit /// A type that handles Pixels for acquisition attributions. -protocol AttributionsPixelHandler: AnyObject { +protocol InstallationAttributionsPixelHandler: AnyObject { /// Fire the Pixel to track the App install. func fireInstallationAttributionPixel() } -final class InstallationAttributionPixelHandler: AttributionsPixelHandler { - enum Parameters { - static let origin = "origin" - static let locale = "locale" - } - - private let fireRequest: FireRequest +final class AppInstallationAttributionPixelHandler: InstallationAttributionsPixelHandler { private let originProvider: AttributionOriginProvider - private let locale: Locale + private let decoratedAttributionPixelHandler: AttributionPixelHandler - /// Creates an instance with the specified fire request, origin provider and locale. - /// - Parameters: - /// - fireRequest: A function for sending the Pixel request. - /// - originProvider: A provider for the origin used to track the acquisition funnel. - /// - locale: The locale of the device. init( - fireRequest: @escaping FireRequest = PixelKit.fire, originProvider: AttributionOriginProvider = AttributionOriginFileProvider(), - locale: Locale = .current + attributionPixelHandler: AttributionPixelHandler = GenericAttributionPixelHandler() ) { - self.fireRequest = fireRequest self.originProvider = originProvider - self.locale = locale + decoratedAttributionPixelHandler = attributionPixelHandler } func fireInstallationAttributionPixel() { - fireRequest( - GeneralPixel.installationAttribution, - .legacyInitial, - [:], - additionalParameters(origin: originProvider.origin, locale: locale.identifier), - nil, - nil, - true, { _, _ in } + decoratedAttributionPixelHandler.fireAttributionPixel( + event: GeneralPixel.installationAttribution, + frequency: .legacyInitial, + origin: originProvider.origin, + additionalParameters: nil ) } } - -// MARK: - Parameter - -private extension InstallationAttributionPixelHandler { - func additionalParameters(origin: String?, locale: String) -> [String: String] { - var dictionary = [Self.Parameters.locale: locale] - if let origin { - dictionary[Self.Parameters.origin] = origin - } - return dictionary - } -} - -// MARK: - FireRequest - -extension InstallationAttributionPixelHandler { - typealias FireRequest = ( - _ event: PixelKit.Event, - _ frequency: PixelKit.Frequency, - _ headers: [String: String], - _ parameters: [String: String]?, - _ error: Error?, - _ allowedQueryReservedCharacters: CharacterSet?, - _ includeAppVersionParameter: Bool, - _ onComplete: @escaping (Bool, Error?) -> Void - ) -> Void -} diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 744f4b6071..84219ba446 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -30,14 +30,14 @@ final class StatisticsLoader { private let statisticsStore: StatisticsStore private let emailManager: EmailManager - private let attributionPixelHandler: AttributionsPixelHandler + private let attributionPixelHandler: InstallationAttributionsPixelHandler private let parser = AtbParser() private var isAppRetentionRequestInProgress = false init( statisticsStore: StatisticsStore = LocalStatisticsStore(), emailManager: EmailManager = EmailManager(), - attributionPixelHandler: AttributionsPixelHandler = InstallationAttributionPixelHandler() + attributionPixelHandler: InstallationAttributionsPixelHandler = AppInstallationAttributionPixelHandler() ) { self.statisticsStore = statisticsStore self.emailManager = emailManager diff --git a/DuckDuckGo/Statistics/PrivacyProPixel.swift b/DuckDuckGo/Statistics/PrivacyProPixel.swift index d50f06656a..a4093228f6 100644 --- a/DuckDuckGo/Statistics/PrivacyProPixel.swift +++ b/DuckDuckGo/Statistics/PrivacyProPixel.swift @@ -65,6 +65,7 @@ enum PrivacyProPixel: PixelKitEventV2 { case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval case privacyProPurchaseStripeSuccess + case privacyProSuccessfulSubscriptionAttribution // Web pixels case privacyProOfferMonthlyPriceClick case privacyProOfferYearlyPriceClick @@ -110,6 +111,7 @@ enum PrivacyProPixel: PixelKitEventV2 { case .privacyProSubscriptionManagementPlanBilling: return "m_mac_\(appDistribution)_privacy-pro_settings_change-plan-or-billing_click" case .privacyProSubscriptionManagementRemoval: return "m_mac_\(appDistribution)_privacy-pro_settings_remove-from-device_click" case .privacyProPurchaseStripeSuccess: return "m_mac_\(appDistribution)_privacy-pro_app_subscription-purchase_stripe_success" + case .privacyProSuccessfulSubscriptionAttribution: return "m_mac_\(appDistribution)_subscribe" // Web case .privacyProOfferMonthlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_monthly-price_click" case .privacyProOfferYearlyPriceClick: return "m_mac_\(appDistribution)_privacy-pro_offer_yearly-price_click" diff --git a/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift b/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift new file mode 100644 index 0000000000..a6361d0427 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionAttributionPixelHandler.swift @@ -0,0 +1,46 @@ +// +// SubscriptionAttributionPixelHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription + +protocol SubscriptionAttributionPixelHandler: AnyObject { + var origin: String? { get set } + func fireSuccessfulSubscriptionAttributionPixel() +} + +// MARK: - SubscriptionAttributionPixelHandler + +final class PrivacyProSubscriptionAttributionPixelHandler: SubscriptionAttributionPixelHandler { + var origin: String? + private let decoratedAttributionPixelHandler: AttributionPixelHandler + + init(attributionPixelHandler: AttributionPixelHandler = GenericAttributionPixelHandler()) { + decoratedAttributionPixelHandler = attributionPixelHandler + } + + func fireSuccessfulSubscriptionAttributionPixel() { + decoratedAttributionPixelHandler.fireAttributionPixel( + event: PrivacyProPixel.privacyProSuccessfulSubscriptionAttribution, + frequency: .standard, + origin: origin, + additionalParameters: nil + ) + } + +} diff --git a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift new file mode 100644 index 0000000000..66e6aefa04 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift @@ -0,0 +1,66 @@ +// +// SubscriptionRedirectManager.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription +import BrowserServicesKit + +protocol SubscriptionRedirectManager: AnyObject { + func redirectURL(for url: URL) -> URL? +} + +final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { + private let featureAvailabiltyProvider: () -> Bool + + init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable) { + self.featureAvailabiltyProvider = featureAvailabiltyProvider + } + + func redirectURL(for url: URL) -> URL? { + guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } + + if url.pathComponents == URL.privacyPro.pathComponents { + let isFeatureAvailable = featureAvailabiltyProvider() + let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts + // Redirect the `/pro` URL to `/subscriptions` URL. If there are any query items in the original URL it appends to the `/subscriptions` URL. + return isPurchasePageRedirectActive ? URL.subscriptionBaseURL.addingQueryItems(from: url) : nil + } + + return nil + } + +} + +private extension URL { + + func addingQueryItems(from url: URL) -> URL { + // If the origin value is of type "do+something" appending the percentEncodedQueryItem crashes the browser as + is replaced by a space. + // Perform encoding on the value to avoid the crash. + guard let queryItems = url.getQueryItems()? + .compactMap({ queryItem -> URLQueryItem? in + guard let value = queryItem.value else { return nil } + let encodedValue = value.percentEncoded(withAllowedCharacters: .urlQueryParameterAllowed) + return URLQueryItem(name: queryItem.name, value: encodedValue) + }) + else { return self } + + return self.appending(percentEncodedQueryItems: queryItems) + } + +} diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index d06cb5f3d3..81f400302c 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -18,13 +18,17 @@ import Navigation import Foundation -import Subscription -import BrowserServicesKit struct RedirectNavigationResponder: NavigationResponder { + private let redirectManager: SubscriptionRedirectManager + + init(redirectManager: SubscriptionRedirectManager = PrivacyProSubscriptionRedirectManager()) { + self.redirectManager = redirectManager + } + func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? { - guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectURL(for: navigationAction.url) else { return .next } + guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectManager.redirectURL(for: navigationAction.url) else { return .next } return .redirect(mainFrame) { navigator in var request = navigationAction.request @@ -33,17 +37,4 @@ struct RedirectNavigationResponder: NavigationResponder { } } - private func redirectURL(for url: URL) -> URL? { - guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } - - if url.pathComponents == URL.privacyPro.pathComponents { - let isFeatureAvailable = DefaultSubscriptionFeatureAvailability().isFeatureAvailable - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false - let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts - - return isPurchasePageRedirectActive ? URL.subscriptionBaseURL : nil - } - - return nil - } } diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index 4d9b38724b..f0e7525820 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -90,6 +90,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { self.broker = broker } + private let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + + init(subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { + self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + } + struct Handlers { static let getSubscription = "getSubscription" static let setSubscription = "setSubscription" @@ -208,7 +214,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // swiftlint:disable:next function_body_length cyclomatic_complexity func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) struct SubscriptionSelection: Decodable { let id: String @@ -216,6 +221,9 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let message = original + // Extract the origin from the webview URL to use for attribution pixel. + subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) + if SubscriptionPurchaseEnvironment.current == .appStore { if #available(macOS 12.0, *) { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController @@ -287,6 +295,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { os_log(.info, log: .subscription, "[Purchase] Purchase complete") PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .unique) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) case .failure(let error): switch error { @@ -420,6 +429,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.dismiss(progressViewController) PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) + subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() return [String: String]() // cannot be nil, the web app expect something back before redirecting the user to the final page } @@ -477,6 +487,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { broker.push(method: method.rawValue, params: params, for: self, into: webView) } + + @MainActor + private func originFrom(originalMessage: WKScriptMessage) -> String? { + let url = originalMessage.webView?.url + return url?.getParameter(named: AttributionParameter.origin) + } } extension MainWindowController { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index ca45e2f839..323f88e898 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: "144.0.7-3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-4"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 5c1a60bab9..2a0f9704d3 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-4"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 6f7d2da22e..20769260df 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: "144.0.7-3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.7-4"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Common/Pixel/PixelCapturedParameters.swift b/UnitTests/Common/Pixel/PixelCapturedParameters.swift new file mode 100644 index 0000000000..b73c5b38af --- /dev/null +++ b/UnitTests/Common/Pixel/PixelCapturedParameters.swift @@ -0,0 +1,31 @@ +// +// PixelCapturedParameters.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 + +struct PixelCapturedParameters { + var event: PixelKit.Event? + var frequency: PixelKit.Frequency = .standard + var headers: [String: String] = [:] + var parameters: [String: String]? + var error: Error? + var reservedCharacters: CharacterSet? + var includeAppVersion: Bool? + var onComplete: (Bool, Error?) -> Void = { _, _ in } +} diff --git a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift index 0d217cf769..df475b229b 100644 --- a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift +++ b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift @@ -21,13 +21,13 @@ import PixelKit @testable import DuckDuckGo_Privacy_Browser final class InstallationAttributionPixelHandlerTests: XCTestCase { - private var sut: InstallationAttributionPixelHandler! - private var capturedParams: CapturedParameters! - private var fireRequest: InstallationAttributionPixelHandler.FireRequest! + private var sut: AppInstallationAttributionPixelHandler! + private var capturedParams: PixelCapturedParameters! + private var fireRequest: GenericAttributionPixelHandler.FireRequest! override func setUpWithError() throws { try super.setUpWithError() - capturedParams = CapturedParameters() + capturedParams = PixelCapturedParameters() fireRequest = { event, frequency, headers, parameters, error, reservedCharacters, includeAppVersion, onComplete in self.capturedParams.event = event self.capturedParams.frequency = frequency @@ -49,7 +49,8 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { func testWhenPixelFiresThenNameIsSetToM_Mac_Install() { // GIVEN - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() @@ -61,13 +62,14 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { func testWhenPixelFiresThenLanguageCodeIsSet() { // GIVEN let locale = Locale(identifier: "hu-HU") - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: locale) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "hu-HU") + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "hu-HU") } func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { @@ -75,14 +77,15 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { let origin = "app_search" let locale = Locale(identifier: "en-US") let originProvider = MockAttributionOriginProvider(origin: origin) - sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: originProvider, attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin], origin) - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") } func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { @@ -90,19 +93,20 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { let origin: String? = nil let locale = Locale(identifier: "en-US") let originProvider = MockAttributionOriginProvider(origin: origin) - sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) - + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = AppInstallationAttributionPixelHandler(originProvider: originProvider, attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() // THEN - XCTAssertNil(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin]) - XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + XCTAssertNil(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") } func testWhenPixelFiresThenAddAppVersionIsTrueAndFrequencyIsLegacyInitial() { // GIVEN - sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = AppInstallationAttributionPixelHandler(originProvider: MockAttributionOriginProvider(), attributionPixelHandler: decoratedPixelHandler) // WHEN sut.fireInstallationAttributionPixel() @@ -112,18 +116,3 @@ final class InstallationAttributionPixelHandlerTests: XCTestCase { XCTAssertEqual(capturedParams.frequency, .legacyInitial) } } - -extension InstallationAttributionPixelHandlerTests { - - struct CapturedParameters { - var event: PixelKit.Event? - var frequency: PixelKit.Frequency = .standard - var headers: [String: String] = [:] - var parameters: [String: String]? - var error: Error? - var reservedCharacters: CharacterSet? - var includeAppVersion: Bool? - var onComplete: (Bool, Error?) -> Void = { _, _ in } - } - -} diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift index 05e91bff49..bd07f8f56d 100644 --- a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription @testable import DuckDuckGo_Privacy_Browser final class MockAttributionOriginProvider: AttributionOriginProvider { diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift index a32ee60a30..cc6aa508a1 100644 --- a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift @@ -19,7 +19,7 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser -final class MockAttributionsPixelHandler: AttributionsPixelHandler { +final class MockAttributionsPixelHandler: InstallationAttributionsPixelHandler { private(set) var fireInstallationAttributionPixelCount = 0 private(set) var didCallFireInstallationAttributionPixel = false diff --git a/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift b/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift new file mode 100644 index 0000000000..8bc003fe07 --- /dev/null +++ b/UnitTests/Subscriptions/SubscriptionAttributionPixelHandlerTests.swift @@ -0,0 +1,124 @@ +// +// SubscriptionAttributionPixelHandlerTests.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 PixelKit +@testable import DuckDuckGo_Privacy_Browser + +final class SubscriptionAttributionPixelHandlerTests: XCTestCase { + private var sut: PrivacyProSubscriptionAttributionPixelHandler! + private var capturedParams: PixelCapturedParameters! + private var fireRequest: GenericAttributionPixelHandler.FireRequest! + + override func setUpWithError() throws { + try super.setUpWithError() + capturedParams = PixelCapturedParameters() + fireRequest = { event, frequency, headers, parameters, error, reservedCharacters, includeAppVersion, onComplete in + self.capturedParams.event = event + self.capturedParams.frequency = frequency + self.capturedParams.headers = headers + self.capturedParams.parameters = parameters + self.capturedParams.error = error + self.capturedParams.reservedCharacters = reservedCharacters + self.capturedParams.includeAppVersion = includeAppVersion + self.capturedParams.onComplete = onComplete + } + } + + override func tearDownWithError() throws { + capturedParams = nil + fireRequest = nil + sut = nil + try super.tearDownWithError() + } + + func testWhenPixelFiresThenNameIsSetToM_Mac_DistributionType_Subscribe() { + // GIVEN + #if APPSTORE + let expectedPixelName = "m_mac_store_subscribe" + #else + let expectedPixelName = "m_mac_direct_subscribe" + #endif + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.event?.name, expectedPixelName) + } + + func testWhenPixelFiresThenLanguageCodeIsSet() { + // GIVEN + let locale = Locale(identifier: "hu-HU") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "hu-HU") + } + + func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { + // GIVEN + let origin = "app_search" + let locale = Locale(identifier: "en-US") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + sut.origin = origin + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { + // GIVEN + let origin: String? = nil + let locale = Locale(identifier: "en-US") + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: locale) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + sut.origin = origin + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertNil(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[GenericAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresThenAddAppVersionIsTrueAndFrequencyIsStandard() { + // GIVEN + let decoratedPixelHandler = GenericAttributionPixelHandler(fireRequest: fireRequest, locale: .current) + sut = PrivacyProSubscriptionAttributionPixelHandler(attributionPixelHandler: decoratedPixelHandler) + + // WHEN + sut.fireSuccessfulSubscriptionAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.includeAppVersion, true) + XCTAssertEqual(capturedParams.frequency, .standard) + } +} diff --git a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift new file mode 100644 index 0000000000..814abc7a42 --- /dev/null +++ b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift @@ -0,0 +1,61 @@ +// +// SubscriptionRedirectManagerTests.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 Subscription +@testable import DuckDuckGo_Privacy_Browser + +final class SubscriptionRedirectManagerTests: XCTestCase { + private var sut: PrivacyProSubscriptionRedirectManager! + + override func setUpWithError() throws { + try super.setUpWithError() + sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true) + SubscriptionPurchaseEnvironment.canPurchase = true + } + + override func tearDownWithError() throws { + sut = nil + try super.tearDownWithError() + } + + func testWhenURLIsPrivacyProAndHasOriginQueryParameterThenRedirectToSubscriptionBaseURLAndAppendQueryParameter() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro?origin=test")) + let expectedURL = URL.subscriptionBaseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) + + // WHEN + let result = sut.redirectURL(for: url) + + // THEN + XCTAssertEqual(result, expectedURL) + } + + func testWhenURLIsPrivacyProAndDoesNotHaveOriginQueryParameterThenRedirectToSubscriptionBaseURL() throws { + // GIVEN + let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro")) + let expectedURL = URL.subscriptionBaseURL + + // WHEN + let result = sut.redirectURL(for: url) + + // THEN + XCTAssertEqual(result, expectedURL) + } + +} From d0f433794a07f7b9bec1ea37d8efe6b63a821208 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 17 May 2024 11:49:56 +0000 Subject: [PATCH 124/134] Bump version to 1.88.0 (189) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 4e984defe0..b1f7508f66 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 188 +CURRENT_PROJECT_VERSION = 189 From 75e5ccc087a11d2a23b312ed9a433204e0c718ba Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 17 May 2024 19:36:55 +0600 Subject: [PATCH 125/134] Select all Find In Page text on cmd+f (#2783) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207298392719916/f --- DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift | 4 ++++ DuckDuckGo/FindInPage/FindInPageViewController.swift | 7 ++++++- DuckDuckGo/NavigationBar/View/AddressBarTextField.swift | 4 ---- DuckDuckGo/Tab/TabExtensions/FindInPageTabExtension.swift | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift b/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift index 563c7c1804..9b37797f7b 100644 --- a/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift @@ -21,6 +21,10 @@ import Common extension NSTextField { + var isFirstResponder: Bool { + window?.firstResponder == currentEditor() + } + static func label(titled title: String) -> NSTextField { let label = NSTextField(string: title) diff --git a/DuckDuckGo/FindInPage/FindInPageViewController.swift b/DuckDuckGo/FindInPage/FindInPageViewController.swift index 142d9c1f18..727b84e82f 100644 --- a/DuckDuckGo/FindInPage/FindInPageViewController.swift +++ b/DuckDuckGo/FindInPage/FindInPageViewController.swift @@ -102,7 +102,12 @@ final class FindInPageViewController: NSViewController { } func makeMeFirstResponder() { - textField.makeMeFirstResponder() + if textField.isFirstResponder { + // select all on Cmd+F while editing + textField.currentEditor()?.selectAll(nil) + } else { + textField.makeMeFirstResponder() + } } private func subscribeToModelChanges() { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 8248039994..943f6a8e66 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -51,10 +51,6 @@ final class AddressBarTextField: NSTextField { tabCollectionViewModel.isBurner } - var isFirstResponder: Bool { - window?.firstResponder == currentEditor() - } - private var suggestionResultCancellable: AnyCancellable? private var selectedSuggestionViewModelCancellable: AnyCancellable? private var selectedTabViewModelCancellable: AnyCancellable? diff --git a/DuckDuckGo/Tab/TabExtensions/FindInPageTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/FindInPageTabExtension.swift index c0c5d59bad..ada3a260c4 100644 --- a/DuckDuckGo/Tab/TabExtensions/FindInPageTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/FindInPageTabExtension.swift @@ -219,7 +219,7 @@ extension FindInPageTabExtension: NavigationResponder { } func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) { - if case .sessionStateReplace = navigationType { + if [.sessionStatePush, .sessionStatePop].contains(navigationType) { close() } } From af516c7c30b6bdae4e03b6b649563bf89846fc8e Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 17 May 2024 15:51:35 +0200 Subject: [PATCH 126/134] implement default zoom per site (#2566) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206933712061973/f **Description**: Implements default Zoom per website --- DuckDuckGo.xcodeproj/project.pbxproj | 12 + .../ZoomIncrease.imageset/Contents.json | 16 ++ .../Zoom-Page-Increase-16D.svg | 4 + DuckDuckGo/Common/Localizables/UserText.swift | 1 + .../Utilities/UserDefaultsWrapper.swift | 1 + DuckDuckGo/Fire/Model/Fire.swift | 15 ++ DuckDuckGo/Localizable.xcstrings | 60 +++++ .../AddressBarButtonsViewController.swift | 66 ++++- .../View/AddressBarViewController.swift | 1 + .../NavigationBar/View/MoreOptionsMenu.swift | 7 +- .../View/NavigationBar.storyboard | 30 ++- .../View/NavigationBarPopovers.swift | 29 ++ .../View/NavigationBarViewController.swift | 4 + .../NavigationBar/View/ZoomPopover.swift | 157 +++++++++++ .../Model/AccessibilityPreferences.swift | 49 +++- DuckDuckGo/Tab/View/WebView.swift | 14 + DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 102 +++++-- IntegrationTests/Tab/AddressBarTests.swift | 35 +++ UnitTests/Fire/Model/FireTests.swift | 30 +++ .../CapturingOptionsButtonMenuDelegate.swift | 5 + UnitTests/Menus/MoreOptionsMenuTests.swift | 2 +- .../AccessibilityPreferencesTests.swift | 166 +++++++++++- .../Tab/ViewModel/TabViewModelTests.swift | 255 +++++++++++++++++- .../ViewModel/ZoomPopoverViewModelTests.swift | 99 +++++++ UnitTests/Tab/WebViewTests.swift | 54 ++++ .../TabBar/Model/TabCollectionTests.swift | 5 + 26 files changed, 1166 insertions(+), 53 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Zoom-Page-Increase-16D.svg create mode 100644 DuckDuckGo/NavigationBar/View/ZoomPopover.swift create mode 100644 UnitTests/Tab/ViewModel/ZoomPopoverViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b7e6458d2b..7d447f74b1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1416,6 +1416,8 @@ 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 */; }; + 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 */; }; 561D29C32BDA745B007B91D0 /* MockSyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */; }; 561D29C62BDA74ED007B91D0 /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */; }; @@ -1424,6 +1426,8 @@ 561D29CB2BDA7530007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C82BDA751E007B91D0 /* MockAppearancePreferencesPersistor.swift */; }; 561D66662B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 561D66672B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; + 562532A02BC069180034D316 /* ZoomPopoverViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5625329D2BC069100034D316 /* ZoomPopoverViewModelTests.swift */; }; + 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5625329D2BC069100034D316 /* ZoomPopoverViewModelTests.swift */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; 562984712AC469E400AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */; }; @@ -3281,10 +3285,12 @@ 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 = ""; }; + 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 = ""; }; 561D29C82BDA751E007B91D0 /* MockAppearancePreferencesPersistor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppearancePreferencesPersistor.swift; sourceTree = ""; }; 561D66692B95C45A008ACC5C /* Suggestion.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Suggestion.storyboard; sourceTree = ""; }; + 5625329D2BC069100034D316 /* ZoomPopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomPopoverViewModelTests.swift; sourceTree = ""; }; 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferencesTests.swift; sourceTree = ""; }; 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDefaultBrowserProvider.swift; sourceTree = ""; }; 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncE2EUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -6992,6 +6998,7 @@ AA7EB6EE27E880EA00036718 /* Animations */, AAC5E4F025D6BF10007F5990 /* AddressBarButton.swift */, AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */, + 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */, AABEE6AE24AD22B90043105B /* AddressBarTextField.swift */, B6676BE02AA986A700525A21 /* AddressBarTextEditor.swift */, B60D64482AAF1B7C00B26F50 /* AddressBarTextSelectionNavigation.swift */, @@ -7110,6 +7117,7 @@ isa = PBXGroup; children = ( AAC9C01B24CB594C00AD1325 /* TabViewModelTests.swift */, + 5625329D2BC069100034D316 /* ZoomPopoverViewModelTests.swift */, ); path = ViewModel; sourceTree = ""; @@ -9557,6 +9565,7 @@ B6E3E5512BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */, 3706FAB0293F65D500E42796 /* BookmarkOutlineCellView.swift in Sources */, 3706FAB1293F65D500E42796 /* UnprotectedDomains.xcdatamodeld in Sources */, + 5614B3A22BBD639D009B5031 /* ZoomPopover.swift in Sources */, 85393C872A6FF1B600F11EB3 /* BookmarksBarAppearance.swift in Sources */, 3706FAB2293F65D500E42796 /* TabInstrumentation.swift in Sources */, 3706FAB5293F65D500E42796 /* ConfigurationManager.swift in Sources */, @@ -10397,6 +10406,7 @@ B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, + 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, @@ -11547,6 +11557,7 @@ AABEE69C24A902BB0043105B /* SuggestionContainer.swift in Sources */, B6C00ECD292F89D9009C73A6 /* FindInPageTabExtension.swift in Sources */, 85589E8327BBB8630038AD11 /* HomePageViewController.swift in Sources */, + 5614B3A12BBD639D009B5031 /* ZoomPopover.swift in Sources */, B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */, 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */, 1D36F4242A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, @@ -11827,6 +11838,7 @@ 4BA1A6FE258C5C1300F6F690 /* EncryptedValueTransformerTests.swift in Sources */, 85F69B3C25EDE81F00978E59 /* URLExtensionTests.swift in Sources */, 4B9292BA2667103100AD2C21 /* BookmarkNodePathTests.swift in Sources */, + 562532A02BC069180034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 4B9292C02667103100AD2C21 /* BookmarkManagedObjectTests.swift in Sources */, 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */, 4B723E0626B0003E00E14D75 /* CSVParserTests.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Contents.json new file mode 100644 index 0000000000..03210e711c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Zoom-Page-Increase-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/ZoomIncrease.imageset/Zoom-Page-Increase-16D.svg b/DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Zoom-Page-Increase-16D.svg new file mode 100644 index 0000000000..1176ff5c2e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/ZoomIncrease.imageset/Zoom-Page-Increase-16D.svg @@ -0,0 +1,4 @@ + + + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index f5a8f3b421..53319c1e7b 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -490,6 +490,7 @@ struct UserText { static let mobileBookmarksImportedFolderTitle = NSLocalizedString("bookmarks.imported.mobile.folder.title", value: "Mobile bookmarks", comment: "Name of the \"Mobile bookmarks\" folder imported from other browser") static let zoom = NSLocalizedString("zoom", value: "Zoom", comment: "Menu with Zooming commands") + static let resetZoom = NSLocalizedString("reset-zoom", value: "Reset", comment: "Button that allows the user to reset the zoom level of the browser page") static let emailOptionsMenuItem = NSLocalizedString("email.optionsMenu", value: "Email Protection", comment: "Menu item email feature") static let emailOptionsMenuCreateAddressSubItem = NSLocalizedString("email.optionsMenu.createAddress", value: "Generate Private Duck Address", comment: "Create an email alias sub menu item") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 7579b0c4ea..5c72907b8e 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -94,6 +94,7 @@ public struct UserDefaultsWrapper { case switchToNewTabWhenOpened = "preferences.tabs.switch-to-new-tab-when-opened" case newTabPosition = "preferences.tabs.new-tab-position" case defaultPageZoom = "preferences.appearance.default-page-zoom" + case websitePageZoom = "preferences.appearance.website-page-zoom" case bookmarksBarAppearance = "preferences.appearance.bookmarks-bar" case homeButtonPosition = "preferences.appeareance.home-button-position" diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 29d355c9cd..a89385145b 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -30,6 +30,7 @@ final class Fire { let webCacheManager: WebCacheManager let historyCoordinating: HistoryCoordinating let permissionManager: PermissionManagerProtocol + let savedZoomLevelsCoordinating: SavedZoomLevelsCoordinating let downloadListCoordinator: DownloadListCoordinator let windowControllerManager: WindowControllersManager let faviconManagement: FaviconManagement @@ -88,6 +89,7 @@ final class Fire { init(cacheManager: WebCacheManager = WebCacheManager.shared, historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, permissionManager: PermissionManagerProtocol = PermissionManager.shared, + savedZoomLevelsCoordinating: SavedZoomLevelsCoordinating = AccessibilityPreferences.shared, downloadListCoordinator: DownloadListCoordinator = DownloadListCoordinator.shared, windowControllerManager: WindowControllersManager = WindowControllersManager.shared, faviconManagement: FaviconManagement = FaviconManager.shared, @@ -104,6 +106,7 @@ final class Fire { self.webCacheManager = cacheManager self.historyCoordinating = historyCoordinating self.permissionManager = permissionManager + self.savedZoomLevelsCoordinating = savedZoomLevelsCoordinating self.downloadListCoordinator = downloadListCoordinator self.windowControllerManager = windowControllerManager self.faviconManagement = faviconManagement @@ -167,6 +170,7 @@ final class Fire { self.burnRecentlyClosed(baseDomains: domains) self.burnAutoconsentCache() + self.burnZoomLevels(of: domains) group.notify(queue: .main) { self.dispatchGroup = nil @@ -218,6 +222,7 @@ final class Fire { self.burnRecentlyClosed() self.burnAutoconsentCache() + self.burnZoomLevels() group.notify(queue: .main) { self.dispatchGroup = nil @@ -370,6 +375,16 @@ final class Fire { historyCoordinating.burnAll(completion: completion) } + // MARK: - Zoom levels + + private func burnZoomLevels() { + savedZoomLevelsCoordinating.burnZoomLevels(except: FireproofDomains.shared) + } + + private func burnZoomLevels(of baseDomains: Set) { + savedZoomLevelsCoordinating.burnZoomLevel(of: baseDomains) + } + // MARK: - Permissions private func burnPermissions(completion: @escaping () -> Void) { diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 8382341179..9ff3c8becb 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -47398,6 +47398,66 @@ } } }, + "reset-zoom" : { + "comment" : "Button that allows the user to reset the zoom level of the browser page", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zurücksetzen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Reset" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réinitialiser" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ripristina" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herstel" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resetowanie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сбросить" + } + } + } + }, "restart.bitwarden" : { "comment" : "Button to restart Bitwarden application", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index f2a57a1c01..b5134a73be 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -34,6 +34,8 @@ final class AddressBarButtonsViewController: NSViewController { weak var delegate: AddressBarButtonsViewControllerDelegate? + private let accessibilityPreferences: AccessibilityPreferences + private var permissionAuthorizationPopover: PermissionAuthorizationPopover? private func permissionAuthorizationPopoverCreatingIfNeeded() -> PermissionAuthorizationPopover { return permissionAuthorizationPopover ?? { @@ -53,6 +55,7 @@ final class AddressBarButtonsViewController: NSViewController { }() } + @IBOutlet weak var zoomButton: NSButton! @IBOutlet weak var privacyEntryPointButton: MouseOverAnimationButton! @IBOutlet weak var bookmarkButton: AddressBarButton! @IBOutlet weak var imageButtonWrapper: NSView! @@ -66,6 +69,7 @@ final class AddressBarButtonsViewController: NSViewController { var trackerAnimationView3: LottieAnimationView! var shieldAnimationView: LottieAnimationView! var shieldDotAnimationView: LottieAnimationView! + @IBOutlet weak var notificationAnimationView: NavigationBarBadgeAnimationView! @IBOutlet weak var permissionButtons: NSView! @@ -108,7 +112,12 @@ final class AddressBarButtonsViewController: NSViewController { @Published private(set) var buttonsWidth: CGFloat = 0 private var tabCollectionViewModel: TabCollectionViewModel - private var tabViewModel: TabViewModel? + private var tabViewModel: TabViewModel? { + didSet { + subscribeToTabZoomLevel() + } + } + private let popovers: NavigationBarPopovers private var bookmarkManager: BookmarkManager = LocalBookmarkManager.shared @@ -139,6 +148,7 @@ final class AddressBarButtonsViewController: NSViewController { private var urlCancellable: AnyCancellable? private var bookmarkListCancellable: AnyCancellable? private var effectiveAppearanceCancellable: AnyCancellable? + private var accessibilityPreferencesCancellable: AnyCancellable? private var permissionsCancellables = Set() private var trackerAnimationTriggerCancellable: AnyCancellable? private var privacyEntryPointIconUpdateCancellable: AnyCancellable? @@ -152,10 +162,11 @@ final class AddressBarButtonsViewController: NSViewController { init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, + accessibilityPreferences: AccessibilityPreferences = AccessibilityPreferences.shared, popovers: NavigationBarPopovers) { self.tabCollectionViewModel = tabCollectionViewModel + self.accessibilityPreferences = accessibilityPreferences self.popovers = popovers - super.init(coder: coder) } @@ -257,6 +268,27 @@ final class AddressBarButtonsViewController: NSViewController { bookmarkButton.isShown = shouldShowBookmarkButton } + private func updateZoomButtonVisibility(animation: Bool = false) { + zoomButton.isHidden = true + let hasURL = tabViewModel?.tab.url != nil + let isEditingMode = controllerMode?.isEditing ?? false + let isTextFieldValueText = textFieldValue?.isText ?? false + + var hasNonDefaultZoom = false + if tabViewModel?.zoomLevel != accessibilityPreferences.defaultPageZoom { + hasNonDefaultZoom = true + } + + let shouldShowZoom = hasURL + && !isEditingMode + && !isTextFieldValueText + && !isTextFieldEditorFirstResponder + && !animation + && (hasNonDefaultZoom || popovers.isZoomPopoverShown == true || popovers.zoomPopover != nil) + + zoomButton.isHidden = !shouldShowZoom + } + func openBookmarkPopover(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) { let result = bookmarkForCurrentUrl(setFavorite: setFavorite, accessPoint: accessPoint) guard let bookmark = result.bookmark else { @@ -330,6 +362,16 @@ final class AddressBarButtonsViewController: NSViewController { updateImageButton() updatePermissionButtons() updateBookmarkButtonVisibility() + updateZoomButtonVisibility() + } + + @IBAction func zoomButtonAction(_ sender: Any) { + if popovers.isZoomPopoverShown { + popovers.closeZoomPopover() + } else { + guard let tabViewModel = tabCollectionViewModel.selectedTabViewModel else { return } + popovers.showZoomPopover(for: tabViewModel, from: zoomButton, withDelegate: self) + } } @IBAction func cameraButtonAction(_ sender: NSButton) { @@ -753,8 +795,10 @@ final class AddressBarButtonsViewController: NSViewController { } animationView.isHidden = false - animationView.play { _ in + updateZoomButtonVisibility(animation: true) + animationView.play { [weak self] _ in animationView.isHidden = true + self?.updateZoomButtonVisibility(animation: false) } default: return @@ -773,11 +817,13 @@ final class AddressBarButtonsViewController: NSViewController { } trackerAnimationView?.isHidden = false trackerAnimationView?.reloadImages() + self.updateZoomButtonVisibility(animation: true) trackerAnimationView?.play { [weak self] _ in trackerAnimationView?.isHidden = true self?.updatePrivacyEntryPointIcon() self?.updatePermissionButtons() self?.playBadgeAnimationIfNecessary() + self?.updateZoomButtonVisibility(animation: false) } } @@ -862,6 +908,14 @@ final class AddressBarButtonsViewController: NSViewController { .sink { [weak self] _ in self?.setupAnimationViews() self?.updatePrivacyEntryPointIcon() + self?.updateZoomButtonVisibility() + } + } + + private func subscribeToTabZoomLevel() { + accessibilityPreferencesCancellable = tabViewModel?.zoomLevelSubject + .sink { [weak self] _ in + self?.updateZoomButtonVisibility() } } @@ -915,8 +969,10 @@ extension AddressBarButtonsViewController: NSPopoverDelegate { NotificationCenter.default.post(name: .bookmarkPromptShouldShow, object: nil) } updateBookmarkButtonVisibility() - - default: break + case popovers.zoomPopover: + updateZoomButtonVisibility() + default: + break } } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 42c1ce7869..37833f9c56 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -147,6 +147,7 @@ final class AddressBarViewController: NSViewController { registerForMouseEnteredAndExitedEvents() subscribeToButtonsWidth() subscribeForShadowViewUpdates() + } // swiftlint:disable notification_center_detachment diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 8adccabf28..f2092a4641 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -39,6 +39,7 @@ protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedPrint(_ menu: NSMenu) func optionsButtonMenuRequestedPreferences(_ menu: NSMenu) func optionsButtonMenuRequestedAppearancePreferences(_ menu: NSMenu) + func optionsButtonMenuRequestedAccessibilityPreferences(_ menu: NSMenu) #if DBP func optionsButtonMenuRequestedDataBrokerProtection(_ menu: NSMenu) #endif @@ -223,6 +224,10 @@ final class MoreOptionsMenu: NSMenu { actionDelegate?.optionsButtonMenuRequestedAppearancePreferences(self) } + @objc func openAccessibilityPreferences(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedAccessibilityPreferences(self) + } + @objc func openSubscriptionPurchasePage(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedSubscriptionPurchasePage(self) } @@ -606,7 +611,7 @@ final class ZoomSubMenu: NSMenu { addItem(.separator()) - let globalZoomSettingItem = NSMenuItem(title: UserText.defaultZoomPageMoreOptionsItem, action: #selector(MoreOptionsMenu.openAppearancePreferences(_:)), keyEquivalent: "") + let globalZoomSettingItem = NSMenuItem(title: UserText.defaultZoomPageMoreOptionsItem, action: #selector(MoreOptionsMenu.openAccessibilityPreferences(_:)), keyEquivalent: "") .targetting(target) addItem(globalZoomSettingItem) } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index ad0980e11b..a73ff36fd3 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -1,5 +1,5 @@ - + @@ -692,6 +692,30 @@ + @@ -828,11 +852,13 @@ + + @@ -864,6 +890,7 @@ + @@ -893,6 +920,7 @@ + diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift index 8480ad9298..fe1c986a92 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift @@ -65,6 +65,9 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter { private(set) var bookmarkPopover: AddBookmarkPopover? private weak var bookmarkPopoverDelegate: NSPopoverDelegate? + private (set) var zoomPopover: ZoomPopover? + private weak var zoomPopoverDelegate: NSPopoverDelegate? + private let networkProtectionPopoverManager: NetPPopoverManager private var popoverIsShownCancellables = Set() @@ -109,6 +112,10 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter { bookmarkPopover?.isShown ?? false } + var isZoomPopoverShown: Bool { + zoomPopover?.isShown ?? false + } + func bookmarksButtonPressed(_ button: MouseOverButton, popoverDelegate delegate: NSPopoverDelegate, tab: Tab?) { if bookmarkListPopoverShown { bookmarkListPopover?.close() @@ -200,6 +207,10 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter { bookmarkPopover?.close() } + if zoomPopover?.isShown ?? false { + zoomPopover?.close() + } + if privacyDashboardPopover?.isShown ?? false { privacyDashboardPopover?.close() } @@ -234,10 +245,24 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter { show(bookmarkPopover, positionedBelow: button) } + func showZoomPopover(for tabViewModel: TabViewModel, from button: NSButton, withDelegate delegate: NSPopoverDelegate) { + guard closeTransientPopovers() else { return } + + let zoomPopover = ZoomPopover(tabViewModel: tabViewModel) + zoomPopover.delegate = self + self.zoomPopover = zoomPopover + self.zoomPopoverDelegate = delegate + show(zoomPopover, positionedBelow: button) + } + func closeEditBookmarkPopover() { bookmarkPopover?.close() } + func closeZoomPopover() { + zoomPopover?.close() + } + func openPrivacyDashboard(for tabViewModel: TabViewModel, from button: MouseOverButton) { guard closeTransientPopovers() else { return } @@ -448,6 +473,10 @@ extension NavigationBarPopovers: NSPopoverDelegate { privacyInfoCancellable = nil privacyDashboadPendingUpdatesCancellable = nil + case zoomPopover: + zoomPopoverDelegate?.popoverDidClose?(notification) + zoomPopover = nil + default: break } } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 1addc31915..cab1a8fa70 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -1027,6 +1027,10 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .appearance) } + func optionsButtonMenuRequestedAccessibilityPreferences(_ menu: NSMenu) { + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .accessibility) + } + func optionsButtonMenuRequestedSubscriptionPurchasePage(_ menu: NSMenu) { WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) diff --git a/DuckDuckGo/NavigationBar/View/ZoomPopover.swift b/DuckDuckGo/NavigationBar/View/ZoomPopover.swift new file mode 100644 index 0000000000..c5e8af65f2 --- /dev/null +++ b/DuckDuckGo/NavigationBar/View/ZoomPopover.swift @@ -0,0 +1,157 @@ +// +// ZoomPopover.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 Combine + +struct ZoomPopoverContentView: View { + @ObservedObject var viewModel: ZoomPopoverViewModel + + var body: some View { + HStack(alignment: .center, spacing: 8) { + Text(viewModel.zoomLevel.displayString) + .frame(width: 50, height: 28) + .padding(.horizontal, 8) + + Button { + viewModel.reset() + } label: { + Text(UserText.resetZoom) + .frame(height: 28) + .fixedSize(horizontal: true, vertical: false) + .padding(.horizontal, 8) + } + + HStack(spacing: 1) { + Button { + viewModel.zoomOut() + } label: { + Image(systemName: "minus") + .frame(width: 32, height: 28) + } + Button { + viewModel.zoomIn() + } label: { + Image(systemName: "plus") + .frame(width: 32, height: 28) + } + } + } + .frame(height: 52) + .padding(.horizontal, 16) + } +} + +final class ZoomPopoverViewModel: ObservableObject { + let tabViewModel: TabViewModel + @Published var zoomLevel: DefaultZoomValue = .percent100 + private var cancellables = Set() + + init(tabViewModel: TabViewModel) { + self.tabViewModel = tabViewModel + zoomLevel = tabViewModel.zoomLevel + tabViewModel.zoomLevelSubject + .receive(on: DispatchQueue.main) + .sink { [weak self] newValue in + self?.zoomLevel = newValue + }.store(in: &cancellables) + } + + func zoomIn() { + tabViewModel.tab.webView.zoomIn() + } + + func zoomOut() { + tabViewModel.tab.webView.zoomOut() + } + + func reset() { + tabViewModel.tab.webView.resetZoomLevel() + } + +} + +final class ZoomPopoverViewController: NSViewController { + let viewModel: ZoomPopoverViewModel + + init(viewModel: ZoomPopoverViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let swiftUIView = ZoomPopoverContentView(viewModel: viewModel) + view = NSHostingView(rootView: swiftUIView) + } +} + +final class ZoomPopover: NSPopover { + + var tabViewModel: TabViewModel + + private weak var addressBar: NSView? + + /// prefferred bounding box for the popover positioning + override var boundingFrame: NSRect { + guard let addressBar, + let window = addressBar.window else { return .infinite } + var frame = window.convertToScreen(addressBar.convert(addressBar.bounds, to: nil)) + frame = frame.insetBy(dx: 0, dy: -window.frame.size.height) + return frame + } + + /// position popover to the right + override func adjustFrame(_ frame: NSRect) -> NSRect { + let boundingFrame = self.boundingFrame + guard !boundingFrame.isInfinite else { return frame } + var frame = frame + frame.origin.x = boundingFrame.minX + return frame + } + + init(tabViewModel: TabViewModel) { + self.tabViewModel = tabViewModel + super.init() + + self.animates = false + self.behavior = .semitransient + setupContentController() + } + + required init?(coder: NSCoder) { + fatalError("BookmarksPopover: Bad initializer") + } + + // swiftlint:disable force_cast + var viewController: ZoomPopoverViewController { contentViewController as! ZoomPopoverViewController } + // swiftlint:enable force_cast + + private func setupContentController() { + let controller = ZoomPopoverViewController(viewModel: ZoomPopoverViewModel(tabViewModel: tabViewModel)) + contentViewController = controller + } + + override func show(relativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge) { + self.addressBar = positioningView.superview + super.show(relativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge) + } +} diff --git a/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift b/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift index c096191757..dc4f32294a 100644 --- a/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AccessibilityPreferences.swift @@ -20,14 +20,24 @@ import Foundation import AppKit import Bookmarks import Common +import Combine protocol AccessibilityPreferencesPersistor { var defaultPageZoom: CGFloat { get set } + var zoomPerWebsite: [String: CGFloat] { get set } +} + +protocol SavedZoomLevelsCoordinating { + func burnZoomLevels(except fireproofDomains: FireproofDomains) + func burnZoomLevel(of baseDomains: Set) } struct AccessibilityPreferencesUserDefaultsPersistor: AccessibilityPreferencesPersistor { @UserDefaultsWrapper(key: .defaultPageZoom, defaultValue: DefaultZoomValue.percent100.rawValue) var defaultPageZoom: CGFloat + + @UserDefaultsWrapper(key: .websitePageZoom, defaultValue: [:]) + var zoomPerWebsite: [String: CGFloat] } enum DefaultZoomValue: CGFloat, CaseIterable { @@ -60,10 +70,47 @@ final class AccessibilityPreferences: ObservableObject { } } + let zoomPerWebsiteUpdatedSubject = PassthroughSubject() + private var zoomPerWebsite: [String: DefaultZoomValue] { + didSet { + persistor.zoomPerWebsite = zoomPerWebsite.mapValues { $0.rawValue } + zoomPerWebsiteUpdatedSubject.send() + } + } + + private var persistor: AccessibilityPreferencesPersistor + init(persistor: AccessibilityPreferencesPersistor = AccessibilityPreferencesUserDefaultsPersistor()) { self.persistor = persistor defaultPageZoom = .init(rawValue: persistor.defaultPageZoom) ?? .percent100 + zoomPerWebsite = persistor.zoomPerWebsite.compactMapValues { DefaultZoomValue(rawValue: $0) } } - private var persistor: AccessibilityPreferencesPersistor + func zoomPerWebsite(url: String) -> DefaultZoomValue? { + guard let domain = TLD().eTLDplus1(forStringURL: url) else { return nil } + return zoomPerWebsite[domain] + } + + func updateZoomPerWebsite(zoomLevel: DefaultZoomValue, url: String) { + guard let domain = TLD().eTLDplus1(forStringURL: url) else { return } + if zoomLevel == defaultPageZoom { + zoomPerWebsite[domain] = nil + } else { + zoomPerWebsite[domain] = zoomLevel + } + } +} + +extension AccessibilityPreferences: SavedZoomLevelsCoordinating { + func burnZoomLevels(except fireproofDomains: FireproofDomains) { + zoomPerWebsite = zoomPerWebsite.filter { + fireproofDomains.isFireproof(fireproofDomain: $0.key) + } + } + + func burnZoomLevel(of baseDomains: Set) { + for website in zoomPerWebsite.keys where baseDomains.contains(website) { + zoomPerWebsite[website] = nil + } + } } diff --git a/DuckDuckGo/Tab/View/WebView.swift b/DuckDuckGo/Tab/View/WebView.swift index 8c9cd2d1f2..16bdc2bbdb 100644 --- a/DuckDuckGo/Tab/View/WebView.swift +++ b/DuckDuckGo/Tab/View/WebView.swift @@ -30,11 +30,16 @@ protocol WebViewInteractionEventsDelegate: AnyObject { func webView(_ webView: WebView, scrollWheel event: NSEvent) } +protocol WebViewZoomLevelDelegate: AnyObject { + func zoomWasSet(to level: DefaultZoomValue) + } + @objc(DuckDuckGo_WebView) final class WebView: WKWebView { weak var contextMenuDelegate: WebViewContextMenuDelegate? weak var interactionEventsDelegate: WebViewInteractionEventsDelegate? + weak var zoomLevelDelegate: WebViewZoomLevelDelegate? private var isLoadingObserver: Any? @@ -101,6 +106,12 @@ final class WebView: WKWebView { return DefaultZoomValue(rawValue: pageZoom) ?? .percent100 } set { + // There are cases where the pageZoom does not reflect the actual display, such as after a command-click on a link. + // The API may not trigger a change if the new value is the same as the current value. + if pageZoom == newValue.rawValue { + // Slightly modify the value to force the API to trigger a change. + pageZoom = newValue.rawValue - 0.001 + } pageZoom = newValue.rawValue } } @@ -124,16 +135,19 @@ final class WebView: WKWebView { func resetZoomLevel() { magnification = 1 zoomLevel = defaultZoomValue + zoomLevelDelegate?.zoomWasSet(to: zoomLevel) } func zoomIn() { guard canZoomIn else { return } zoomLevel = DefaultZoomValue.allCases[self.zoomLevel.index + 1] + zoomLevelDelegate?.zoomWasSet(to: zoomLevel) } func zoomOut() { guard canZoomOut else { return } zoomLevel = DefaultZoomValue.allCases[self.zoomLevel.index - 1] + zoomLevelDelegate?.zoomWasSet(to: zoomLevel) } // MARK: - Menu diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index e9726a5dc0..04e3d1e4cb 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -75,6 +75,16 @@ final class TabViewModel { @Published private(set) var usedPermissions = Permissions() @Published private(set) var permissionAuthorizationQuery: PermissionAuthorizationQuery? + let zoomLevelSubject = PassthroughSubject() + private (set) var zoomLevel: DefaultZoomValue = .percent100 { + didSet { + self.tab.webView.zoomLevel = zoomLevel + if oldValue != zoomLevel { + zoomLevelSubject.send(zoomLevel) + } + } + } + var canPrint: Bool { !isShowingErrorPage && canReload && tab.webView.canPrint } @@ -102,7 +112,7 @@ final class TabViewModel { self.tab = tab self.appearancePreferences = appearancePreferences self.accessibilityPreferences = accessibilityPreferences - + zoomLevel = accessibilityPreferences.defaultPageZoom subscribeToUrl() subscribeToCanGoBackForwardAndReload() subscribeToTitle() @@ -131,7 +141,7 @@ final class TabViewModel { return Empty().eraseToAnyPublisher() case .url(let url, _, source: .webViewUpdated), - .url(let url, _, source: .link): + .url(let url, _, source: .link): guard !url.isEmpty, url != .blankPage, !url.isDuckPlayer else { fallthrough } @@ -144,21 +154,21 @@ final class TabViewModel { .asVoid().eraseToAnyPublisher() case .url(_, _, source: .pendingStateRestoration), - .url(_, _, source: .loadedByStateRestoration), - .url(_, _, source: .userEntered), - .url(_, _, source: .historyEntry), - .url(_, _, source: .bookmark), - .url(_, _, source: .ui), - .url(_, _, source: .appOpenUrl), - .url(_, _, source: .reload), - .newtab, - .settings, - .bookmarks, - .onboarding, - .none, - .dataBrokerProtection, - .subscription, - .identityTheftRestoration: + .url(_, _, source: .loadedByStateRestoration), + .url(_, _, source: .userEntered), + .url(_, _, source: .historyEntry), + .url(_, _, source: .bookmark), + .url(_, _, source: .ui), + .url(_, _, source: .appOpenUrl), + .url(_, _, source: .reload), + .newtab, + .settings, + .bookmarks, + .onboarding, + .none, + .dataBrokerProtection, + .subscription, + .identityTheftRestoration: // Update the address bar instantly for built-in content types or user-initiated navigations return Just( () ).eraseToAnyPublisher() } @@ -170,6 +180,7 @@ final class TabViewModel { updateAddressBarStrings() updateFavicon() updateCanBeBookmarked() + updateZoomForWebsite() } .store(in: &cancellables) } @@ -231,19 +242,42 @@ final class TabViewModel { } private func subscribeToPreferences() { + self.tab.webView.zoomLevelDelegate = self appearancePreferences.$showFullURL.dropFirst().sink { [weak self] showFullURL in self?.updatePassiveAddressBarString(showFullURL: showFullURL) }.store(in: &cancellables) accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } self.tab.webView.defaultZoomValue = newValue - self.tab.webView.zoomLevel = newValue + if !isThereZoomPerWebsite { + self.zoomLevel = newValue + } }.store(in: &cancellables) + accessibilityPreferences.zoomPerWebsiteUpdatedSubject + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateZoomForWebsite() + }.store(in: &cancellables) + } + + private var isThereZoomPerWebsite: Bool { + guard let urlString = tab.url?.absoluteString else { return false } + guard !tab.burnerMode.isBurner else { return false } + return accessibilityPreferences.zoomPerWebsite(url: urlString) != nil + } + + private func updateZoomForWebsite() { + guard let urlString = tab.url?.absoluteString else { return } + guard !tab.burnerMode.isBurner else { return } + let zoomToApply: DefaultZoomValue = accessibilityPreferences.zoomPerWebsite(url: urlString) ?? accessibilityPreferences.defaultPageZoom + self.zoomLevel = zoomToApply } private func subscribeToWebViewDidFinishNavigation() { tab.webViewDidFinishNavigationPublisher.sink { [weak self] in - self?.sendAnimationTrigger() + guard let self = self else { return } + self.sendAnimationTrigger() + self.updateZoomForWebsite() }.store(in: &cancellables) } @@ -272,21 +306,21 @@ final class TabViewModel { let showFullURL = showFullURL ?? appearancePreferences.showFullURL passiveAddressBarAttributedString = switch tab.content { case .newtab, .onboarding, .none: - .init() // empty + .init() // empty case .settings: - .settingsTrustedIndicator + .settingsTrustedIndicator case .bookmarks: - .bookmarksTrustedIndicator + .bookmarksTrustedIndicator case .dataBrokerProtection: - .dbpTrustedIndicator + .dbpTrustedIndicator case .subscription: - .subscriptionTrustedIndicator + .subscriptionTrustedIndicator case .identityTheftRestoration: - .identityTheftRestorationTrustedIndicator + .identityTheftRestorationTrustedIndicator case .url(let url, _, _) where url.isDuckPlayer: - .duckPlayerTrustedIndicator + .duckPlayerTrustedIndicator case .url(let url, _, _) where url.isEmailProtection: - .emailProtectionTrustedIndicator + .emailProtectionTrustedIndicator case .url(let url, _, _): NSAttributedString(string: passiveAddressBarString(with: url, showFullURL: showFullURL)) } @@ -313,7 +347,7 @@ final class TabViewModel { private func updateTitle() { // swiftlint:disable:this cyclomatic_complexity var title: String switch tab.content { - // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors + // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors case _ where isShowingErrorPage && (tab.error?.code != .webContentProcessTerminated || tab.title == nil): if tab.error?.errorCode == NSURLErrorServerCertificateUntrusted { title = UserText.sslErrorPageTabTitle @@ -386,6 +420,7 @@ final class TabViewModel { func reload() { tab.reload() updateAddressBarStrings() + self.updateZoomForWebsite() } private func errorFaviconToShow(error: WKError?) -> NSImage { @@ -440,6 +475,17 @@ extension TabViewModel: TabDataClearing { } +extension TabViewModel: WebViewZoomLevelDelegate { + func zoomWasSet(to level: DefaultZoomValue) { + zoomLevel = level + guard let urlString = tab.url?.absoluteString else { return } + guard !tab.burnerMode.isBurner else { return } + if accessibilityPreferences.zoomPerWebsite(url: urlString) != level { + accessibilityPreferences.updateZoomPerWebsite(zoomLevel: level, url: urlString) + } + } +} + private extension NSAttributedString { private typealias Component = NSAttributedString diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index 9323afb45c..37325e3063 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -861,6 +861,41 @@ class AddressBarTests: XCTestCase { let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) } + + @MainActor + func test_ZoomLevelNonDefault_ThenZoomButtonIsVisible() async throws { + // GIVEN + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered(""))) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + viewModel.selectedTabViewModel?.zoomWasSet(to: .percent150) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let zoomButton = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.zoomButton! + XCTAssertFalse(zoomButton.isHidden) + } + + @MainActor + func test_ZoomLevelDefault_ThenZoomButtonIsNotVisible() async throws { + // GIVEN + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered(""))) + tab.webView.zoomLevel = AccessibilityPreferences.shared.defaultPageZoom + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + viewModel.selectedTabViewModel?.zoomWasSet(to: .percent100) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let zoomButton = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.zoomButton! + XCTAssertTrue(zoomButton.isHidden) + } } protocol MainActorPerformer { diff --git a/UnitTests/Fire/Model/FireTests.swift b/UnitTests/Fire/Model/FireTests.swift index 2d9340bb09..9c4c15898b 100644 --- a/UnitTests/Fire/Model/FireTests.swift +++ b/UnitTests/Fire/Model/FireTests.swift @@ -100,6 +100,7 @@ final class FireTests: XCTestCase { func testWhenBurnAll_ThenAllWebsiteDataAreRemoved() { let manager = WebCacheManagerMock() let historyCoordinator = HistoryCoordinatingMock() + let zoomLevelsCoordinator = MockSavedZoomCoordinator() let permissionManager = PermissionManagerMock() let faviconManager = FaviconManagerMock() let recentlyClosedCoordinator = RecentlyClosedCoordinatorMock() @@ -107,6 +108,7 @@ final class FireTests: XCTestCase { let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, permissionManager: permissionManager, + savedZoomLevelsCoordinating: zoomLevelsCoordinator, windowControllerManager: WindowControllersManager.shared, faviconManagement: faviconManager, recentlyClosedCoordinator: recentlyClosedCoordinator, @@ -124,6 +126,7 @@ final class FireTests: XCTestCase { XCTAssert(historyCoordinator.burnAllCalled) XCTAssert(permissionManager.burnPermissionsCalled) XCTAssert(recentlyClosedCoordinator.burnCacheCalled) + XCTAssert(zoomLevelsCoordinator.burnAllZoomLevelsCalled) } func testWhenBurnAllThenBurningFlagToggles() { @@ -192,6 +195,18 @@ final class FireTests: XCTestCase { XCTAssertFalse(appStateRestorationManager.canRestoreLastSessionState) } + func testWhenBurnDomainsIsCalledThenSelectedDomainsZoomLevelsAreBurned() { + let domainsToBurn: Set = ["test.com", "provola.co.uk"] + let zoomLevelsCoordinator = MockSavedZoomCoordinator() + let fire = Fire(savedZoomLevelsCoordinating: zoomLevelsCoordinator, + tld: ContentBlocking.shared.tld) + + fire.burnEntity(entity: .none(selectedDomains: domainsToBurn)) + + XCTAssertTrue(zoomLevelsCoordinator.burnZoomLevelsOfDomainsCalled) + XCTAssertEqual(zoomLevelsCoordinator.domainsBurned, domainsToBurn) + } + func testWhenBurnVisitIsCalledForTodayThenAllExistingTabsAreCleared() { let manager = WebCacheManagerMock() let historyCoordinator = HistoryCoordinatingMock() @@ -291,3 +306,18 @@ fileprivate extension TabCollectionViewModel { } } + +class MockSavedZoomCoordinator: SavedZoomLevelsCoordinating { + var burnAllZoomLevelsCalled = false + var burnZoomLevelsOfDomainsCalled = false + var domainsBurned: Set = [] + + func burnZoomLevels(except fireproofDomains: DuckDuckGo_Privacy_Browser.FireproofDomains) { + burnAllZoomLevelsCalled = true + } + + func burnZoomLevel(of baseDomains: Set) { + burnZoomLevelsOfDomainsCalled = true + domainsBurned = baseDomains + } +} diff --git a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift index 05e9c206e7..7d6145b68f 100644 --- a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift +++ b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift @@ -23,6 +23,7 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { var optionsButtonMenuRequestedPreferencesCalled = false var optionsButtonMenuRequestedAppearancePreferencesCalled = false + var optionsButtonMenuRequestedAccessibilityPreferencesCalled = false var optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = false func optionsButtonMenuRequestedDataBrokerProtection(_ menu: NSMenu) { @@ -96,4 +97,8 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { func optionsButtonMenuRequestedIdentityTheftRestoration(_ menu: NSMenu) { } + + func optionsButtonMenuRequestedAccessibilityPreferences(_ menu: NSMenu) { + optionsButtonMenuRequestedAccessibilityPreferencesCalled = true + } } diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index e5ed58d0f8..f9680be11d 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -112,7 +112,7 @@ final class MoreOptionsMenuTests: XCTestCase { zoomSubmenu.performActionForItem(at: defaultZoomItemIndex) - XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedAppearancePreferencesCalled) + XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedAccessibilityPreferencesCalled) } // MARK: Preferences diff --git a/UnitTests/Preferences/AccessibilityPreferencesTests.swift b/UnitTests/Preferences/AccessibilityPreferencesTests.swift index 819f2c6a0a..8354d7bb1e 100644 --- a/UnitTests/Preferences/AccessibilityPreferencesTests.swift +++ b/UnitTests/Preferences/AccessibilityPreferencesTests.swift @@ -18,27 +18,169 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser - -class MockAccessibilityPreferencesPersistor: AccessibilityPreferencesPersistor { - var defaultPageZoom: CGFloat = DefaultZoomValue.percent100.rawValue -} +import Combine class AccessibilityPreferencesTests: XCTestCase { - func testWhenInitializedThenItLoadsPersistedDefaultPageZoom() { - let mockPersistor = MockAccessibilityPreferencesPersistor() - mockPersistor.defaultPageZoom = DefaultZoomValue.percent150.rawValue + let website1 = "https://www.bbc.com" + let website2 = "https://duckduckgo.com" + let website3 = "https://www.test.com" + let website4 = "https://somesite.it" + let domain1 = "bbc.com" + let domain2 = "duckduckgo.com" + let domain3 = "test.com" + let domain4 = "somesite.it" + + var zoom1: DefaultZoomValue! + var zoom2: DefaultZoomValue! + var zoom3: DefaultZoomValue! + var zoom4: DefaultZoomValue! + var mockPersistor: MockAccessibilityPreferencesPersistor! + private var cancellables = Set() + + override func setUp() { + UserDefaultsWrapper.clearAll() + mockPersistor = MockAccessibilityPreferencesPersistor() + let filteredCases = DefaultZoomValue.allCases.filter { $0 != .percent100 } + zoom1 = filteredCases.randomElement()! + zoom2 = filteredCases.randomElement()! + zoom3 = filteredCases.randomElement()! + zoom4 = filteredCases.randomElement()! + } + + override func tearDown() { + mockPersistor = nil + zoom1 = nil + zoom2 = nil + zoom3 = nil + zoom4 = nil + UserDefaultsWrapper.clearAll() + } + + func test_whenPreferencesInitialized_thenItLoadsPersistedDefaultPageZoom() { + // GIVEN + let randomZoom = DefaultZoomValue.allCases.randomElement()! + mockPersistor.defaultPageZoom = randomZoom.rawValue + + // WHEN let accessibilityPreferences = AccessibilityPreferences(persistor: mockPersistor) - XCTAssertEqual(accessibilityPreferences.defaultPageZoom, DefaultZoomValue.percent150) + // THEN + XCTAssertEqual(accessibilityPreferences.defaultPageZoom, randomZoom) } - func testWhenDefaultPageZoomUpdatedThenPersistorUpdates() { - let mockPersistor = MockAccessibilityPreferencesPersistor() + func test_whenDefaultPageZoomUpdated_ThenPersistorUpdatesDefaultZoom() { + // GIVEN + let randomZoom = DefaultZoomValue.allCases.randomElement()! let accessibilityPreferences = AccessibilityPreferences(persistor: mockPersistor) - accessibilityPreferences.defaultPageZoom = .percent75 - XCTAssertEqual(mockPersistor.defaultPageZoom, DefaultZoomValue.percent75.rawValue) + // WHEN + accessibilityPreferences.defaultPageZoom = randomZoom + + // THEN + XCTAssertEqual(mockPersistor.defaultPageZoom, randomZoom.rawValue) + } + + func test_whenZoomLevelPerWebsiteChangedInPreferences_thenThePersisterAndUserDefaultsZoomPerWebsiteValuesAreUpdated() { + // GIVEN + let expectation = XCTestExpectation() + UserDefaultsWrapper.clearAll() + let persister = AccessibilityPreferencesUserDefaultsPersistor() + let model = AccessibilityPreferences(persistor: persister) + model.zoomPerWebsiteUpdatedSubject + .receive(on: DispatchQueue.main) + .sink { _ in + expectation.fulfill() + }.store(in: &cancellables) + + // WHEN + model.updateZoomPerWebsite(zoomLevel: zoom1, url: website1) + model.updateZoomPerWebsite(zoomLevel: zoom2, url: website2) + wait(for: [expectation], timeout: 5) + + // THEN + XCTAssertEqual(model.zoomPerWebsite(url: website1), zoom1) + XCTAssertEqual(model.zoomPerWebsite(url: website2), zoom2) + XCTAssertEqual(persister.zoomPerWebsite[domain1], zoom1.rawValue) + XCTAssertEqual(persister.zoomPerWebsite[domain2], zoom2.rawValue) } + func test_whenBurningZoomLevels_thenOnlyFireproofSiteZoomLevelAreRetained() { + // GIVEN + let expectation = XCTestExpectation() + UserDefaultsWrapper.clearAll() + let persister = AccessibilityPreferencesUserDefaultsPersistor() + let model = AccessibilityPreferences(persistor: persister) + model.updateZoomPerWebsite(zoomLevel: zoom1, url: website1) + model.updateZoomPerWebsite(zoomLevel: zoom2, url: website2) + model.updateZoomPerWebsite(zoomLevel: zoom3, url: website3) + model.updateZoomPerWebsite(zoomLevel: zoom4, url: website4) + let fireProofDomains = MockFireproofDomains(domains: [website1, website3]) + model.zoomPerWebsiteUpdatedSubject + .receive(on: DispatchQueue.main) + .sink { _ in + expectation.fulfill() + }.store(in: &cancellables) + + // WHEN + model.burnZoomLevels(except: fireProofDomains) + + wait(for: [expectation], timeout: 5) + + // THEN + XCTAssertEqual(model.zoomPerWebsite(url: website1), zoom1) + XCTAssertNil(model.zoomPerWebsite(url: website2)) + XCTAssertEqual(model.zoomPerWebsite(url: website3), zoom3) + XCTAssertNil(model.zoomPerWebsite(url: website4)) + XCTAssertEqual(persister.zoomPerWebsite[domain1], zoom1.rawValue) + XCTAssertNil(persister.zoomPerWebsite[domain2]) + XCTAssertEqual(persister.zoomPerWebsite[domain3], zoom3.rawValue) + XCTAssertNil(persister.zoomPerWebsite[domain4]) + } + + func test_whenBurningZoomLevelsPerSites_thenZoomLevelOfTheSiteIsNotRetained() { + // GIVEN + let expectation = XCTestExpectation() + UserDefaultsWrapper.clearAll() + let persister = AccessibilityPreferencesUserDefaultsPersistor() + let model = AccessibilityPreferences(persistor: persister) + model.updateZoomPerWebsite(zoomLevel: zoom1, url: website1) + model.updateZoomPerWebsite(zoomLevel: zoom2, url: website2) + model.updateZoomPerWebsite(zoomLevel: zoom3, url: website3) + model.updateZoomPerWebsite(zoomLevel: zoom4, url: website4) + model.zoomPerWebsiteUpdatedSubject + .receive(on: DispatchQueue.main) + .sink { _ in + expectation.fulfill() + }.store(in: &cancellables) + + // WHEN + model.burnZoomLevel(of: [domain1, domain4]) + wait(for: [expectation], timeout: 5) + + // THEN + XCTAssertNil(model.zoomPerWebsite(url: website1)) + XCTAssertEqual(model.zoomPerWebsite(url: website2), zoom2) + XCTAssertEqual(model.zoomPerWebsite(url: website3), zoom3) + XCTAssertNil(model.zoomPerWebsite(url: website4)) + XCTAssertNil(persister.zoomPerWebsite[domain1]) + XCTAssertEqual(persister.zoomPerWebsite[domain2], zoom2.rawValue) + XCTAssertEqual(persister.zoomPerWebsite[domain3], zoom3.rawValue) + XCTAssertNil(persister.zoomPerWebsite[domain4]) + } + +} + +class MockAccessibilityPreferencesPersistor: AccessibilityPreferencesPersistor { + var zoomPerWebsite: [String: CGFloat] = [:] + var defaultPageZoom: CGFloat = DefaultZoomValue.percent100.rawValue +} + +class MockFireproofDomains: FireproofDomains { + init(domains: [String]) { + super.init(store: FireproofDomainsStoreMock()) + for domain in domains { + super.add(domain: domain) + } + } } diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index d06378f00f..c2e83d6fbb 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -242,19 +242,21 @@ final class TabViewModelTests: XCTestCase { } @MainActor - func testWhenAppearancePreferencesZoomLevelIsSetThenTabsWebViewZoomLevelIsUpdated() { + func testWhenPreferencesDefaultZoomLevelIsSetThenTabsWebViewZoomLevelIsUpdated() { UserDefaultsWrapper.clearAll() let tabVM = TabViewModel(tab: Tab()) - let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) } @MainActor - func testWhenAppearancePreferencesZoomLevelIsSetAndANewTabIsOpenThenItsWebViewHasTheLatestValueOfZoomLevel() { + func testWhenPreferencesDefaultZoomLevelIsSetAndANewTabIsOpenThenItsWebViewHasTheLatestValueOfZoomLevel() { UserDefaultsWrapper.clearAll() - let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! AccessibilityPreferences.shared.defaultPageZoom = randomZoomLevel let tabVM = TabViewModel(tab: Tab(), appearancePreferences: AppearancePreferences()) @@ -262,6 +264,251 @@ final class TabViewModelTests: XCTestCase { XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) } + @MainActor + func test_WhenPreferencesDefaultZoomLevelIsSet_AndThereIsAZoomLevelForWebsite_ThenTabsWebViewZoomLevelIsNotUpdated() { + // GIVEN + UserDefaultsWrapper.clearAll() + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + var tab = Tab(url: url) + var tabVM = TabViewModel(tab: tab) + + // WHEN + AccessibilityPreferences.shared.defaultPageZoom = .percent50 + tab = Tab(url: url) + tabVM = TabViewModel(tab: tab) + + // THEN + XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) + } + + @MainActor + func test_WhenPreferencesDefaultZoomLevelIsSet_AndThereIsAZoomLevelForWebsite_AndIsFireWindow_ThenTabsWebViewZoomLevelIsNotUpdated() { + // GIVEN + UserDefaultsWrapper.clearAll() + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + var tab = Tab(url: url) + var tabVM = TabViewModel(tab: tab) + + // WHEN + AccessibilityPreferences.shared.defaultPageZoom = .percent50 + let burnerTab = Tab(content: .url(url, credential: nil, source: .ui), burnerMode: BurnerMode(isBurner: true)) + tabVM = TabViewModel(tab: burnerTab) + + // THEN + XCTAssertEqual(tabVM.tab.webView.zoomLevel, AccessibilityPreferences.shared.defaultPageZoom) + } + + @MainActor + func test_WhenPreferencesZoomPerWebsiteLevelIsSet_AndANewTabIsOpen_ThenItsWebViewHasTheLatestValueOfZoomLevel() { + // GIVEN + UserDefaultsWrapper.clearAll() + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + + // WHEN + let tab = Tab(url: url) + let tabVM = TabViewModel(tab: tab, appearancePreferences: AppearancePreferences()) + + // THEN + XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel) + } + + @MainActor + func test_WhenPreferencesZoomPerWebsiteLevelIsSet_AndANewBurnerTabIsOpen_ThenItsWebViewHasTheDefaultZoomLevel() { + // GIVEN + UserDefaultsWrapper.clearAll() + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + + // WHEN + let burnerTab = Tab(content: .url(url, credential: nil, source: .ui), burnerMode: BurnerMode(isBurner: true)) + let tabVM = TabViewModel(tab: burnerTab, appearancePreferences: AppearancePreferences()) + + // THEN + XCTAssertEqual(tabVM.tab.webView.zoomLevel, AccessibilityPreferences.shared.defaultPageZoom) + } + + @MainActor + func test_WhenPreferencesZoomPerWebsiteLevelIsSet_ThenTabsWebViewZoomLevelIsUpdated() async { + // GIVEN + UserDefaultsWrapper.clearAll() + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + let tab = Tab(url: url) + let tabVM = TabViewModel(tab: tab) + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + + // WHEN + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + + // THEN + await MainActor.run { + XCTAssertEqual(tabVM.tab.webView.zoomLevel, randomZoomLevel, "Tab's web view zoom level was not updated as expected.") + } + } + + @MainActor + func test_WhenPreferencesZoomPerWebsiteLevelIsSet_AndIsFireWindow_ThenTabsWebViewZoomLevelIsNot() async { + // GIVEN + UserDefaultsWrapper.clearAll() + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + let burnerTab = Tab(content: .url(url, credential: nil, source: .ui), burnerMode: BurnerMode(isBurner: true)) + let tabVM = TabViewModel(tab: burnerTab) + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + + // WHEN + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + + // THEN + await MainActor.run { + XCTAssertEqual(tabVM.tab.webView.zoomLevel, AccessibilityPreferences.shared.defaultPageZoom) + } + } + + @MainActor + func test_WhenZoomWasSetIsCalled_ThenAppearancePreferencesPerWebsiteZoomIsSet() { + // GIVEN + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + UserDefaultsWrapper.clearAll() + let tab = Tab(url: url) + let tabVM = TabViewModel(tab: tab) + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom} + let randomZoomLevel = filteredCases.randomElement()! + + // WHEN + tabVM.zoomWasSet(to: randomZoomLevel) + + // THEN + XCTAssertEqual(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL), randomZoomLevel) + } + + @MainActor + func test_WhenZoomWasSetIsCalled_AndIsFireWindow_ThenAppearancePreferencesPerWebsiteZoomIsNotSet() { + // GIVEN + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: AccessibilityPreferences.shared.defaultPageZoom, url: hostURL) + UserDefaultsWrapper.clearAll() + let burnerTab = Tab(content: .url(url, credential: nil, source: .ui), burnerMode: BurnerMode(isBurner: true)) + let tabVM = TabViewModel(tab: burnerTab) + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom} + let randomZoomLevel = filteredCases.randomElement()! + + // WHEN + print(randomZoomLevel) + tabVM.zoomWasSet(to: randomZoomLevel) + + // THEN + XCTAssertEqual(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL), nil) + } + + @MainActor + func test_WhenWebViewResetZoomLevelForASite_ThenNoZoomSavedForTheSite() { + // GIVEN + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + UserDefaultsWrapper.clearAll() + let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } + let randomZoomLevel = filteredCases.randomElement()! + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + let tab = Tab(url: url) + let tabView = TabViewModel(tab: tab) + + // WHEN + tabView.tab.webView.resetZoomLevel() + + // THEN + XCTAssertEqual(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL), nil) + } + + @MainActor + func test_WhenWebViewZoomInForASite_ThenNewZoomSavedForTheSite() async { + // GIVEN + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + UserDefaultsWrapper.clearAll() + let (randomZoomLevel, nextZoomLevel, _) = randomLevelAndAdjacent() + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .resizable], + backing: .buffered, defer: false) + window.contentView = NSView(frame: window.contentRect(forFrameRect: window.frame)) + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + let tab = Tab(url: url) + window.contentView?.addSubview(tab.webView) + tab.webView.frame = window.contentView!.bounds + window.makeKeyAndOrderFront(nil) + let tabView = TabViewModel(tab: tab) + + // WHEN + tabView.tab.webView.zoomIn() + + // THEN + if nextZoomLevel == AccessibilityPreferences.shared.defaultPageZoom { + XCTAssertNil(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL)) + } else { + XCTAssertEqual(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL), nextZoomLevel) + } + } + + @MainActor + func test_WhenWebViewZoomOutForASite_ThenNewZoomSavedForTheSite() async { + // GIVEN + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + UserDefaultsWrapper.clearAll() + let (randomZoomLevel, _, previousZoomLevel) = randomLevelAndAdjacent() + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .resizable], + backing: .buffered, defer: false) + window.contentView = NSView(frame: window.contentRect(forFrameRect: window.frame)) + AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) + let tab = Tab(url: url) + window.contentView?.addSubview(tab.webView) + tab.webView.frame = window.contentView!.bounds + window.makeKeyAndOrderFront(nil) + let tabView = TabViewModel(tab: tab) + + // WHEN + tabView.tab.webView.zoomOut() + + // THEN + if previousZoomLevel == AccessibilityPreferences.shared.defaultPageZoom { + XCTAssertNil(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL)) + } else { + XCTAssertEqual(AccessibilityPreferences.shared.zoomPerWebsite(url: hostURL), previousZoomLevel) + } + } + + private func randomLevelAndAdjacent() -> (randomLevel: DefaultZoomValue, nextLevel: DefaultZoomValue, previousLevel: DefaultZoomValue) { + let allCases = DefaultZoomValue.allCases + + let selectableRange = 1..<(allCases.count - 1) + let randomIndex = selectableRange.randomElement()! + let randomLevel = allCases[randomIndex] + + let nextLevel = allCases[randomIndex + 1] + let previousLevel = allCases[randomIndex - 1] + + return (randomLevel, nextLevel, previousLevel) + } } extension TabViewModel { diff --git a/UnitTests/Tab/ViewModel/ZoomPopoverViewModelTests.swift b/UnitTests/Tab/ViewModel/ZoomPopoverViewModelTests.swift new file mode 100644 index 0000000000..b9f4adaf74 --- /dev/null +++ b/UnitTests/Tab/ViewModel/ZoomPopoverViewModelTests.swift @@ -0,0 +1,99 @@ +// +// ZoomPopoverViewModelTests.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 +@testable import DuckDuckGo_Privacy_Browser + +final class ZoomPopoverViewModelTests: XCTestCase { + + var tabVM: TabViewModel! + var zoomPopover: ZoomPopoverViewModel! + var accessibilityPreferences: AccessibilityPreferences! + let url = URL(string: "https://app.asana.com/0/1")! + let hostURL = "https://app.asana.com/" + + @MainActor + override func setUp() { + UserDefaultsWrapper.clearAll() + let tab = Tab(url: url) + tabVM = TabViewModel(tab: tab) + zoomPopover = ZoomPopoverViewModel(tabViewModel: tabVM) + let window = NSWindow() + window.contentView = tabVM.tab.webView + } + + @MainActor + func test_WhenZoomInFromPopover_ThenWebViewIsZoomedIn() async { + var increasableDefaultValue = DefaultZoomValue.allCases + increasableDefaultValue.removeLast() + let randomZoomLevel = increasableDefaultValue.randomElement()! + tabVM.tab.webView.zoomLevel = randomZoomLevel + + zoomPopover.zoomIn() + + await MainActor.run { + XCTAssertEqual(randomZoomLevel.index + 1, tabVM.tab.webView.zoomLevel.index) + XCTAssertEqual(randomZoomLevel.index + 1, zoomPopover.zoomLevel.index) + } + } + + @MainActor + func test_WhenZoomOutFromPopover_ThenWebViewIsZoomedOut() async { + var decreasableDefaultValue = DefaultZoomValue.allCases + decreasableDefaultValue.removeFirst() + let randomZoomLevel = decreasableDefaultValue.randomElement()! + tabVM.tab.webView.zoomLevel = randomZoomLevel + + zoomPopover.zoomOut() + + await MainActor.run { + XCTAssertEqual(randomZoomLevel.index - 1, tabVM.tab.webView.zoomLevel.index) + XCTAssertEqual(randomZoomLevel.index - 1, zoomPopover.zoomLevel.index) + } + } + + @MainActor + func test_WhenResetZoomFromPopover_ThenWebViewIsReset() async { + let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! + tabVM.tab.webView.defaultZoomValue = .percent100 + tabVM.tab.webView.zoomLevel = randomZoomLevel + + zoomPopover.reset() + + XCTAssertEqual(.percent100, tabVM.tab.webView.zoomLevel) + await MainActor.run { + XCTAssertEqual(.percent100, zoomPopover.zoomLevel) + } + } + + @MainActor + func test_WhenZoomValueIsSetInTab_ThenPopoverZoomLevelUpdated() async { + let expectation = XCTestExpectation() + zoomPopover = ZoomPopoverViewModel(tabViewModel: tabVM) + let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! + + Task { + tabVM.zoomWasSet(to: randomZoomLevel) + expectation.fulfill() + } + + await fulfillment(of: [expectation], timeout: 5.0) + XCTAssertEqual(zoomPopover.zoomLevel, randomZoomLevel) + } + +} diff --git a/UnitTests/Tab/WebViewTests.swift b/UnitTests/Tab/WebViewTests.swift index aedaeccc0d..836029b704 100644 --- a/UnitTests/Tab/WebViewTests.swift +++ b/UnitTests/Tab/WebViewTests.swift @@ -26,14 +26,17 @@ final class WebViewTests: XCTestCase { let window = NSWindow() var webView: WebView! + var capturingZoomLevelDelegate: CapturingZoomLevelDelegate! override func setUp() { webView = .init(frame: .zero) window.contentView?.addSubview(webView) + capturingZoomLevelDelegate = CapturingZoomLevelDelegate() } override func tearDown() { webView = nil + capturingZoomLevelDelegate = nil } func testInitialZoomLevelAndMagnification() { @@ -162,4 +165,55 @@ final class WebViewTests: XCTestCase { XCTAssertEqual(tabVM.tab.webView.magnification, 1) } + + func test_WhenZoomingIn_ThenZoomDelegateZoomWasSetIsCalled() { + // GIVEN + var increasableDefaultValue = DefaultZoomValue.allCases + increasableDefaultValue.removeLast() + let randomZoomLevel = increasableDefaultValue.randomElement()! + webView.zoomLevel = randomZoomLevel + webView.zoomLevelDelegate = capturingZoomLevelDelegate + + // WHEN + webView.zoomIn() + + // THEN + XCTAssertEqual(capturingZoomLevelDelegate.setLevel, DefaultZoomValue.allCases[randomZoomLevel.index + 1]) + } + + func test_WhenZoomingOut_ThenZoomDelegateZoomWasSetIsCalled() { + // GIVEN + var decreasableDefaultValue = DefaultZoomValue.allCases + decreasableDefaultValue.removeFirst() + let randomZoomLevel = decreasableDefaultValue.randomElement()! + webView.zoomLevel = randomZoomLevel + webView.zoomLevelDelegate = capturingZoomLevelDelegate + + // WHEN + webView.zoomOut() + + // THEN + XCTAssertEqual(capturingZoomLevelDelegate.setLevel, DefaultZoomValue.allCases[randomZoomLevel.index - 1]) + } + + func testWhenResettingZoomThenZoomDelegateZoomWasSetIsCalled() { + // GIVEN + let randomZoomLevel = DefaultZoomValue.allCases.randomElement()! + webView.zoomLevel = randomZoomLevel + webView.zoomLevelDelegate = capturingZoomLevelDelegate + + // WHEN + webView.resetZoomLevel() + + // THEN + XCTAssertEqual(capturingZoomLevelDelegate.setLevel, .percent100) + } } + +class CapturingZoomLevelDelegate: WebViewZoomLevelDelegate { + var setLevel: DefaultZoomValue? + + func zoomWasSet(to level: DefaultZoomValue) { + setLevel = level + } + } diff --git a/UnitTests/TabBar/Model/TabCollectionTests.swift b/UnitTests/TabBar/Model/TabCollectionTests.swift index 983091b856..7d9b94a790 100644 --- a/UnitTests/TabBar/Model/TabCollectionTests.swift +++ b/UnitTests/TabBar/Model/TabCollectionTests.swift @@ -184,6 +184,11 @@ extension Tab { convenience override init() { self.init(content: .newtab) } + + @MainActor + convenience init(url: URL) { + self.init(content: .url(url, credential: nil, source: .userEntered(url.absoluteString, downloadRequested: false))) + } } class HistoryTabExtensionMock: TabExtension, HistoryExtensionProtocol { From 5cf0022d739a6685f6f4e41a330f235e026959ed Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 17 May 2024 16:29:53 +0100 Subject: [PATCH 127/134] Use subscription package for DBP access_token (#2765) Task/Issue URL: https://app.asana.com/0/72649045549333/1207193091966580/f **Description**: Set up subscription package for DBP access_token. This PR also gets rid of the old way of saving the token when the sign-in notification was triggered because we don't need it anymore. --- DuckDuckGo.xcodeproj/project.pbxproj | 14 ++ DuckDuckGo/DBP/DBPHomeViewController.swift | 23 ++- .../DBP/DataBrokerProtectionDebugMenu.swift | 3 +- .../DBP/DataBrokerProtectionManager.swift | 12 +- ...erProtectionSubscriptionEventHandler.swift | 20 +-- DuckDuckGo/Statistics/GeneralPixel.swift | 5 - ...taBrokerAuthenticationManagerBuilder.swift | 38 +++++ ...ataBrokerProtectionBackgroundManager.swift | 11 +- .../DuckDuckGoDBPBackgroundAgent.entitlements | 1 + ...kGoDBPBackgroundAgentAppStore.entitlements | 1 + .../Info-AppStore.plist | 18 +-- DuckDuckGoDBPBackgroundAgent/Info.plist | 18 +-- ...okerProtectionAuthenticationManaging.swift | 69 +++++++++ ...BrokerProtectionSubscriptionManaging.swift | 65 ++++++++ ...scriptionPurchaseEnvironmentManaging.swift | 37 +++++ ...ataBrokerRunCustomJSONViewController.swift | 13 +- .../DataBrokerRunCustomJSONViewModel.swift | 14 +- .../DebugUI/DebugScanOperation.swift | 4 +- .../Operations/OptOutOperation.swift | 4 +- .../Operations/ScanOperation.swift | 4 +- .../DataBrokerProtectionScheduler.swift | 8 +- .../Services/CaptchaService.swift | 10 +- .../Services/EmailService.swift | 10 +- .../Services/RedeemCodeServices.swift | 5 +- .../Utils/ServicesAuthHeaderBuilder.swift | 38 +++++ .../CaptchaServiceTests.swift | 25 ++-- ...ProtectionAuthenticationManagerTests.swift | 141 ++++++++++++++++++ .../EmailServiceTests.swift | 27 ++-- .../DataBrokerProtectionTests/Mocks.swift | 32 ++++ .../ServicesAuthHeaderBuilderTests.swift | 72 +++++++++ 30 files changed, 622 insertions(+), 120 deletions(-) create mode 100644 DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7d447f74b1..ddff21e5e7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -206,6 +206,12 @@ 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */; }; 31E163BD293A579E00963C10 /* PrivacyReferenceTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */; }; 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; + 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; + 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; + 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; + 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */; }; @@ -2907,6 +2913,7 @@ 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSiteReportingReferenceTests.swift; sourceTree = ""; }; 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = ""; }; 31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; + 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerAuthenticationManagerBuilder.swift; sourceTree = ""; }; 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerUserScript.swift; sourceTree = ""; }; 31F28C4E28C8EEC500119F70 /* YoutubeOverlayUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScript.swift; sourceTree = ""; }; 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckURLSchemeHandler.swift; sourceTree = ""; }; @@ -6322,6 +6329,7 @@ 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, + 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */, 9D9AE9192AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore.entitlements */, @@ -10276,6 +10284,7 @@ B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, + 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, @@ -10842,6 +10851,8 @@ 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */, + 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10856,6 +10867,8 @@ 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, 315A023D2B64216B00BFA577 /* IPCServiceManager.swift in Sources */, + 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -11156,6 +11169,7 @@ B60293E62BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, + 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index cf9e05e900..d6ececa0bb 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -105,8 +105,9 @@ final class DBPHomeViewController: NSViewController { override func viewDidAppear() { super.viewDidAppear() - if shouldAskForInviteCode() { - presentInviteCodeFlow() + if !dataBrokerProtectionManager.isUserAuthenticated() { + assertionFailure("This UI should never be presented if the user is not authenticated") + closeUI() } } @@ -118,9 +119,7 @@ final class DBPHomeViewController: NSViewController { } private func setupUI() { - if !shouldAskForInviteCode() { - setupUIWithCurrentStatus() - } + setupUIWithCurrentStatus() } private func setupObserver() { @@ -163,10 +162,6 @@ final class DBPHomeViewController: NSViewController { } } - private func shouldAskForInviteCode() -> Bool { - prerequisiteVerifier.checkStatus() == .valid && dataBrokerProtectionManager.shouldAskForInviteCode() - } - private func displayDBPUI() { replaceChildController(dataBrokerProtectionViewController) } @@ -185,6 +180,12 @@ final class DBPHomeViewController: NSViewController { NotificationCenter.default.removeObserver(observer) } } + + private func closeUI() { + presentedWindowController?.window?.close() + presentedWindowController = nil + NotificationCenter.default.post(name: .dbpDidClose, object: nil) + } } extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDelegate { @@ -195,9 +196,7 @@ extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDeleg } func dataBrokerProtectionInviteDialogsViewModelDidCancel(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { - presentedWindowController?.window?.close() - presentedWindowController = nil - NotificationCenter.default.post(name: .dbpDidClose, object: nil) + closeUI() } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index b22ee8eac2..a552bf4bc6 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -316,7 +316,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func runCustomJSON() { - let viewController = DataBrokerRunCustomJSONViewController() + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() + let viewController = DataBrokerRunCustomJSONViewController(authenticationManager: authenticationManager) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f3b8705674..9ecde48a3e 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -29,9 +29,7 @@ public final class DataBrokerProtectionManager { static let shared = DataBrokerProtectionManager() private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() - private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() - private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() private let dataBrokerProtectionWaitlistDataSource: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp) @@ -56,13 +54,11 @@ public final class DataBrokerProtectionManager { }() private init() { - self.redeemUseCase = RedeemUseCase(authenticationService: authenticationService, - authenticationRepository: authenticationRepository) - + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() } - public func shouldAskForInviteCode() -> Bool { - redeemUseCase.shouldAskForInviteCode() + public func isUserAuthenticated() -> Bool { + authenticationManager.isUserAuthenticated } // MARK: - Debugging Features diff --git a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift index 57dd6d5c33..50f8a36d4b 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift @@ -23,34 +23,16 @@ import DataBrokerProtection import PixelKit final class DataBrokerProtectionSubscriptionEventHandler { - - private let accountManager: AccountManaging - private let authRepository: AuthenticationRepository private let featureDisabler: DataBrokerProtectionFeatureDisabling - init(accountManager: AccountManaging = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), - authRepository: AuthenticationRepository = KeychainAuthenticationData(), - featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { - self.accountManager = accountManager - self.authRepository = authRepository + init(featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { self.featureDisabler = featureDisabler } func registerForSubscriptionAccountManagerEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) } - @objc private func handleAccountDidSignIn() { - guard let token = accountManager.accessToken else { - PixelKit.fire(GeneralPixel.dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn) - assertionFailure("[DBP Subscription] AccountManager signed in but token could not be retrieved") - return - } - - authRepository.save(accessToken: token) - } - @objc private func handleAccountDidSignOut() { featureDisabler.disableAndDelete() } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 90cb21ae2b..0380cedd98 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -140,9 +140,6 @@ enum GeneralPixel: PixelKitEventV2 { case dataBrokerResetLoginItemDaily case dataBrokerDisableAndDeleteDaily - // DataBrokerProtection Other - case dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn - // Default Browser case defaultRequestedFromHomepage case defaultRequestedFromHomepageSetupView @@ -507,8 +504,6 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_dbp_imp_terms" case .dataBrokerProtectionWaitlistTermsAndConditionsAccepted: return "m_mac_dbp_ev_terms_accepted" - case .dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn: - return "m_mac_dbp_error_when_fetching_subscription_auth_token_after_sign_in" case .dataBrokerProtectionRemoteMessageDisplayed(let messageID): return "m_mac_dbp_remote_message_displayed_\(messageID)" case .dataBrokerProtectionRemoteMessageDismissed(let messageID): diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift new file mode 100644 index 0000000000..594eb0b3d3 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift @@ -0,0 +1,38 @@ +// +// DataBrokerAuthenticationManagerBuilder.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import DataBrokerProtection +import Subscription + +final public class DataBrokerAuthenticationManagerBuilder { + static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase()) -> DataBrokerProtectionAuthenticationManager { + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let subscriptionManager = DataBrokerProtectionSubscriptionManager(accountManager: accountManager, + environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManager()) + return DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + } +} + +extension AccountManager: DataBrokerProtectionAccountManaging { + public func hasEntitlement(for cachePolicy: CachePolicy) async -> Result { + await hasEntitlement(for: .dataBrokerProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift index 2bdc06947a..459f52fce6 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift @@ -21,6 +21,7 @@ import Common import BrowserServicesKit import DataBrokerProtection import PixelKit +import Subscription public final class DataBrokerProtectionBackgroundManager { @@ -30,7 +31,7 @@ public final class DataBrokerProtectionBackgroundManager { private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) @@ -65,13 +66,15 @@ public final class DataBrokerProtectionBackgroundManager { dataManager: dataManager, notificationCenter: NotificationCenter.default, pixelHandler: pixelHandler, - redeemUseCase: redeemUseCase, + authenticationManager: authenticationManager, userNotificationService: userNotificationService) }() private init() { - self.redeemUseCase = RedeemUseCase(authenticationService: authenticationService, - authenticationRepository: authenticationRepository) + let redeemUseCase = RedeemUseCase(authenticationService: authenticationService, + authenticationRepository: authenticationRepository) + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase) + _ = ipcServiceManager } diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements index c1bf3bf0e0..fc132e719b 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements @@ -11,6 +11,7 @@ keychain-access-groups $(DBP_APP_GROUP) + $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements index b90c211a88..19c29d3d2f 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements @@ -15,6 +15,7 @@ keychain-access-groups $(DBP_APP_GROUP) + $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist b/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist index cbd58651f6..86eede1b79 100644 --- a/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist +++ b/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist @@ -2,14 +2,14 @@ - DBP_APP_GROUP - $(DBP_APP_GROUP) - LSApplicationCategoryType - public.app-category.productivity - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - + DBP_APP_GROUP + $(DBP_APP_GROUP) + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + + SUBSCRIPTION_APP_GROUP + $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoDBPBackgroundAgent/Info.plist b/DuckDuckGoDBPBackgroundAgent/Info.plist index cbd58651f6..86eede1b79 100644 --- a/DuckDuckGoDBPBackgroundAgent/Info.plist +++ b/DuckDuckGoDBPBackgroundAgent/Info.plist @@ -2,14 +2,14 @@ - DBP_APP_GROUP - $(DBP_APP_GROUP) - LSApplicationCategoryType - public.app-category.productivity - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - + DBP_APP_GROUP + $(DBP_APP_GROUP) + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + + SUBSCRIPTION_APP_GROUP + $(SUBSCRIPTION_APP_GROUP) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift new file mode 100644 index 0000000000..0aea2ac3ef --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift @@ -0,0 +1,69 @@ +// +// DataBrokerProtectionAuthenticationManaging.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public protocol DataBrokerProtectionAuthenticationManaging { + var isUserAuthenticated: Bool { get } + var accessToken: String? { get } + func hasValidEntitlement() async throws -> Bool + func shouldAskForInviteCode() -> Bool + func redeem(inviteCode: String) async throws + func getAuthHeader() -> String? +} + +public final class DataBrokerProtectionAuthenticationManager: DataBrokerProtectionAuthenticationManaging { + private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let subscriptionManager: DataBrokerProtectionSubscriptionManaging + + public var isUserAuthenticated: Bool { + subscriptionManager.isUserAuthenticated + } + + public var accessToken: String? { + subscriptionManager.accessToken + } + + public init(redeemUseCase: any DataBrokerProtectionRedeemUseCase, + subscriptionManager: any DataBrokerProtectionSubscriptionManaging) { + self.redeemUseCase = redeemUseCase + self.subscriptionManager = subscriptionManager + } + + public func hasValidEntitlement() async throws -> Bool { + try await subscriptionManager.hasValidEntitlement() + } + + public func getAuthHeader() -> String? { + ServicesAuthHeaderBuilder().getAuthHeader(accessToken) + } + + // MARK: - Redeem code flow + + // We might want the ability to ask for invite code later on, keeping this here to make things easier + // https://app.asana.com/0/1204167627774280/1207270521849479/f + + public func shouldAskForInviteCode() -> Bool { + // redeemUseCase.shouldAskForInviteCode() + return false + } + + public func redeem(inviteCode: String) async throws { + // await redeemUseCase.redeem(inviteCode: inviteCode) + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift new file mode 100644 index 0000000000..4b94e75351 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift @@ -0,0 +1,65 @@ +// +// DataBrokerProtectionSubscriptionManaging.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription +import Common + +public protocol DataBrokerProtectionSubscriptionManaging { + var isUserAuthenticated: Bool { get } + var accessToken: String? { get } + func hasValidEntitlement() async throws -> Bool +} + +public final class DataBrokerProtectionSubscriptionManager: DataBrokerProtectionSubscriptionManaging { + private let accountManager: DataBrokerProtectionAccountManaging + private let environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging + + public var isUserAuthenticated: Bool { + accountManager.accessToken != nil + } + + public var accessToken: String? { + accountManager.accessToken + } + + public init(accountManager: DataBrokerProtectionAccountManaging, + environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging) { + self.accountManager = accountManager + self.environmentManager = environmentManager + } + + public func hasValidEntitlement() async throws -> Bool { + environmentManager.updateEnvironment() + + switch await accountManager.hasEntitlement(for: .reloadIgnoringLocalCacheData) { + case let .success(result): + return result + case .failure(let error): + throw error + } + } +} + +// MARK: - Wrapper Protocols + +/// This protocol exists only as a wrapper on top of the AccountManager since it is a concrete type on BSK +public protocol DataBrokerProtectionAccountManaging { + var accessToken: String? { get } + func hasEntitlement(for cachePolicy: AccountManager.CachePolicy) async -> Result +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift new file mode 100644 index 0000000000..a18796bace --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift @@ -0,0 +1,37 @@ +// +// DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription + +/// This protocol exists only as a wrapper on top of the SubscriptionPurchaseEnvironment since it is a concrete type on BSK +public protocol DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { + func updateEnvironment() +} + +public final class DataBrokerProtectionSubscriptionPurchaseEnvironmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { + private let settings: DataBrokerProtectionSettings + + public init(settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings()) { + self.settings = settings + } + + public func updateEnvironment() { + SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift index 8ef73fc3f4..46deebb2c3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift @@ -20,8 +20,19 @@ import Foundation import SwiftUI public final class DataBrokerRunCustomJSONViewController: NSViewController { + private let authenticationManager: DataBrokerProtectionAuthenticationManaging + + public init(authenticationManager: DataBrokerProtectionAuthenticationManaging) { + self.authenticationManager = authenticationManager + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public override func loadView() { - let contentView = DataBrokerRunCustomJSONView(viewModel: DataBrokerRunCustomJSONViewModel()) + let contentView = DataBrokerRunCustomJSONView(viewModel: DataBrokerRunCustomJSONViewModel(authenticationManager: authenticationManager)) let hostingController = NSHostingController(rootView: contentView) hostingController.view.autoresizingMask = [.width, .height] self.view = hostingController.view diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 3cf0289b99..75c2f1ab78 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -150,8 +150,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } private let contentScopeProperties: ContentScopeProperties private let csvColumns = ["name_input", "age_input", "city_input", "state_input", "name_scraped", "age_scraped", "address_scraped", "relatives_scraped", "url", "broker name", "screenshot_id", "error", "matched_fields", "result_match", "expected_match"] + private let authenticationManager: DataBrokerProtectionAuthenticationManaging - init() { + init(authenticationManager: DataBrokerProtectionAuthenticationManaging) { let privacyConfigurationManager = PrivacyConfigurationManagingMock() let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, @@ -164,6 +165,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { thirdPartyCredentialsProvider: false) let sessionKey = UUID().uuidString + self.authenticationManager = authenticationManager let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, sessionKey: sessionKey, featureToggles: features) @@ -171,8 +173,8 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { self.runnerProvider = DataBrokerOperationRunnerProvider( privacyConfigManager: privacyConfigurationManager, contentScopeProperties: contentScopeProperties, - emailService: EmailService(), - captchaService: CaptchaService()) + emailService: EmailService(authenticationManager: authenticationManager), + captchaService: CaptchaService(authenticationManager: authenticationManager)) self.privacyConfigManager = privacyConfigurationManager self.contentScopeProperties = contentScopeProperties @@ -190,7 +192,11 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { try await withThrowingTaskGroup(of: DebugScanReturnValue.self) { group in for queryData in brokerProfileQueryData { - let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, prefs: self.contentScopeProperties, query: queryData) { + let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, + prefs: self.contentScopeProperties, + query: queryData, + emailService: EmailService(authenticationManager: self.authenticationManager), + captchaService: CaptchaService(authenticationManager: self.authenticationManager)) { true } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift index bc6ff398d2..364be47c18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift @@ -78,8 +78,8 @@ final class DebugScanOperation: DataBrokerOperation { init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, query: BrokerProfileQueryData, - emailService: EmailServiceProtocol = EmailService(), - captchaService: CaptchaServiceProtocol = CaptchaService(), + emailService: EmailServiceProtocol, + captchaService: CaptchaServiceProtocol, operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 0, shouldRunNextStep: @escaping () -> Bool diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift index 9f48f0faa1..98b29f6827 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift @@ -54,8 +54,8 @@ final class OptOutOperation: DataBrokerOperation { init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, query: BrokerProfileQueryData, - emailService: EmailServiceProtocol = EmailService(), - captchaService: CaptchaServiceProtocol = CaptchaService(), + emailService: EmailServiceProtocol, + captchaService: CaptchaServiceProtocol, cookieHandler: CookieHandler = BrokerCookieHandler(), operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 40, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index 964c7cdf69..7f7ba274a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -48,8 +48,8 @@ final class ScanOperation: DataBrokerOperation { init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, query: BrokerProfileQueryData, - emailService: EmailServiceProtocol = EmailService(), - captchaService: CaptchaServiceProtocol = CaptchaService(), + emailService: EmailServiceProtocol, + captchaService: CaptchaServiceProtocol, cookieHandler: CookieHandler = BrokerCookieHandler(), operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 0, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 509b13272c..b0f0d324b6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -131,6 +131,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch private let captchaService: CaptchaServiceProtocol private let userNotificationService: DataBrokerProtectionUserNotificationService private var currentOperation: DataBrokerProtectionCurrentOperation = .idle + private let authenticationManager: DataBrokerProtectionAuthenticationManaging /// Ensures that only one scheduler operation is executed at the same time. /// @@ -161,7 +162,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch dataManager: DataBrokerProtectionDataManager, notificationCenter: NotificationCenter = NotificationCenter.default, pixelHandler: EventMapping, - redeemUseCase: DataBrokerProtectionRedeemUseCase, + authenticationManager: DataBrokerProtectionAuthenticationManaging, userNotificationService: DataBrokerProtectionUserNotificationService ) { activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) @@ -176,9 +177,10 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch self.pixelHandler = pixelHandler self.notificationCenter = notificationCenter self.userNotificationService = userNotificationService + self.authenticationManager = authenticationManager - self.emailService = EmailService(redeemUseCase: redeemUseCase) - self.captchaService = CaptchaService(redeemUseCase: redeemUseCase) + self.emailService = EmailService(authenticationManager: authenticationManager) + self.captchaService = CaptchaService(authenticationManager: authenticationManager) } public func startScheduler(showWebView: Bool = false) { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift index c249359ef8..b29112dab0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift @@ -123,16 +123,16 @@ struct CaptchaService: CaptchaServiceProtocol { } private let urlSession: URLSession - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let settings: DataBrokerProtectionSettings private let servicePixel: DataBrokerProtectionBackendServicePixels init(urlSession: URLSession = URLSession.shared, - redeemUseCase: DataBrokerProtectionRedeemUseCase = RedeemUseCase(), + authenticationManager: DataBrokerProtectionAuthenticationManaging, settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings(), servicePixel: DataBrokerProtectionBackendServicePixels = DefaultDataBrokerProtectionBackendServicePixels()) { self.urlSession = urlSession - self.redeemUseCase = redeemUseCase + self.authenticationManager = authenticationManager self.settings = settings self.servicePixel = servicePixel } @@ -186,7 +186,7 @@ struct CaptchaService: CaptchaServiceProtocol { os_log("Submitting captcha request ...", log: .service) var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .submitCaptchaInformationRequest) throw AuthenticationError.noAuthToken } @@ -272,7 +272,7 @@ struct CaptchaService: CaptchaServiceProtocol { } var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .submitCaptchaToBeResolvedRequest) throw AuthenticationError.noAuthToken } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 8cd57ea70c..48cd0364e7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -51,16 +51,16 @@ struct EmailService: EmailServiceProtocol { } public let urlSession: URLSession - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let settings: DataBrokerProtectionSettings private let servicePixel: DataBrokerProtectionBackendServicePixels init(urlSession: URLSession = URLSession.shared, - redeemUseCase: DataBrokerProtectionRedeemUseCase = RedeemUseCase(), + authenticationManager: DataBrokerProtectionAuthenticationManaging, settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings(), servicePixel: DataBrokerProtectionBackendServicePixels = DefaultDataBrokerProtectionBackendServicePixels()) { self.urlSession = urlSession - self.redeemUseCase = redeemUseCase + self.authenticationManager = authenticationManager self.settings = settings self.servicePixel = servicePixel } @@ -78,7 +78,7 @@ struct EmailService: EmailServiceProtocol { } var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .getEmail) throw AuthenticationError.noAuthToken } @@ -161,7 +161,7 @@ struct EmailService: EmailServiceProtocol { var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .extractEmailLink) throw AuthenticationError.noAuthToken } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift index 60a71ad658..7c446cac03 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift @@ -77,10 +77,7 @@ public final class RedeemUseCase: DataBrokerProtectionRedeemUseCase { } public func getAuthHeader() -> String? { - guard let token = authenticationRepository.getAccessToken() else { - return nil - } - return "bearer \(token)" + ServicesAuthHeaderBuilder().getAuthHeader(authenticationRepository.getAccessToken()) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift new file mode 100644 index 0000000000..59d6316877 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift @@ -0,0 +1,38 @@ +// +// ServicesAuthHeaderBuilder.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 ServicesAuthHeaderBuilder { + + /** + * Receives an auth token and returns the header value as expected by our services + * + * - Parameters: + * - token: The authentication token to be included in the header + * + * - Returns: The formatted header value with the token included, or nil if the token is nil or empty + */ + public func getAuthHeader(_ token: String?) -> String? { + guard let token = token, !token.isEmpty else { + return nil + } + return "bearer \(token)" + } + +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift index 9eca0082dd..16596cd6f1 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift @@ -23,6 +23,7 @@ import Foundation final class CaptchaServiceTests: XCTestCase { private let servicePixel = MockDataBrokerProtectionBackendServicePixels() let jsonEncoder = JSONEncoder() + private let mockAuthenticationManager = MockAuthenticationManager() enum MockError: Error { case someError @@ -37,12 +38,13 @@ final class CaptchaServiceTests: XCTestCase { override func tearDown() async throws { MockURLProtocol.requestHandlerQueue.removeAll() servicePixel.reset() + mockAuthenticationManager.reset() } func testWhenSessionThrowsOnSubmittingCaptchaInfo_thenTheCorrectErrorIsThrown() async { MockURLProtocol.requestHandlerQueue.append({ _ in throw MockError.someError }) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -62,7 +64,7 @@ final class CaptchaServiceTests: XCTestCase { let response = CaptchaTransaction(message: .failureCritical, transactionId: nil) MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, try? self.jsonEncoder.encode(response)) }) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -82,7 +84,7 @@ final class CaptchaServiceTests: XCTestCase { let response = CaptchaTransaction(message: .invalidRequest, transactionId: nil) MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, try? self.jsonEncoder.encode(response)) }) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -106,7 +108,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -128,7 +130,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -150,7 +152,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -172,7 +174,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -196,7 +198,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -218,7 +220,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -231,11 +233,10 @@ final class CaptchaServiceTests: XCTestCase { } func testWhenNoAuthTokenAvailable_noAuthTokenErrorIsThrown() async { - let redeemUseCase = MockRedeemUseCase() - redeemUseCase.shouldSendNilAuthHeader = true + mockAuthenticationManager.authHeaderValue = nil let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: redeemUseCase, + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift new file mode 100644 index 0000000000..b0603c8399 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift @@ -0,0 +1,141 @@ +// +// DataBrokerProtectionAuthenticationManagerTests.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 DataBrokerProtection + +class DataBrokerProtectionAuthenticationManagerTests: XCTestCase { + var authenticationManager: DataBrokerProtectionAuthenticationManager! + var redeemUseCase: DataBrokerProtectionRedeemUseCase! + var subscriptionManager: MockDataBrokerProtectionSubscriptionManaging! + + override func setUp() async throws { + redeemUseCase = MockRedeemUseCase() + subscriptionManager = MockDataBrokerProtectionSubscriptionManaging() + } + + override func tearDown() async throws { + authenticationManager = nil + redeemUseCase = nil + subscriptionManager = nil + } + + func testUserNotAuthenticatedWhenSubscriptionManagerReturnsFalse() { + subscriptionManager.userAuthenticatedValue = false + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertEqual(authenticationManager.isUserAuthenticated, false) + } + + func testEmptyAccessTokenResultsInNilAuthHeader() { + subscriptionManager.accessTokenValue = nil + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertNil(authenticationManager.getAuthHeader()) + } + + func testUserAuthenticatedWhenSubscriptionManagerReturnsTrue() { + subscriptionManager.userAuthenticatedValue = true + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertEqual(authenticationManager.isUserAuthenticated, true) + } + + func testNonEmptyAccessTokenResultsInValidAuthHeader() { + let accessToken = "validAccessToken" + subscriptionManager.accessTokenValue = accessToken + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertNotNil(authenticationManager.getAuthHeader()) + } + + func testValidEntitlementCheckWithSuccess() async { + subscriptionManager.entitlementResultValue = true + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + do { + let result = try await authenticationManager.hasValidEntitlement() + XCTAssertTrue(result, "Entitlement check should return true for valid entitlement") + } catch { + XCTFail("Entitlement check should not fail: \(error)") + } + } + + func testValidEntitlementCheckWithSuccessFalse() async { + subscriptionManager.entitlementResultValue = false + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + do { + let result = try await authenticationManager.hasValidEntitlement() + XCTAssertFalse(result, "Entitlement check should return false for valid entitlement") + } catch { + XCTFail("Entitlement check should not fail: \(error)") + } + } + + func testValidEntitlementCheckWithFailure() async { + let mockError = NSError(domain: "TestErrorDomain", code: 123, userInfo: nil) + subscriptionManager.entitlementError = mockError + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + do { + _ = try await authenticationManager.hasValidEntitlement() + XCTFail("Entitlement check should fail") + } catch let error as NSError { + XCTAssertEqual(mockError.domain, error.domain) + XCTAssertEqual(mockError.code, error.code) + } + } +} + +final class MockDataBrokerProtectionSubscriptionManaging: DataBrokerProtectionSubscriptionManaging { + typealias EntitlementResult = Result + + var userAuthenticatedValue = false + var accessTokenValue: String? + var entitlementResultValue = false + var entitlementError: Error? + + var isUserAuthenticated: Bool { + userAuthenticatedValue + } + + var accessToken: String? { + accessTokenValue + } + + func hasValidEntitlement() async throws -> Bool { + if let error = entitlementError { + throw error + } + return entitlementResultValue + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift index 66bf08995e..dc853279ae 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift @@ -22,6 +22,7 @@ import Foundation final class EmailServiceTests: XCTestCase { private let servicePixel = MockDataBrokerProtectionBackendServicePixels() + private let mockAuthenticationManager = MockAuthenticationManager() enum MockError: Error { case someError @@ -36,12 +37,13 @@ final class EmailServiceTests: XCTestCase { override func tearDown() async throws { MockURLProtocol.requestHandlerQueue.removeAll() servicePixel.reset() + mockAuthenticationManager.reset() } func testWhenSessionThrows_thenTheCorrectErrorIsThrown() async { MockURLProtocol.requestHandlerQueue.append({ _ in throw MockError.someError }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -65,7 +67,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -87,7 +89,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -106,7 +108,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(unknownResponse) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -136,7 +138,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(notReadyResponse) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -169,7 +171,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(successResponse) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -193,7 +195,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -220,7 +222,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -247,7 +249,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -266,10 +268,9 @@ final class EmailServiceTests: XCTestCase { } func testWhenNoAuthTokenAvailable_noAuthTokenErrorIsThrown() async { - let redeemUseCase = MockRedeemUseCase() - redeemUseCase.shouldSendNilAuthHeader = true + mockAuthenticationManager.authHeaderValue = nil let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: redeemUseCase, + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -290,7 +291,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.noAuth, nil) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 9b60fe4812..99f01aa8ff 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -960,3 +960,35 @@ final class MockDataBrokerProtectionBackendServicePixels: DataBrokerProtectionBa statusCode = nil } } + +final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManaging { + var isUserAuthenticatedValue = false + var accessTokenValue: String? = "fake token" + var shouldAskForInviteCodeValue = false + var redeemCodeCalled = false + var authHeaderValue: String? = "fake auth header" + + var isUserAuthenticated: Bool { isUserAuthenticatedValue } + + var accessToken: String? { accessTokenValue } + + func hasValidEntitlement() async throws -> Bool { + return true + } + + func shouldAskForInviteCode() -> Bool { shouldAskForInviteCodeValue } + + func redeem(inviteCode: String) async throws { + redeemCodeCalled = true + } + + func getAuthHeader() -> String? { authHeaderValue } + + func reset() { + isUserAuthenticatedValue = false + accessTokenValue = "fake token" + shouldAskForInviteCodeValue = false + redeemCodeCalled = false + authHeaderValue = "fake auth header" + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift new file mode 100644 index 0000000000..ac3844e0dd --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift @@ -0,0 +1,72 @@ +// +// ServicesAuthHeaderBuilderTests.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 DataBrokerProtection + +final class ServicesAuthHeaderBuilderTests: XCTestCase { + + func testValidToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("validToken123") + XCTAssertEqual(result, "bearer validToken123") + } + + func testNilToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader(nil) + XCTAssertNil(result) + } + + func testEmptyToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("") + XCTAssertNil(result) + } + + func testTokenWithSpaces() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader(" tokenWithSpaces ") + XCTAssertEqual(result, "bearer tokenWithSpaces ") + } + + func testTokenWithSpecialCharacters() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("token@123!#") + XCTAssertEqual(result, "bearer token@123!#") + } + + func testLongToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("averylongtokenthatneedstobeincludedintheheader") + XCTAssertEqual(result, "bearer averylongtokenthatneedstobeincludedintheheader") + } + + func testTokenWithLeadingBearer() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("bearer token123") + XCTAssertEqual(result, "bearer bearer token123") + } + + func testTokenWithLeadingBearerUppercase() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("Bearer token456") + XCTAssertEqual(result, "bearer Bearer token456") + } + +} From c34580ad961a1d6eda14a16aa7716f42a132bdb2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Mon, 20 May 2024 11:26:04 +0100 Subject: [PATCH 128/134] Fix debug UI (#2795) Task/Issue URL: https://app.asana.com/0/1201011656765697/1207356723564109/f **Description**: Fix debug UI using the wrong init method --- .../DebugUI/DataBrokerRunCustomJSONViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift index 46deebb2c3..63948be0bc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift @@ -24,7 +24,7 @@ public final class DataBrokerRunCustomJSONViewController: NSViewController { public init(authenticationManager: DataBrokerProtectionAuthenticationManaging) { self.authenticationManager = authenticationManager - super.init() + super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { From 40b9e6cd00d3bd34a2de01a933b47e061bf16b0a Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Mon, 20 May 2024 09:06:55 -0500 Subject: [PATCH 129/134] Sort broker names in debug mode (#2794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/72649045549333/1207353159129206/f Tech Design URL: CC: **Description** ![Screenshot 2024-05-20 at 6 16 47 AM](https://github.com/duckduckgo/macos-browser/assets/18444/90fbeeb2-eef8-4a98-8f3b-1d9a4dd2075c) : **Steps to test this PR**: 1. --- ###### 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) --- .../DebugUI/DataBrokerRunCustomJSONView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift index 22b8c6b90a..78fb1ba4ad 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift @@ -75,7 +75,7 @@ struct DataBrokerRunCustomJSONView: View { Divider() - List(viewModel.brokers, id: \.name) { broker in + List(viewModel.brokers.sorted(by: { $0.name.lowercased() < $1.name.lowercased() }), id: \.name) { broker in Text(broker.name) .onTapGesture { jsonText = broker.toJSONString() From 72a4af1fe554187d994fa4d95cf6c0ffbe982ec4 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 20 May 2024 18:42:14 +0000 Subject: [PATCH 130/134] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- .../AppTrackerDataSetProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 95 +++- DuckDuckGo/ContentBlocker/trackerData.json | 470 +++++++++++++++++- 4 files changed, 563 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index f256a7425d..6309508918 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"e1744d6617db82fc621d71131270b348\"" - public static let embeddedDataSHA = "60bbae2f81deb1d44f6d192d063f8798ac09d927db0decfd7d994568b614392d" + public static let embeddedDataETag = "\"f2379428bfb9f97dffb6f9e700c27840\"" + public static let embeddedDataSHA = "0fb9abce9db169dec195c611c4520a8d643f620c3dc1bf075c73052591f7acf4" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift index a182f36fd5..85ba594612 100644 --- a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"ef8ebcc98d8abccca793c7e04422b160\"" - public static let embeddedDataSHA = "e2e8e5e191df54227222fbb0545a7eb8634b1156a69182323981bb6aed2c639d" + public static let embeddedDataETag = "\"004872ea25514c61490f047cd5f088b8\"" + public static let embeddedDataSHA = "4a06a3df999fad7829baecc9ccfcbc54c20526ba304f6c5f2846899d29b281cc" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 0491e2e042..82bac9ddc8 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1715350028654, + "version": 1716212212537, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -300,6 +300,9 @@ { "domain": "hertz.com" }, + { + "domain": "www.thrifty.com" + }, { "domain": "marvel.com" }, @@ -315,7 +318,7 @@ ] }, "state": "enabled", - "hash": "cf3197bc061274e9c23a870dea493b9a" + "hash": "6ea22969c48f1cb624808cd49a296f4d" }, "autofill": { "exceptions": [ @@ -2636,6 +2639,15 @@ } ] }, + { + "domain": "genius.com", + "rules": [ + { + "selector": "[class^='DfpAd']", + "type": "hide" + } + ] + }, { "domain": "getpocket.com", "rules": [ @@ -3026,6 +3038,27 @@ } ] }, + { + "domain": "livestrong.com", + "rules": [ + { + "selector": ".component-ar-horizontal-bar-ad", + "type": "hide-empty" + }, + { + "selector": ".component-article-section-votd-container-outer", + "type": "hide-empty" + }, + { + "selector": ".inline-ad", + "type": "hide" + }, + { + "selector": ".component-header-sticky-ad", + "type": "hide" + } + ] + }, { "domain": "macrumors.com", "rules": [ @@ -3289,6 +3322,19 @@ } ] }, + { + "domain": "obsev.com", + "rules": [ + { + "selector": ".my-10", + "type": "hide-empty" + }, + { + "selector": "[class^='Ad_ad']", + "type": "hide" + } + ] + }, { "domain": "oceanofcompressed.xyz", "rules": [ @@ -3473,6 +3519,15 @@ } ] }, + { + "domain": "quora.com", + "rules": [ + { + "selector": "#onetap_google_intermediate_iframe", + "type": "hide" + } + ] + }, { "domain": "qz.com", "rules": [ @@ -4236,6 +4291,15 @@ } ] }, + { + "domain": "xhamster.com", + "rules": [ + { + "selector": "#credential_picker_container", + "type": "override" + } + ] + }, { "domain": "first-party.site", "rules": [ @@ -4290,7 +4354,7 @@ ] }, "state": "enabled", - "hash": "893bd7422971b3a7b4c6e02cdfc6332d" + "hash": "9a895fea083dabb6e3496242636a8370" }, "exceptionHandler": { "exceptions": [ @@ -6839,6 +6903,16 @@ } ] }, + "igodigital.com": { + "rules": [ + { + "rule": "collect.igodigital.com/collect.js", + "domains": [ + "goodwillfinds.com" + ] + } + ] + }, "iheart.com": { "rules": [ { @@ -7936,7 +8010,8 @@ { "rule": "sundaysky.com", "domains": [ - "bankofamerica.com" + "bankofamerica.com", + "idnotify.com" ] } ] @@ -8333,6 +8408,16 @@ } ] }, + "rbcroyalbank.com": { + "rules": [ + { + "rule": "collect.rbcroyalbank.com/collect.js", + "domains": [ + "goodwillfinds.com" + ] + } + ] + }, "canadapost-postescanada.ca": { "rules": [ { @@ -8373,7 +8458,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "2dd3c945cc9811719be893edcd79682e" + "hash": "d9db93f32201ee8d2e545eeead7cc66f" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/ContentBlocker/trackerData.json b/DuckDuckGo/ContentBlocker/trackerData.json index f7618c909f..2c632f15c2 100644 --- a/DuckDuckGo/ContentBlocker/trackerData.json +++ b/DuckDuckGo/ContentBlocker/trackerData.json @@ -1,6 +1,6 @@ { "_builtWith": { - "tracker-radar": "74dd9601901673a7c0f87e609695b5a0e31b808adabd62e6db6ed7c99bde966d-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-radar": "a8f276714a31d43fb185a1b233ed92f12680627ca54c6f98da44de9fe27f097e-4013b4e91930c643394cb31c6c745356f133b04f", "tracker-surrogates": "0528e3226df15b1a3e319ad68ef76612a8f26623" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", @@ -32654,6 +32654,17 @@ "cookies": 0.01, "default": "block" }, + "agilebreeze.com": { + "domain": "agilebreeze.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "agreeablearch.com": { "domain": "agreeablearch.com", "owner": { @@ -32940,6 +32951,17 @@ "cookies": 0.01, "default": "block" }, + "badgerabbit.com": { + "domain": "badgerabbit.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "baitbaseball.com": { "domain": "baitbaseball.com", "owner": { @@ -33083,6 +33105,17 @@ "cookies": 0.01, "default": "block" }, + "blissfullagoon.com": { + "domain": "blissfullagoon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "blushingbeast.com": { "domain": "blushingbeast.com", "owner": { @@ -33578,6 +33611,17 @@ "cookies": 0.01, "default": "block" }, + "cheerycraze.com": { + "domain": "cheerycraze.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "chesscolor.com": { "domain": "chesscolor.com", "owner": { @@ -33864,6 +33908,17 @@ "cookies": 0.01, "default": "block" }, + "cooingcoal.com": { + "domain": "cooingcoal.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "coordinatedcoat.com": { "domain": "coordinatedcoat.com", "owner": { @@ -34095,6 +34150,28 @@ "cookies": 0.01, "default": "block" }, + "dandydune.com": { + "domain": "dandydune.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "dandyglow.com": { + "domain": "dandyglow.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "daughterstone.com": { "domain": "daughterstone.com", "owner": { @@ -34359,6 +34436,17 @@ "cookies": 0.01, "default": "block" }, + "eagereden.com": { + "domain": "eagereden.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "eagerknight.com": { "domain": "eagerknight.com", "owner": { @@ -34667,6 +34755,17 @@ "cookies": 0.01, "default": "block" }, + "facilitatebreakfast.com": { + "domain": "facilitatebreakfast.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "fadedsnow.com": { "domain": "fadedsnow.com", "owner": { @@ -34689,6 +34788,17 @@ "cookies": 0.01, "default": "block" }, + "fairytaleflame.com": { + "domain": "fairytaleflame.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "fancyactivity.com": { "domain": "fancyactivity.com", "owner": { @@ -34700,6 +34810,17 @@ "cookies": 0.01, "default": "block" }, + "fancydune.com": { + "domain": "fancydune.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "farshake.com": { "domain": "farshake.com", "owner": { @@ -35470,6 +35591,28 @@ "cookies": 0.01, "default": "block" }, + "helpcollar.com": { + "domain": "helpcollar.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "highfalutinhoney.com": { + "domain": "highfalutinhoney.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "historicalbeam.com": { "domain": "historicalbeam.com", "owner": { @@ -35547,6 +35690,17 @@ "cookies": 0.01, "default": "block" }, + "idyllicjazz.com": { + "domain": "idyllicjazz.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "illinvention.com": { "domain": "illinvention.com", "owner": { @@ -35646,6 +35800,17 @@ "cookies": 0.01, "default": "block" }, + "intelligentscissors.com": { + "domain": "intelligentscissors.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "internalcondition.com": { "domain": "internalcondition.com", "owner": { @@ -35668,6 +35833,39 @@ "cookies": 0.01, "default": "block" }, + "irritatingfog.com": { + "domain": "irritatingfog.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "jollylens.com": { + "domain": "jollylens.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "joyfulkeen.com": { + "domain": "joyfulkeen.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "jubilantaura.com": { "domain": "jubilantaura.com", "owner": { @@ -35998,6 +36196,17 @@ "cookies": 0.01, "default": "block" }, + "magicaljoin.com": { + "domain": "magicaljoin.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "majesticwaterscape.com": { "domain": "majesticwaterscape.com", "owner": { @@ -36009,6 +36218,17 @@ "cookies": 0.01, "default": "block" }, + "majesticwilderness.com": { + "domain": "majesticwilderness.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "maliciousmusic.com": { "domain": "maliciousmusic.com", "owner": { @@ -36031,6 +36251,17 @@ "cookies": 0.01, "default": "block" }, + "marriedvalue.com": { + "domain": "marriedvalue.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "materialisticmoon.com": { "domain": "materialisticmoon.com", "owner": { @@ -36130,6 +36361,17 @@ "cookies": 0.01, "default": "block" }, + "meremark.com": { + "domain": "meremark.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "merequartz.com": { "domain": "merequartz.com", "owner": { @@ -36141,6 +36383,28 @@ "cookies": 0.01, "default": "block" }, + "merryopal.com": { + "domain": "merryopal.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "merryvault.com": { + "domain": "merryvault.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "mightyspiders.com": { "domain": "mightyspiders.com", "owner": { @@ -36163,6 +36427,17 @@ "cookies": 0.01, "default": "block" }, + "minuteburst.com": { + "domain": "minuteburst.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "mixedreading.com": { "domain": "mixedreading.com", "owner": { @@ -36493,6 +36768,17 @@ "cookies": 0.01, "default": "block" }, + "opalquill.com": { + "domain": "opalquill.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "operationchicken.com": { "domain": "operationchicken.com", "owner": { @@ -36658,6 +36944,17 @@ "cookies": 0.01, "default": "block" }, + "piquantmeadow.com": { + "domain": "piquantmeadow.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "piquantvortex.com": { "domain": "piquantvortex.com", "owner": { @@ -36768,6 +37065,17 @@ "cookies": 0.01, "default": "block" }, + "polishedcrescent.com": { + "domain": "polishedcrescent.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "polishedfolly.com": { "domain": "polishedfolly.com", "owner": { @@ -36834,6 +37142,17 @@ "cookies": 0.01, "default": "block" }, + "preciousplanes.com": { + "domain": "preciousplanes.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "pricklypollution.com": { "domain": "pricklypollution.com", "owner": { @@ -36944,6 +37263,17 @@ "cookies": 0.01, "default": "block" }, + "quaintlake.com": { + "domain": "quaintlake.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "quillkick.com": { "domain": "quillkick.com", "owner": { @@ -36955,6 +37285,17 @@ "cookies": 0.01, "default": "block" }, + "quirkybliss.com": { + "domain": "quirkybliss.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "quirkysugar.com": { "domain": "quirkysugar.com", "owner": { @@ -37285,6 +37626,28 @@ "cookies": 0.01, "default": "block" }, + "rigidveil.com": { + "domain": "rigidveil.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "ringplant.com": { + "domain": "ringplant.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "rollconnection.com": { "domain": "rollconnection.com", "owner": { @@ -37373,6 +37736,17 @@ "cookies": 0.01, "default": "block" }, + "scarcestructure.com": { + "domain": "scarcestructure.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "scaredcomfort.com": { "domain": "scaredcomfort.com", "owner": { @@ -37593,6 +37967,17 @@ "cookies": 0.01, "default": "block" }, + "serenesurf.com": { + "domain": "serenesurf.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "serioussuit.com": { "domain": "serioussuit.com", "owner": { @@ -39166,6 +39551,17 @@ "cookies": 0.01, "default": "block" }, + "vibrantvale.com": { + "domain": "vibrantvale.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, "virtualvincent.com": { "domain": "virtualvincent.com", "owner": { @@ -49465,6 +49861,7 @@ "adorableanger.com", "adorableattention.com", "adventurousamount.com", + "agilebreeze.com", "agreeablearch.com", "agreeabletouch.com", "aheadday.com", @@ -49501,6 +49898,7 @@ "awzbijw.com", "axiomaticanger.com", "badgeboat.com", + "badgerabbit.com", "badgevolcano.com", "bagbeam.com", "baitbaseball.com", @@ -49522,6 +49920,7 @@ "blackbrake.com", "bleachbubble.com", "blesspizzas.com", + "blissfullagoon.com", "blushingbeast.com", "blushingbread.com", "boilingcredit.com", @@ -49577,6 +49976,7 @@ "changeablecats.com", "chargecracker.com", "charmingplate.com", + "cheerycraze.com", "cherriescare.com", "chesscolor.com", "chickensstation.com", @@ -49611,6 +50011,7 @@ "confusedcart.com", "consciouscheese.com", "consciousdirt.com", + "cooingcoal.com", "coordinatedcoat.com", "copycarpenter.com", "cosmicsculptor.com", @@ -49641,6 +50042,8 @@ "damageddistance.com", "damdoor.com", "dampdock.com", + "dandydune.com", + "dandyglow.com", "dapperfloor.com", "daughterstone.com", "dazzlingbook.com", @@ -49674,6 +50077,7 @@ "drollwharf.com", "dustydime.com", "dustyhammer.com", + "eagereden.com", "eagerknight.com", "echoinghaven.com", "effervescentcoral.com", @@ -49706,11 +50110,14 @@ "expansioneggnog.com", "exquisiteartisanship.com", "exuberantedge.com", + "facilitatebreakfast.com", "fadedsnow.com", "fadewaves.com", "fairiesbranch.com", + "fairytaleflame.com", "fallaciousfifth.com", "fancyactivity.com", + "fancydune.com", "farmergoldfish.com", "farshake.com", "farsnails.com", @@ -49805,6 +50212,8 @@ "heartbreakingmind.com", "hearthorn.com", "heavyplayground.com", + "helpcollar.com", + "highfalutinhoney.com", "historicalbeam.com", "hocgeese.com", "hollowafterthought.com", @@ -49815,6 +50224,7 @@ "hospitablehat.com", "humdrumtouch.com", "hystericalcloth.com", + "idyllicjazz.com", "illinvention.com", "importantmeat.com", "impossibleexpansion.com", @@ -49824,9 +50234,13 @@ "inconclusiveaction.com", "inputicicle.com", "inquisitiveice.com", + "intelligentscissors.com", "internalcondition.com", "internalsink.com", + "irritatingfog.com", "j93557g.com", + "jollylens.com", + "joyfulkeen.com", "jubilantaura.com", "jubilantcanyon.com", "jubilantcascade.com", @@ -49864,10 +50278,13 @@ "lunchroomlock.com", "lustroushaven.com", "maddeningpowder.com", + "magicaljoin.com", "majesticwaterscape.com", + "majesticwilderness.com", "maliciousmusic.com", "marketspiders.com", "marriedbelief.com", + "marriedvalue.com", "materialisticmoon.com", "materialparcel.com", "materialplayground.com", @@ -49882,9 +50299,13 @@ "memorizematch.com", "memorizeneck.com", "mentorsticks.com", + "meremark.com", "merequartz.com", + "merryopal.com", + "merryvault.com", "mightyspiders.com", "minorcattle.com", + "minuteburst.com", "mixedreading.com", "modularmental.com", "monacobeatles.com", @@ -49920,6 +50341,7 @@ "oafishchance.com", "obscenesidewalk.com", "oldfashionedoffer.com", + "opalquill.com", "operationchicken.com", "optimallimit.com", "opulentsylvan.com", @@ -49941,6 +50363,7 @@ "peacefullimit.com", "petiteumbrella.com", "piquantgrove.com", + "piquantmeadow.com", "piquantvortex.com", "placidactivity.com", "placidperson.com", @@ -49956,6 +50379,7 @@ "pointdigestion.com", "pointlesspocket.com", "pointlessprofit.com", + "polishedcrescent.com", "polishedfolly.com", "politeplanes.com", "politicalporter.com", @@ -49965,6 +50389,7 @@ "potatoinvention.com", "powderjourney.com", "powerfulcopper.com", + "preciousplanes.com", "predictplate.com", "prepareplanes.com", "priceypies.com", @@ -49981,8 +50406,10 @@ "pumpedpancake.com", "punyplant.com", "purposepipe.com", + "quaintlake.com", "quietknowledge.com", "quillkick.com", + "quirkybliss.com", "quirkysugar.com", "quizzicalpartner.com", "quizzicalzephyr.com", @@ -50030,6 +50457,8 @@ "righteouscrayon.com", "rightfulfall.com", "rigidrobin.com", + "rigidveil.com", + "ringplant.com", "rollconnection.com", "roofrelation.com", "roseincome.com", @@ -50043,6 +50472,7 @@ "satisfycork.com", "savoryorange.com", "scarceshock.com", + "scarcestructure.com", "scaredcomfort.com", "scaredslip.com", "scaredsnake.com", @@ -50073,6 +50503,7 @@ "seraphicjubilee.com", "serenecascade.com", "serenepebble.com", + "serenesurf.com", "serioussuit.com", "serpentshampoo.com", "settleshoes.com", @@ -50267,6 +50698,7 @@ "vibranthaven.com", "vibrantpact.com", "vibranttalisman.com", + "vibrantvale.com", "virtualvincent.com", "vividcanopy.com", "vividfrost.com", @@ -51037,6 +51469,7 @@ "adorableanger.com": "Leven Labs, Inc. DBA Admiral", "adorableattention.com": "Leven Labs, Inc. DBA Admiral", "adventurousamount.com": "Leven Labs, Inc. DBA Admiral", + "agilebreeze.com": "Leven Labs, Inc. DBA Admiral", "agreeablearch.com": "Leven Labs, Inc. DBA Admiral", "agreeabletouch.com": "Leven Labs, Inc. DBA Admiral", "aheadday.com": "Leven Labs, Inc. DBA Admiral", @@ -51073,6 +51506,7 @@ "awzbijw.com": "Leven Labs, Inc. DBA Admiral", "axiomaticanger.com": "Leven Labs, Inc. DBA Admiral", "badgeboat.com": "Leven Labs, Inc. DBA Admiral", + "badgerabbit.com": "Leven Labs, Inc. DBA Admiral", "badgevolcano.com": "Leven Labs, Inc. DBA Admiral", "bagbeam.com": "Leven Labs, Inc. DBA Admiral", "baitbaseball.com": "Leven Labs, Inc. DBA Admiral", @@ -51094,6 +51528,7 @@ "blackbrake.com": "Leven Labs, Inc. DBA Admiral", "bleachbubble.com": "Leven Labs, Inc. DBA Admiral", "blesspizzas.com": "Leven Labs, Inc. DBA Admiral", + "blissfullagoon.com": "Leven Labs, Inc. DBA Admiral", "blushingbeast.com": "Leven Labs, Inc. DBA Admiral", "blushingbread.com": "Leven Labs, Inc. DBA Admiral", "boilingcredit.com": "Leven Labs, Inc. DBA Admiral", @@ -51149,6 +51584,7 @@ "changeablecats.com": "Leven Labs, Inc. DBA Admiral", "chargecracker.com": "Leven Labs, Inc. DBA Admiral", "charmingplate.com": "Leven Labs, Inc. DBA Admiral", + "cheerycraze.com": "Leven Labs, Inc. DBA Admiral", "cherriescare.com": "Leven Labs, Inc. DBA Admiral", "chesscolor.com": "Leven Labs, Inc. DBA Admiral", "chickensstation.com": "Leven Labs, Inc. DBA Admiral", @@ -51183,6 +51619,7 @@ "confusedcart.com": "Leven Labs, Inc. DBA Admiral", "consciouscheese.com": "Leven Labs, Inc. DBA Admiral", "consciousdirt.com": "Leven Labs, Inc. DBA Admiral", + "cooingcoal.com": "Leven Labs, Inc. DBA Admiral", "coordinatedcoat.com": "Leven Labs, Inc. DBA Admiral", "copycarpenter.com": "Leven Labs, Inc. DBA Admiral", "cosmicsculptor.com": "Leven Labs, Inc. DBA Admiral", @@ -51213,6 +51650,8 @@ "damageddistance.com": "Leven Labs, Inc. DBA Admiral", "damdoor.com": "Leven Labs, Inc. DBA Admiral", "dampdock.com": "Leven Labs, Inc. DBA Admiral", + "dandydune.com": "Leven Labs, Inc. DBA Admiral", + "dandyglow.com": "Leven Labs, Inc. DBA Admiral", "dapperfloor.com": "Leven Labs, Inc. DBA Admiral", "daughterstone.com": "Leven Labs, Inc. DBA Admiral", "dazzlingbook.com": "Leven Labs, Inc. DBA Admiral", @@ -51246,6 +51685,7 @@ "drollwharf.com": "Leven Labs, Inc. DBA Admiral", "dustydime.com": "Leven Labs, Inc. DBA Admiral", "dustyhammer.com": "Leven Labs, Inc. DBA Admiral", + "eagereden.com": "Leven Labs, Inc. DBA Admiral", "eagerknight.com": "Leven Labs, Inc. DBA Admiral", "echoinghaven.com": "Leven Labs, Inc. DBA Admiral", "effervescentcoral.com": "Leven Labs, Inc. DBA Admiral", @@ -51278,11 +51718,14 @@ "expansioneggnog.com": "Leven Labs, Inc. DBA Admiral", "exquisiteartisanship.com": "Leven Labs, Inc. DBA Admiral", "exuberantedge.com": "Leven Labs, Inc. DBA Admiral", + "facilitatebreakfast.com": "Leven Labs, Inc. DBA Admiral", "fadedsnow.com": "Leven Labs, Inc. DBA Admiral", "fadewaves.com": "Leven Labs, Inc. DBA Admiral", "fairiesbranch.com": "Leven Labs, Inc. DBA Admiral", + "fairytaleflame.com": "Leven Labs, Inc. DBA Admiral", "fallaciousfifth.com": "Leven Labs, Inc. DBA Admiral", "fancyactivity.com": "Leven Labs, Inc. DBA Admiral", + "fancydune.com": "Leven Labs, Inc. DBA Admiral", "farmergoldfish.com": "Leven Labs, Inc. DBA Admiral", "farshake.com": "Leven Labs, Inc. DBA Admiral", "farsnails.com": "Leven Labs, Inc. DBA Admiral", @@ -51377,6 +51820,8 @@ "heartbreakingmind.com": "Leven Labs, Inc. DBA Admiral", "hearthorn.com": "Leven Labs, Inc. DBA Admiral", "heavyplayground.com": "Leven Labs, Inc. DBA Admiral", + "helpcollar.com": "Leven Labs, Inc. DBA Admiral", + "highfalutinhoney.com": "Leven Labs, Inc. DBA Admiral", "historicalbeam.com": "Leven Labs, Inc. DBA Admiral", "hocgeese.com": "Leven Labs, Inc. DBA Admiral", "hollowafterthought.com": "Leven Labs, Inc. DBA Admiral", @@ -51387,6 +51832,7 @@ "hospitablehat.com": "Leven Labs, Inc. DBA Admiral", "humdrumtouch.com": "Leven Labs, Inc. DBA Admiral", "hystericalcloth.com": "Leven Labs, Inc. DBA Admiral", + "idyllicjazz.com": "Leven Labs, Inc. DBA Admiral", "illinvention.com": "Leven Labs, Inc. DBA Admiral", "importantmeat.com": "Leven Labs, Inc. DBA Admiral", "impossibleexpansion.com": "Leven Labs, Inc. DBA Admiral", @@ -51396,9 +51842,13 @@ "inconclusiveaction.com": "Leven Labs, Inc. DBA Admiral", "inputicicle.com": "Leven Labs, Inc. DBA Admiral", "inquisitiveice.com": "Leven Labs, Inc. DBA Admiral", + "intelligentscissors.com": "Leven Labs, Inc. DBA Admiral", "internalcondition.com": "Leven Labs, Inc. DBA Admiral", "internalsink.com": "Leven Labs, Inc. DBA Admiral", + "irritatingfog.com": "Leven Labs, Inc. DBA Admiral", "j93557g.com": "Leven Labs, Inc. DBA Admiral", + "jollylens.com": "Leven Labs, Inc. DBA Admiral", + "joyfulkeen.com": "Leven Labs, Inc. DBA Admiral", "jubilantaura.com": "Leven Labs, Inc. DBA Admiral", "jubilantcanyon.com": "Leven Labs, Inc. DBA Admiral", "jubilantcascade.com": "Leven Labs, Inc. DBA Admiral", @@ -51436,10 +51886,13 @@ "lunchroomlock.com": "Leven Labs, Inc. DBA Admiral", "lustroushaven.com": "Leven Labs, Inc. DBA Admiral", "maddeningpowder.com": "Leven Labs, Inc. DBA Admiral", + "magicaljoin.com": "Leven Labs, Inc. DBA Admiral", "majesticwaterscape.com": "Leven Labs, Inc. DBA Admiral", + "majesticwilderness.com": "Leven Labs, Inc. DBA Admiral", "maliciousmusic.com": "Leven Labs, Inc. DBA Admiral", "marketspiders.com": "Leven Labs, Inc. DBA Admiral", "marriedbelief.com": "Leven Labs, Inc. DBA Admiral", + "marriedvalue.com": "Leven Labs, Inc. DBA Admiral", "materialisticmoon.com": "Leven Labs, Inc. DBA Admiral", "materialparcel.com": "Leven Labs, Inc. DBA Admiral", "materialplayground.com": "Leven Labs, Inc. DBA Admiral", @@ -51454,9 +51907,13 @@ "memorizematch.com": "Leven Labs, Inc. DBA Admiral", "memorizeneck.com": "Leven Labs, Inc. DBA Admiral", "mentorsticks.com": "Leven Labs, Inc. DBA Admiral", + "meremark.com": "Leven Labs, Inc. DBA Admiral", "merequartz.com": "Leven Labs, Inc. DBA Admiral", + "merryopal.com": "Leven Labs, Inc. DBA Admiral", + "merryvault.com": "Leven Labs, Inc. DBA Admiral", "mightyspiders.com": "Leven Labs, Inc. DBA Admiral", "minorcattle.com": "Leven Labs, Inc. DBA Admiral", + "minuteburst.com": "Leven Labs, Inc. DBA Admiral", "mixedreading.com": "Leven Labs, Inc. DBA Admiral", "modularmental.com": "Leven Labs, Inc. DBA Admiral", "monacobeatles.com": "Leven Labs, Inc. DBA Admiral", @@ -51492,6 +51949,7 @@ "oafishchance.com": "Leven Labs, Inc. DBA Admiral", "obscenesidewalk.com": "Leven Labs, Inc. DBA Admiral", "oldfashionedoffer.com": "Leven Labs, Inc. DBA Admiral", + "opalquill.com": "Leven Labs, Inc. DBA Admiral", "operationchicken.com": "Leven Labs, Inc. DBA Admiral", "optimallimit.com": "Leven Labs, Inc. DBA Admiral", "opulentsylvan.com": "Leven Labs, Inc. DBA Admiral", @@ -51513,6 +51971,7 @@ "peacefullimit.com": "Leven Labs, Inc. DBA Admiral", "petiteumbrella.com": "Leven Labs, Inc. DBA Admiral", "piquantgrove.com": "Leven Labs, Inc. DBA Admiral", + "piquantmeadow.com": "Leven Labs, Inc. DBA Admiral", "piquantvortex.com": "Leven Labs, Inc. DBA Admiral", "placidactivity.com": "Leven Labs, Inc. DBA Admiral", "placidperson.com": "Leven Labs, Inc. DBA Admiral", @@ -51528,6 +51987,7 @@ "pointdigestion.com": "Leven Labs, Inc. DBA Admiral", "pointlesspocket.com": "Leven Labs, Inc. DBA Admiral", "pointlessprofit.com": "Leven Labs, Inc. DBA Admiral", + "polishedcrescent.com": "Leven Labs, Inc. DBA Admiral", "polishedfolly.com": "Leven Labs, Inc. DBA Admiral", "politeplanes.com": "Leven Labs, Inc. DBA Admiral", "politicalporter.com": "Leven Labs, Inc. DBA Admiral", @@ -51537,6 +51997,7 @@ "potatoinvention.com": "Leven Labs, Inc. DBA Admiral", "powderjourney.com": "Leven Labs, Inc. DBA Admiral", "powerfulcopper.com": "Leven Labs, Inc. DBA Admiral", + "preciousplanes.com": "Leven Labs, Inc. DBA Admiral", "predictplate.com": "Leven Labs, Inc. DBA Admiral", "prepareplanes.com": "Leven Labs, Inc. DBA Admiral", "priceypies.com": "Leven Labs, Inc. DBA Admiral", @@ -51553,8 +52014,10 @@ "pumpedpancake.com": "Leven Labs, Inc. DBA Admiral", "punyplant.com": "Leven Labs, Inc. DBA Admiral", "purposepipe.com": "Leven Labs, Inc. DBA Admiral", + "quaintlake.com": "Leven Labs, Inc. DBA Admiral", "quietknowledge.com": "Leven Labs, Inc. DBA Admiral", "quillkick.com": "Leven Labs, Inc. DBA Admiral", + "quirkybliss.com": "Leven Labs, Inc. DBA Admiral", "quirkysugar.com": "Leven Labs, Inc. DBA Admiral", "quizzicalpartner.com": "Leven Labs, Inc. DBA Admiral", "quizzicalzephyr.com": "Leven Labs, Inc. DBA Admiral", @@ -51602,6 +52065,8 @@ "righteouscrayon.com": "Leven Labs, Inc. DBA Admiral", "rightfulfall.com": "Leven Labs, Inc. DBA Admiral", "rigidrobin.com": "Leven Labs, Inc. DBA Admiral", + "rigidveil.com": "Leven Labs, Inc. DBA Admiral", + "ringplant.com": "Leven Labs, Inc. DBA Admiral", "rollconnection.com": "Leven Labs, Inc. DBA Admiral", "roofrelation.com": "Leven Labs, Inc. DBA Admiral", "roseincome.com": "Leven Labs, Inc. DBA Admiral", @@ -51615,6 +52080,7 @@ "satisfycork.com": "Leven Labs, Inc. DBA Admiral", "savoryorange.com": "Leven Labs, Inc. DBA Admiral", "scarceshock.com": "Leven Labs, Inc. DBA Admiral", + "scarcestructure.com": "Leven Labs, Inc. DBA Admiral", "scaredcomfort.com": "Leven Labs, Inc. DBA Admiral", "scaredslip.com": "Leven Labs, Inc. DBA Admiral", "scaredsnake.com": "Leven Labs, Inc. DBA Admiral", @@ -51645,6 +52111,7 @@ "seraphicjubilee.com": "Leven Labs, Inc. DBA Admiral", "serenecascade.com": "Leven Labs, Inc. DBA Admiral", "serenepebble.com": "Leven Labs, Inc. DBA Admiral", + "serenesurf.com": "Leven Labs, Inc. DBA Admiral", "serioussuit.com": "Leven Labs, Inc. DBA Admiral", "serpentshampoo.com": "Leven Labs, Inc. DBA Admiral", "settleshoes.com": "Leven Labs, Inc. DBA Admiral", @@ -51839,6 +52306,7 @@ "vibranthaven.com": "Leven Labs, Inc. DBA Admiral", "vibrantpact.com": "Leven Labs, Inc. DBA Admiral", "vibranttalisman.com": "Leven Labs, Inc. DBA Admiral", + "vibrantvale.com": "Leven Labs, Inc. DBA Admiral", "virtualvincent.com": "Leven Labs, Inc. DBA Admiral", "vividcanopy.com": "Leven Labs, Inc. DBA Admiral", "vividfrost.com": "Leven Labs, Inc. DBA Admiral", From 5428ba3ed4fe64281dac3c7a88ed0d274ce5afc5 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 20 May 2024 18:42:14 +0000 Subject: [PATCH 131/134] Set marketing version to 1.89.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 94ced13f63..cee5ef12e7 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.88.0 +MARKETING_VERSION = 1.89.0 From f8ac63bdd06a05c5abdce28850d3f9e633862548 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 20 May 2024 18:53:55 +0000 Subject: [PATCH 132/134] Bump version to 1.89.0 (190) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index b1f7508f66..a8018d8541 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 189 +CURRENT_PROJECT_VERSION = 190 From 245d75acc7a22b81831b959fee1a991c89329111 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Mon, 20 May 2024 21:32:12 +0200 Subject: [PATCH 133/134] Update autoconsent to v10.8.0 (#2793) --- DuckDuckGo/Autoconsent/autoconsent-bundle.js | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index 85e242339a..77774fdb03 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_KLARO_OPEN_POPUP:()=>{klaro.show(void 0,!0)},EVAL_KLARO_TRY_API_OPT_OUT:()=>{if(window.klaro&&"function"==typeof klaro.show&&"function"==typeof klaro.getManager)try{return klaro.getManager().changeAll(!1),klaro.getManager().saveAndApplyConsents(),!0}catch(e){return console.warn(e),!1}return!1},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_TRUSTARC_FRAME_TEST:()=>window&&window.QueryString&&"0"===window.QueryString.preferences,EVAL_TRUSTARC_FRAME_GTM:()=>window&&window.QueryString&&"1"===window.QueryString.gtm,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!0}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){if(await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST"))return!0;let e=3e3;return await this.mainWorldEval("EVAL_TRUSTARC_FRAME_GTM")&&(e=1500),await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",e),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',e),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",e),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",e),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",10*e),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST")}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:['[data-project="mol-fe-cmp"] [class*=footer]',"xpath///button[contains(., 'Save & Exit')]"]}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"espace-personnel.agirc-arrco.fr",runContext:{urlPattern:"^https://espace-personnel\\.agirc-arrco\\.fr/"},prehideSelectors:[".cdk-overlay-container"],detectCmp:[{exists:".cdk-overlay-container app-esaa-cookie-component"}],detectPopup:[{visible:".cdk-overlay-container app-esaa-cookie-component"}],optIn:[{waitForThenClick:".btn-cookie-accepter"}],optOut:[{waitForThenClick:".btn-cookie-refuser"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6:has(._a9--)"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{waitForThenClick:".iubenda-cs-accept-btn"}],optOut:[{waitForThenClick:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{waitForThenClick:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"#rejectAllMain"}],optIn:[{click:"#acceptAllMain"}],optOut:[{click:"#rejectAllMain"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",prehideSelectors:[".cc_dialog.cc_css_reboot,.cc_overlay_lock"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{if:{exists:".cc_dialog.cc_css_reboot .cc_b_cp"},then:[{click:".cc_dialog.cc_css_reboot .cc_b_cp"},{waitForVisible:".cookie-consent-preferences-dialog .cc_cp_f_save button"},{waitForThenClick:".cookie-consent-preferences-dialog .cc_cp_f_save button"}],else:[{hide:".cc_dialog.cc_css_reboot,.cc_overlay_lock"}]}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); diff --git a/package-lock.json b/package-lock.json index 353201dd2a..29fc93354e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.6.1" + "@duckduckgo/autoconsent": "^10.8.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.6.1.tgz", - "integrity": "sha512-ptgT0sp4zmQTZHAyGR9TN/WJT9W7kTb/yvaF20FwwSIcLKd2xLe2jCDwbGTaLVSqAixWDKqzZ1Dg3l7HE159Sw==" + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.8.0.tgz", + "integrity": "sha512-n4axPmOsDxK9X6UYUb+S7KWqvppG75IemnH+pK1SAp01+US4ez+t7fzq4VQU19dy0EpGyC1bUnsB2BzdhUx65g==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index 78eb438846..a66b5e26a8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.6.1" + "@duckduckgo/autoconsent": "^10.8.0" } } From 5f20c6c8519bfb666f5637e11ec0f4b26ce0f404 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 21 May 2024 10:54:31 +0200 Subject: [PATCH 134/134] Adding to the Dock automatically (#2722) Task/Issue URL: https://app.asana.com/0/72649045549333/1206797051025460/f Tech Design URL: CC: **Description**: Adding to the Dock automatically during the onboarding, through the new tab page card and Settings. --- DuckDuckGo.xcodeproj/project.pbxproj | 24 + DuckDuckGo/Application/DockCustomizer.swift | 146 ++++ .../Application/DockPositionProvider.swift | 79 ++ .../Images/Dock-128.imageset/Contents.json | 12 + .../Images/Dock-128.imageset/Dock-128.pdf | Bin 0 -> 12816 bytes DuckDuckGo/Common/Localizables/UserText.swift | 11 + .../Utilities/UserDefaultsWrapper.swift | 1 + .../Model/HomePageContinueSetUpModel.swift | 105 ++- .../HomePage/View/ContinueSetUpView.swift | 35 +- .../View/HomePageViewController.swift | 1 + DuckDuckGo/Localizable.xcstrings | 678 +++++++++++++++++- .../Onboarding/View/OnboardingFlow.swift | 17 +- .../ViewModel/OnboardingViewModel.swift | 44 ++ .../Model/DefaultBrowserPreferences.swift | 7 +- .../View/PreferencesGeneralView.swift | 48 +- .../View/PreferencesRootView.swift | 3 +- .../Statistics/ATB/StatisticsLoader.swift | 10 + DuckDuckGo/Statistics/GeneralPixel.swift | 19 + .../Tab/View/BrowserTabViewController.swift | 6 + UnitTests/App/DockCustomizerMock.swift | 39 + UnitTests/App/DockPositionProviderTests.swift | 48 ++ .../HomePage/ContinueSetUpModelTests.swift | 38 +- .../CapturingDefaultBrowserProvider.swift | 2 + UnitTests/Onboarding/OnboardingTests.swift | 41 ++ .../DefaultBrowserPreferencesTests.swift | 2 + 25 files changed, 1378 insertions(+), 38 deletions(-) create mode 100644 DuckDuckGo/Application/DockCustomizer.swift create mode 100644 DuckDuckGo/Application/DockPositionProvider.swift create mode 100644 DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf create mode 100644 UnitTests/App/DockCustomizerMock.swift create mode 100644 UnitTests/App/DockPositionProviderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ddff21e5e7..acd1b22af2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC12936B816006F4388 /* BWMessageIdGeneratorTests.swift */; }; 1D3B1AC429378953006F4388 /* BWResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC329378953006F4388 /* BWResponseTests.swift */; }; 1D3B1AC62937A478006F4388 /* BWRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC52937A478006F4388 /* BWRequestTests.swift */; }; + 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */; }; + 1D4071AF2BD64267002D4537 /* DockCustomizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */; }; 1D43EB32292788C70065E5D6 /* BWEncryptionOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB31292788C70065E5D6 /* BWEncryptionOutput.m */; }; 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 1D43EB36292ACE690065E5D6 /* ApplicationVersionReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB35292ACE690065E5D6 /* ApplicationVersionReader.swift */; }; @@ -70,6 +72,8 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D69C552291302F200B75945 /* BWVault.swift */; }; 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */; }; 1D6A492129CF7A490011DF74 /* NSPopoverExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */; }; + 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */; }; + 1D7694002BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */; }; 1D77921828FDC54C00BE0210 /* FaviconReferenceCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921728FDC54C00BE0210 /* FaviconReferenceCacheTests.swift */; }; 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */; }; 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; @@ -85,6 +89,8 @@ 1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */; }; 1D8C2FF02B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */; }; 1D8C2FF12B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */; }; + 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */; }; + 1D9A37682BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */; }; 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; @@ -103,6 +109,8 @@ 1DA6D0FE2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; 1DA6D1032A1FFA3B00540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; + 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */; }; + 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */; }; 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */; }; 1DB67F2A2B6FEB17003DF243 /* WebViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */; }; 1DB67F2D2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F2C2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift */; }; @@ -2823,6 +2831,7 @@ 1D3B1AC12936B816006F4388 /* BWMessageIdGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWMessageIdGeneratorTests.swift; sourceTree = ""; }; 1D3B1AC329378953006F4388 /* BWResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWResponseTests.swift; sourceTree = ""; }; 1D3B1AC52937A478006F4388 /* BWRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWRequestTests.swift; sourceTree = ""; }; + 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockCustomizer.swift; sourceTree = ""; }; 1D43EB30292788C70065E5D6 /* BWEncryptionOutput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BWEncryptionOutput.h; sourceTree = ""; }; 1D43EB31292788C70065E5D6 /* BWEncryptionOutput.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BWEncryptionOutput.m; sourceTree = ""; }; 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWNotRespondingAlert.swift; sourceTree = ""; }; @@ -2833,6 +2842,7 @@ 1D6216B129069BBF00386B2C /* BWKeyStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWKeyStorage.swift; sourceTree = ""; }; 1D69C552291302F200B75945 /* BWVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWVault.swift; sourceTree = ""; }; 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPopoverExtension.swift; sourceTree = ""; }; + 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockCustomizerMock.swift; sourceTree = ""; }; 1D77921728FDC54C00BE0210 /* FaviconReferenceCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconReferenceCacheTests.swift; sourceTree = ""; }; 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconStoringMock.swift; sourceTree = ""; }; 1D77921C28FFF27C00BE0210 /* RunningApplicationCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningApplicationCheck.swift; sourceTree = ""; }; @@ -2842,6 +2852,7 @@ 1D8C2FE92B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTabSnapshotStore.swift; sourceTree = ""; }; + 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockPositionProvider.swift; sourceTree = ""; }; 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtension.swift; sourceTree = ""; }; 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferencesTests.swift; sourceTree = ""; }; 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferencesTests.swift; sourceTree = ""; }; @@ -2851,6 +2862,7 @@ 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatusTests.swift; sourceTree = ""; }; 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookie.swift; sourceTree = ""; }; 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookieTests.swift; sourceTree = ""; }; + 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockPositionProviderTests.swift; sourceTree = ""; }; 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSnapshotRenderer.swift; sourceTree = ""; }; 1DB67F2C2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewSnapshotRenderer.swift; sourceTree = ""; }; 1DB9617929F1D06D00CF5568 /* InternalUserDeciderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderMock.swift; sourceTree = ""; }; @@ -6514,6 +6526,8 @@ 858A798226A8B75F00A75A42 /* CopyHandler.swift */, 1D36E65A298ACD2900AA485D /* AppIconChanger.swift */, CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */, + 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */, + 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */, 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */, ); path = Application; @@ -7928,6 +7942,8 @@ B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */, B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */, B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */, + 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */, + 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */, ); path = App; sourceTree = ""; @@ -9821,6 +9837,7 @@ B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, + 1D4071AF2BD64267002D4537 /* DockCustomizer.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, @@ -10185,6 +10202,7 @@ 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 3706FC57293F65D500E42796 /* TabPreviewWindowController.swift in Sources */, + 1D9A37682BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 3706FC58293F65D500E42796 /* NSSizeExtension.swift in Sources */, 3706FC59293F65D500E42796 /* Fire.swift in Sources */, 3706FC5A293F65D500E42796 /* RandomAccessCollectionExtension.swift in Sources */, @@ -10385,6 +10403,7 @@ 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */, 3706FE19293F661700E42796 /* DeviceAuthenticatorTests.swift in Sources */, 3706FE1A293F661700E42796 /* BrowserProfileTests.swift in Sources */, + 1D7694002BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 3706FE1B293F661700E42796 /* PermissionManagerTests.swift in Sources */, 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, @@ -10520,6 +10539,7 @@ 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 3706FE68293F661700E42796 /* DuckPlayerURLExtensionTests.swift in Sources */, 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, + 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */, C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, @@ -11333,6 +11353,7 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */, AA6FFB4424DC33320028F4D0 /* NSViewExtension.swift in Sources */, B6C0B23E26E8BF1F0031CB7F /* DownloadListViewModel.swift in Sources */, + 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */, F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */, 4B723E1026B0006700E14D75 /* CSVImporter.swift in Sources */, @@ -11550,6 +11571,7 @@ B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, + 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, @@ -11742,6 +11764,7 @@ 4B11060525903E570039B979 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, 858A798826A99DBE00A75A42 /* PasswordManagementItemListModelTests.swift in Sources */, 566B196529CDB828007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, + 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, @@ -11866,6 +11889,7 @@ B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, + 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, diff --git a/DuckDuckGo/Application/DockCustomizer.swift b/DuckDuckGo/Application/DockCustomizer.swift new file mode 100644 index 0000000000..6a889bd058 --- /dev/null +++ b/DuckDuckGo/Application/DockCustomizer.swift @@ -0,0 +1,146 @@ +// +// DockCustomizer.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 Common + +protocol DockCustomization { + var isAddedToDock: Bool { get } + + @discardableResult + func addToDock() -> Bool +} + +final class DockCustomizer: DockCustomization { + + private let positionProvider: DockPositionProviding + + init(positionProvider: DockPositionProviding = DockPositionProvider()) { + self.positionProvider = positionProvider + } + + private var dockPlistURL: URL = URL(fileURLWithPath: NSString(string: "~/Library/Preferences/com.apple.dock.plist").expandingTildeInPath) + + private var dockPlistDict: [String: AnyObject]? { + return NSDictionary(contentsOf: dockPlistURL) as? [String: AnyObject] + } + + // This checks whether the bundle identifier of the current bundle + // is present in the 'persistent-apps' array of the Dock's plist. + var isAddedToDock: Bool { + guard let bundleIdentifier = Bundle.main.bundleIdentifier, + let dockPlistDict = dockPlistDict, + let persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] else { + return false + } + + return persistentApps.contains(where: { ($0["tile-data"] as? [String: AnyObject])?["bundle-identifier"] as? String == bundleIdentifier }) + } + + // Adds a dictionary representing the application, either by using an existing + // one from 'recent-apps' or creating a new one if the application isn't recently used. + // It then inserts this dictionary into the 'persistent-apps' list at a position + // determined by `positionProvider`. Following the plist update, it schedules the Dock + // to restart after a brief delay to apply the changes. + @discardableResult + func addToDock() -> Bool { + let appPath = Bundle.main.bundleURL.path + guard !isAddedToDock, + let bundleIdentifier = Bundle.main.bundleIdentifier, + var dockPlistDict = dockPlistDict else { + return false + } + + var persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] ?? [] + let recentApps = dockPlistDict["recent-apps"] as? [[String: AnyObject]] ?? [] + + let appDict: [String: AnyObject] + // Find the app in recent apps + if let recentAppIndex = recentApps.firstIndex(where: { appDict in + if let tileData = appDict["tile-data"] as? [String: AnyObject], + let appBundleIdentifier = tileData["bundle-identifier"] as? String { + return appBundleIdentifier == bundleIdentifier + } + return false + }) { + // Use existing dictonary from recentApps + appDict = recentApps[recentAppIndex] + } else { + // Create the dictionary for the current application if not found in recent apps + appDict = Self.appDict(appPath: appPath, bundleIdentifier: bundleIdentifier) + } + + // Insert to persistent apps + let index = positionProvider.newDockIndex(from: makeAppURLs(from: persistentApps)) + persistentApps.insert(appDict, at: index) + + // Update the plist + dockPlistDict["persistent-apps"] = persistentApps as AnyObject? + dockPlistDict["recent-apps"] = recentApps as AnyObject? + + // Update mod-count + dockPlistDict["mod-count"] = ((dockPlistDict["mod-count"] as? Int) ?? 0) + 1 as AnyObject + + do { + try (dockPlistDict as NSDictionary).write(to: dockPlistURL) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.restartDock() + } + return true + } catch { + os_log(.error, "Error writing to Dock plist: %{public}@", error.localizedDescription) + return false + } + } + + private func restartDock() { + let task = Process() + task.launchPath = "/usr/bin/killall" + task.arguments = ["Dock"] + task.launch() + } + + private func makeAppURLs(from persistentApps: [[String: AnyObject]]) -> [URL] { + return persistentApps.compactMap { appDict in + if let tileData = appDict["tile-data"] as? [String: AnyObject], + let appBundleIdentifier = tileData["file-data"] as? [String: AnyObject], + let urlString = appBundleIdentifier["_CFURLString"] as? String, + let url = URL(string: urlString) { + return url + } else { + return nil + } + } + } + + static func appDict(appPath: String, bundleIdentifier: String) -> [String: AnyObject] { + return ["tile-type": "file-tile" as AnyObject, + "tile-data": [ + "dock-extra": 0 as AnyObject, + "file-type": 1 as AnyObject, + "file-data": [ + "_CFURLString": "file://" + appPath + "/", + "_CFURLStringType": 15 + ], + "file-label": "DuckDuckGo" as AnyObject, + "bundle-identifier": bundleIdentifier as AnyObject, + "is-beta": 0 as AnyObject + ] as AnyObject + ] + } +} diff --git a/DuckDuckGo/Application/DockPositionProvider.swift b/DuckDuckGo/Application/DockPositionProvider.swift new file mode 100644 index 0000000000..f93ba428c1 --- /dev/null +++ b/DuckDuckGo/Application/DockPositionProvider.swift @@ -0,0 +1,79 @@ +// +// DockPositionProvider.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 + +enum DockApp: String, CaseIterable { + case chrome = "/Applications/Google Chrome.app/" + case firefox = "/Applications/Firefox.app/" + case edge = "/Applications/Microsoft Edge.app/" + case brave = "/Applications/Brave Browser.app/" + case opera = "/Applications/Opera.app/" + case arc = "/Applications/Arc.app/" + case safari = "/Applications/Safari.app/" + case safariLong = "/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app/" + + var url: URL { + return URL(string: "file://" + self.rawValue)! + } +} + +protocol DockPositionProviding { + func newDockIndex(from currentAppURLs: [URL]) -> Int +} + +/// Class to determine the best positioning in the Dock +final class DockPositionProvider: DockPositionProviding { + + private let preferredOrder: [DockApp] = [ + .chrome, + .firefox, + .edge, + .brave, + .opera, + .arc, + .safari, + .safariLong + ] + + private var defaultBrowserProvider: DefaultBrowserProvider + + init(defaultBrowserProvider: DefaultBrowserProvider = SystemDefaultBrowserProvider()) { + self.defaultBrowserProvider = defaultBrowserProvider + } + + /// Determines the new dock index for a new app based on the default browser or preferred order + func newDockIndex(from currentAppURLs: [URL]) -> Int { + // Place next to the default browser + if !defaultBrowserProvider.isDefault, + let defaultBrowserURL = defaultBrowserProvider.defaultBrowserURL, + let position = currentAppURLs.firstIndex(of: defaultBrowserURL) { + return position + 1 + } + + // Place based on the preferred order + for app in preferredOrder { + if let position = currentAppURLs.firstIndex(of: app.url) { + return position + 1 + } + } + + // Otherwise, place at the end + return currentAppURLs.count + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json new file mode 100644 index 0000000000..e7bb0888d9 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Dock-128.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9a0511ce2c0c14e8f62484a2e049937b18473ab2 GIT binary patch literal 12816 zcmeHNTW=e=6@H&zF)smj3q%@{LkmRNiMQo?0@ff zNX|#<;z~|>LEFrOO?^Wio;wdYe0KTz&2u&Cs!G*Oy?Xy+RoCBrSC7A+EpN9UYYp%U z5T5bXWO+NQ|5!JpCOowo8s{K?N>pC2?k4l)Q8Gi9HxJ9{cD`Ev{-^s{-D2E}7u9CF zo=xtmpKDCt8Pix-&pu5*%CEk4t{Z;!4o~`Z^KhS;z1`M7$4>XF`y-8=&YOq#_48(A zOsBQ3?;aL;*2&FH9DQtnHr^RO42X<9J^*Sn@}&NWb@uGa zoma`4+Kbx;Ws}a?%7&lmF#NRoL?=;ws_m&hOH|bN^5yDN83Q?jv>&3XcSCE?-_>lh zdRR|qn-HRxlkIl4UdFZx|K3fu>$wnqu5}!&YpX{##_RK)BuD)7jhg zyp@+=w4>`beSa!Qco@Wh&`^DLAqvqK2<1__8QD#}NU4bmq<0@GvqTT~yb!7Xc; zXMCyZ&2(9PsFu}{1yX!1X86{2%`b{?|GzbYRe$-q}{U+#z2(G-$Xu2h=m zrQ9!1vV%fGpq`gj$Dfx-hn#Rk_`w-YFmX< zeo2>9(PQi$Co;a!Sw*4lYq$vQGBUofXZP%L&l75IbI%jAo3neWCU-qCeJmz}Cp|uW z&>a2@`vbp|{?e-4i}epzMdXZPQIuaux__z07plJ9RM?NEr7$mwxy6@CnW3MJEv$86 zeY9~~)r|HVlGD2p3f#6ebvp_h0#)BtcI36Uov-ak_43pUV7ArXwZOE+2&|)_felrB zWo_#29U4Nw_4TxhWK~{CHGMhwij_L+%nXUS zlQuJmEFt-#6>7oJf`%g}c~^myfR6b zybV?g;)$;FtaOXrfHqGEXeJO7TW7`dVMh77BL6?H$j!fbMV6}Ie|bf2C4gBrGr=7z zskI%FWE;zZvjP}P*j^bV2U4yRG^broeE5n4iiJgwj7|g+C6@c_PDDz?)SVa`hp$LL z7NoQ)kSlg4!|YCEH$&R96`T+9bWGGz-iosJcPd0=%fdX)`DIAIr*pnMd_@9s26)>dYq-uz~c? zvWai8BlD4nRG(?A<6N8|LwtvFl5IAm)=pH|IF6OODKAPqlBtiKkvNa-Zf{%g>qdR= zEcPv@)u^Phb~yVG6&QR##%4;+FtqHKh{^u6b*4eVNGvtvv_M|<&G1S?VSzx0m6g3v z`?lJECsIOv+~@{I;(SCMJ|eqGsDFYb zWH)P^Hz?XI{7-w>_=>r#? z)fN*`B02BDp^Xch!z4GhTu`kwSvO{6`@Yd~qLnl#3)j9@U*guj*jl{Yh%y@0V-?w1c=86jSb3D} z=y#VM#c4Q8hRS_C%b-!(?lGnHoDGt#X{18yK7Vrva(3B zLc_LLZQZWr+;A9OYzpRNr~*_s+BlZ5onAFvW1;2zUe8ivB@HsvmlCAuW-@0&Jbz9aAYDADO!BXA$fq* zz6Yc;1BX13Mxj5FV1pFcorvt1I&3|$`Gh#3fm{Ol!(}puii#jU-iLltC(1oFB(4la z8lm3JU?PIDupk$ypl6_dh7lBSAzL;lvzTDTJM^ax+_)HpDT zk2RQ7o&ti4>_Of_^xa-wLVX8<*FuwfkbIEbfv_qknE>-d*L`{A8KXG zoO*57llfw{u0(aed5xRD-)vTQvv2Ef<~QF)ON_5pE0{sF9Lznwp8sto8iNfs+sS$x Qbf|ROx$4=oiyz+n9rjo5;s5{u literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 53319c1e7b..02fb18ac46 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -616,6 +616,10 @@ struct UserText { static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") + static let shortcuts = NSLocalizedString("preferences.shortcuts", value: "Shortcuts", comment: "Name of the preferences section related to shortcuts") + static let isAddedToDock = NSLocalizedString("preferences.is-added-to-dock", value: "DuckDuckGo is added to the Dock.", comment: "Indicates that the browser is added to the macOS system Dock") + static let isNotAddedToDock = NSLocalizedString("preferences.not-added-to-dock", value: "DuckDuckGo is not added to the Dock.", comment: "Indicate that the browser is not added to macOS system Dock") + static let addToDock = NSLocalizedString("preferences.add-to-dock", value: "Add to Dock…", comment: "Action button to add the app to the Dock") static let onStartup = NSLocalizedString("preferences.on-startup", value: "On Startup", comment: "Name of the preferences section related to app startup") static let reopenAllWindowsFromLastSession = NSLocalizedString("preferences.reopen-windows", value: "Reopen all windows from last session", comment: "Option to control session restoration") static let showHomePage = NSLocalizedString("preferences.show-home", value: "Open a new window", comment: "Option to control session startup") @@ -794,11 +798,14 @@ struct UserText { static let onboardingWelcomeText = NSLocalizedString("onboarding.welcome.text", value: "Tired of being tracked online? You've come to the right place 👍\n\nI'll help you stay private️ as you search and browse the web. Trackers be gone!", comment: "Detailed welcome to the app text") static let onboardingImportDataText = NSLocalizedString("onboarding.importdata.text", value: "First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers.", comment: "Call to action to import data from other browsers") static let onboardingSetDefaultText = NSLocalizedString("onboarding.setdefault.text", value: "Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time.", comment: "Call to action to set the browser as default") + static let onboardingAddToDockText = NSLocalizedString("onboarding.addtodock.text", value: "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?", comment: "Call to action to add the DuckDuckGo app icon to the macOS system dock") static let onboardingStartBrowsingText = NSLocalizedString("onboarding.startbrowsing.text", value: "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser") + static let onboardingStartBrowsingAddedToDockText = NSLocalizedString("onboarding.startbrowsing.added-to-dock.text", value: "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser") static let onboardingStartButton = NSLocalizedString("onboarding.welcome.button", value: "Get Started", comment: "Start the onboarding flow") static let onboardingImportDataButton = NSLocalizedString("onboarding.importdata.button", value: "Import", comment: "Launch the import data UI") static let onboardingSetDefaultButton = NSLocalizedString("onboarding.setdefault.button", value: "Let's Do It!", comment: "Launch the set default UI") + static let onboardingAddToDockButton = NSLocalizedString("onboarding.addtodock.button", value: "Keep in Dock", comment: "Button label to add application to the macOS system dock") static let onboardingNotNowButton = NSLocalizedString("onboarding.notnow.button", value: "Maybe Later", comment: "Skip a step of the onboarding flow") static func importingBookmarks(_ numberOfBookmarks: Int?) -> String { @@ -1070,17 +1077,21 @@ struct UserText { // Set Up static let newTabSetUpSectionTitle = NSLocalizedString("newTab.setup.section.title", value: "Next Steps", comment: "Title of the setup section in the home page") static let newTabSetUpDefaultBrowserCardTitle = NSLocalizedString("newTab.setup.default.browser.title", value: "Default to Privacy", comment: "Title of the Default Browser card of the Set Up section in the home page") + static let newTabSetUpDockCardTitle = NSLocalizedString("newTab.setup.dock.title", value: "Keep in Your Dock", comment: "Title of the new tab page card for adding application to the Dock") static let newTabSetUpImportCardTitle = NSLocalizedString("newTab.setup.import.title", value: "Bring Your Stuff", comment: "Title of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerCardTitle = NSLocalizedString("newTab.setup.duck.player.title", value: "Clean Up YouTube", comment: "Title of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionCardTitle = NSLocalizedString("newTab.setup.email.protection.title", value: "Protect Your Inbox", comment: "Title of the Email Protection card of the Set Up section in the home page") static let newTabSetUpDefaultBrowserAction = NSLocalizedString("newTab.setup.default.browser.action", value: "Make Default Browser", comment: "Action title on the action menu of the Default Browser card") + static let newTabSetUpDockAction = NSLocalizedString("newTab.setup.dock.action", value: "Keep In Dock", comment: "Action title on the action menu of the 'Add App to the Dock' card") + static let newTabSetUpDockConfirmation = NSLocalizedString("newTab.setup.dock.confirmation", value: "Added to Dock!", comment: "Confirmation title after user clicks on 'Add to Dock' card") static let newTabSetUpImportAction = NSLocalizedString("newTab.setup.Import.action", value: "Import Now", comment: "Action title on the action menu of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerAction = NSLocalizedString("newTab.setup.duck.player.action", value: "Try Duck Player", comment: "Action title on the action menu of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionAction = NSLocalizedString("newTab.setup.email.protection.action", value: "Get a Duck Address", comment: "Action title on the action menu of the Email Protection card of the Set Up section in the home page") static let newTabSetUpRemoveItemAction = NSLocalizedString("newTab.setup.remove.item", value: "Dismiss", comment: "Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item") static let newTabSetUpDefaultBrowserSummary = NSLocalizedString("newTab.setup.default.browser.summary", value: "We automatically block trackers as you browse. It's privacy, simplified.", comment: "Summary of the Default Browser card") + static let newTabSetUpDockSummary = NSLocalizedString("newTab.setup.dock.summary", value: "Get to DuckDuckGo faster by adding it to your Dock.", comment: "Summary of the 'Add App to the Dock' card") static let newTabSetUpImportSummary = NSLocalizedString("newTab.setup.import.summary", value: "Import bookmarks, favorites, and passwords from your old browser.", comment: "Summary of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerSummary = NSLocalizedString("newTab.setup.duck.player.summary", value: "Enjoy a clean viewing experience without personalized ads.", comment: "Summary of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionSummary = NSLocalizedString("newTab.setup.email.protection.summary", value: "Generate custom @duck.com addresses that clean trackers from incoming email.", comment: "Summary of the Email Protection card of the Set Up section in the home page") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 5c72907b8e..13193f793f 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -117,6 +117,7 @@ public struct UserDefaultsWrapper { case homePageShowAllFavorites = "home.page.show.all.favorites" case homePageShowAllFeatures = "home.page.show.all.features" case homePageShowMakeDefault = "home.page.show.make.default" + case homePageShowAddToDock = "home.page.show.add.to.dock" case homePageShowImport = "home.page.show.import" case homePageShowDuckPlayer = "home.page.show.duck.player" case homePageShowEmailProtection = "home.page.show.email.protection" diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index b99a4a2949..22ddf43e7e 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -48,6 +48,7 @@ extension HomePage.Models { } private let defaultBrowserProvider: DefaultBrowserProvider + private let dockCustomizer: DockCustomization private let dataImportProvider: DataImportStatusProviding private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager @@ -63,6 +64,9 @@ extension HomePage.Models { @UserDefaultsWrapper(key: .homePageShowMakeDefault, defaultValue: true) private var shouldShowMakeDefaultSetting: Bool + @UserDefaultsWrapper(key: .homePageShowAddToDock, defaultValue: true) + private var shouldShowAddToDockSetting: Bool + @UserDefaultsWrapper(key: .homePageShowImport, defaultValue: true) private var shouldShowImportSetting: Bool @@ -100,6 +104,7 @@ extension HomePage.Models { @Published var visibleFeaturesMatrix: [[FeatureType]] = [[]] init(defaultBrowserProvider: DefaultBrowserProvider, + dockCustomizer: DockCustomization, dataImportProvider: DataImportStatusProviding, tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), @@ -108,6 +113,7 @@ extension HomePage.Models { privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, permanentSurveyManager: SurveyManager = PermanentSurveyManager()) { self.defaultBrowserProvider = defaultBrowserProvider + self.dockCustomizer = dockCustomizer self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager @@ -125,22 +131,15 @@ extension HomePage.Models { @MainActor func performAction(for featureType: FeatureType) { switch featureType { case .defaultBrowser: - do { - PixelKit.fire(GeneralPixel.defaultRequestedFromHomepageSetupView) - try defaultBrowserProvider.presentDefaultBrowserPrompt() - } catch { - defaultBrowserProvider.openSystemPreferences() - } + performDefaultBrowserAction() + case .dock: + performDockAction() case .importBookmarksAndPasswords: - dataImportProvider.showImportWindow(completion: {self.refreshFeaturesMatrix()}) + performImportBookmarksAndPasswordsAction() case .duckplayer: - if let videoUrl = URL(string: duckPlayerURL) { - let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - } + performDuckPlayerAction() case .emailProtection: - let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) + performEmailProtectionAction() case .permanentSurvey: visitSurvey() case .networkProtectionRemoteMessage(let message): @@ -148,16 +147,56 @@ extension HomePage.Models { case .dataBrokerProtectionRemoteMessage(let message): handle(remoteMessage: message) case .dataBrokerProtectionWaitlistInvited: -#if DBP - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .cardUI) -#endif + performDataBrokerProtectionWaitlistInvitedAction() + } + } + + private func performDefaultBrowserAction() { + do { + PixelKit.fire(GeneralPixel.defaultRequestedFromHomepageSetupView) + try defaultBrowserProvider.presentDefaultBrowserPrompt() + } catch { + defaultBrowserProvider.openSystemPreferences() } } + private func performImportBookmarksAndPasswordsAction() { + dataImportProvider.showImportWindow(completion: { self.refreshFeaturesMatrix() }) + } + + @MainActor + private func performDuckPlayerAction() { + if let videoUrl = URL(string: duckPlayerURL) { + let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + } + } + + @MainActor + private func performEmailProtectionAction() { + let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + } + + @MainActor + private func performDataBrokerProtectionWaitlistInvitedAction() { + #if DBP + DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .cardUI) + #endif + } + + func performDockAction() { + PixelKit.fire(GeneralPixel.userAddedToDockFromNewTabPageCard, + includeAppVersionParameter: false) + dockCustomizer.addToDock() + } + func removeItem(for featureType: FeatureType) { switch featureType { case .defaultBrowser: shouldShowMakeDefaultSetting = false + case .dock: + shouldShowAddToDockSetting = false case .importBookmarksAndPasswords: shouldShowImportSetting = false case .duckplayer: @@ -196,7 +235,6 @@ extension HomePage.Models { for message in homePageRemoteMessaging.networkProtectionRemoteMessaging.presentableRemoteMessages() { PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) } - appendFeatureCards(&features) featuresMatrix = features.chunked(into: itemsPerRow) @@ -214,6 +252,8 @@ extension HomePage.Models { return shouldMakeDefaultCardBeVisible case .importBookmarksAndPasswords: return shouldImportCardBeVisible + case .dock: + return shouldDockCardBeVisible case .duckplayer: return shouldDuckPlayerCardBeVisible case .emailProtection: @@ -274,6 +314,15 @@ extension HomePage.Models { !defaultBrowserProvider.isDefault } + private var shouldDockCardBeVisible: Bool { +#if !APPSTORE + shouldShowAddToDockSetting && + !dockCustomizer.isAddedToDock +#else + return false +#endif + } + private var shouldImportCardBeVisible: Bool { shouldShowImportSetting && !dataImportProvider.didImport @@ -369,12 +418,17 @@ extension HomePage.Models { // We ignore the `networkProtectionRemoteMessage` case here to avoid it getting accidentally included - it has special handling and will get // included elsewhere. static var allCases: [HomePage.Models.FeatureType] { +#if APPSTORE [.duckplayer, .emailProtection, .defaultBrowser, .importBookmarksAndPasswords, .permanentSurvey] +#else + [.duckplayer, .emailProtection, .defaultBrowser, .dock, .importBookmarksAndPasswords, .permanentSurvey] +#endif } case duckplayer case emailProtection case defaultBrowser + case dock case importBookmarksAndPasswords case permanentSurvey case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage) @@ -385,6 +439,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserCardTitle + case .dock: + return UserText.newTabSetUpDockCardTitle case .importBookmarksAndPasswords: return UserText.newTabSetUpImportCardTitle case .duckplayer: @@ -406,6 +462,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserSummary + case .dock: + return UserText.newTabSetUpDockSummary case .importBookmarksAndPasswords: return UserText.newTabSetUpImportSummary case .duckplayer: @@ -427,6 +485,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserAction + case .dock: + return UserText.newTabSetUpDockAction case .importBookmarksAndPasswords: return UserText.newTabSetUpImportAction case .duckplayer: @@ -444,12 +504,23 @@ extension HomePage.Models { } } + var confirmation: String? { + switch self { + case .dock: + return UserText.newTabSetUpDockConfirmation + default: + return nil + } + } + var icon: NSImage { let iconSize = NSSize(width: 64, height: 48) switch self { case .defaultBrowser: return .defaultApp128.resized(to: iconSize)! + case .dock: + return .dock128.resized(to: iconSize)! case .importBookmarksAndPasswords: return .import128.resized(to: iconSize)! case .duckplayer: diff --git a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift index 1fa9e6639b..e6d56fd18f 100644 --- a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift +++ b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift @@ -18,6 +18,7 @@ import SwiftUI import SwiftUIExtensions +import PixelKit extension HomePage.Views { @@ -100,7 +101,7 @@ extension HomePage.Views { .frame(width: 24, height: 24) } ZStack { - CardTemplate(title: featureType.title, summary: featureType.summary, actionText: featureType.action, icon: icon, width: model.itemWidth, height: model.itemHeight, action: { model.performAction(for: featureType) }) + CardTemplate(title: featureType.title, summary: featureType.summary, actionText: featureType.action, confirmationText: featureType.confirmation, icon: icon, width: model.itemWidth, height: model.itemHeight, action: { model.performAction(for: featureType) }) .contextMenu(ContextMenu(menuItems: { Button(featureType.action, action: { model.performAction(for: featureType) }) Divider() @@ -121,6 +122,13 @@ extension HomePage.Views { .onHover { isHovering in self.isHovering = isHovering } + .onAppear { + if featureType == .dock { + PixelKit.fire(GeneralPixel.addToDockNewTabPageCardPresented, + frequency: .unique, + includeAppVersionParameter: false) + } + } } } @@ -129,12 +137,14 @@ extension HomePage.Views { var title: String var summary: String var actionText: String + var confirmationText: String? @ViewBuilder var icon: Content let width: CGFloat let height: CGFloat let action: () -> Void @State var isHovering = false + @State var isClicked = false var body: some View { ZStack(alignment: .center) { @@ -166,7 +176,23 @@ extension HomePage.Views { .frame(width: 208, height: 130) VStack { Spacer() - ActionButton(title: actionText, isHoveringOnCard: $isHovering, action: action) + if let confirmationText, isClicked { + HStack { + Image(.successCheckmark) + Text(confirmationText) + .bold() + .multilineTextAlignment(.center) + .lineLimit(1) + .font(.system(size: 11)) + .fixedSize(horizontal: false, vertical: true) + } + .offset(y: -3) + } else { + ActionButton(title: actionText, + isHoveringOnCard: $isHovering, + isClicked: $isClicked, + action: action) + } } .padding(8) } @@ -188,11 +214,13 @@ extension HomePage.Views { @State var isHovering = false @Binding var isHoveringOnCard: Bool + @Binding var isClicked: Bool - init(title: String, isHoveringOnCard: Binding, action: @escaping () -> Void) { + init(title: String, isHoveringOnCard: Binding, isClicked: Binding, action: @escaping () -> Void) { self.title = title self.action = action self._isHoveringOnCard = isHoveringOnCard + self._isClicked = isClicked self.titleWidth = (title as NSString).size(withAttributes: [.font: NSFont.systemFont(ofSize: 11) as Any]).width + 14 } @@ -217,6 +245,7 @@ extension HomePage.Views { .foregroundColor(Color(.linkBlue)) } .onTapGesture { + isClicked = true action() } .onHover { isHovering in diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 9769333182..092a33384c 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -150,6 +150,7 @@ final class HomePageViewController: NSViewController { func createFeatureModel() -> HomePage.Models.ContinueSetUpModel { return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: SystemDefaultBrowserProvider(), + dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 9ff3c8becb..4503c03e71 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -30609,6 +30609,246 @@ } } }, + "newTab.setup.dock.action" : { + "comment" : "Action title on the action menu of the 'Add App to the Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep In Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans le Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resta nel dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In het Dock houden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык для док-панели" + } + } + } + }, + "newTab.setup.dock.confirmation" : { + "comment" : "Confirmation title after user clicks on 'Add to Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Dock hinzugefügt!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Added to Dock!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Añadido al Dock!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouté au Dock !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiunto al dock!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegevoegd aan Dock!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodano do Docka!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionado à Dock!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык добавлен на док-панель." + } + } + } + }, + "newTab.setup.dock.summary" : { + "comment" : "Summary of the 'Add App to the Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst DuckDuckGo schneller erreichen, indem du es zu deinem Dock hinzufügst." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Get to DuckDuckGo faster by adding it to your Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accede a DuckDuckGo más rápido añadiéndolo a tu Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accédez plus rapidement à DuckDuckGo en l'ajoutant à votre Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accedi più velocemente a DuckDuckGo aggiungendolo al tuo dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ga sneller naar DuckDuckGo door het aan je Dock toe te voegen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzyskuj szybszy dostęp do przeglądarki DuckDuckGo dzięki jej dodaniu do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acede ao DuckDuckGo mais rapidamente adicionando-o à tua Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавьте DuckDuckGo на док-панель для быстрого запуска." + } + } + } + }, + "newTab.setup.dock.title" : { + "comment" : "Title of the new tab page card for adding application to the Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In deinem Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep in Your Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en tu Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans votre Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni nel tuo dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bewaar in je dock" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na tua Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык для док-панели" + } + } + } + }, "newTab.setup.duck.player.action" : { "comment" : "Action title on the action menu of the Duck Player card of the Set Up section in the home page", "extractionState" : "extracted_with_value", @@ -31696,55 +31936,175 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "OK" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "OK" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De acuerdo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хорошо" + } + } + } + }, + "onboarding.addtodock.button" : { + "comment" : "Button label to add application to the macOS system dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep in Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans le Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni nel Dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Dock bewaren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Держать на док-панели" + } + } + } + }, + "onboarding.addtodock.text" : { + "comment" : "Call to action to add the DuckDuckGo app icon to the macOS system dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine letzte Sache. Du möchtest DuckDuckGo in deinem Dock haben, damit der Browser immer in Reichweite ist?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "OK" + "value" : "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "De acuerdo" + "value" : "Una última cosa. ¿Quieres tener DuckDuckGo en tu Dock para que el navegador esté siempre al alcance de la mano?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "Une dernière chose. Vous voulez garder DuckDuckGo dans votre Dock pour que le navigateur reste à portée de main ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Un'ultima cosa. Vuoi tenere DuckDuckGo nel tuo dock in modo che il browser sia sempre disponibile?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Nog een laatste ding. Wil je DuckDuckGo in je Dock houden zodat de browser altijd binnen handbereik is?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Ostatnia sprawa. Czy chcesz trzymać przeglądarkę DuckDuckGo w Docku, aby zawsze ją mieć pod ręką?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Só mais uma coisa. Queres ter o navegador DuckDuckGo na tua Dock estar sempre à mão?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Хорошо" + "value" : "И кое-что еще... Хотите сохранить DuckDuckGo на док-панели, чтобы наш браузер всегда был под рукой?" } } } @@ -32049,6 +32409,66 @@ } } }, + "onboarding.startbrowsing.added-to-dock.text" : { + "comment" : "Call to action to start using the app as a browser", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Du bist bereit! Du kannst mich jederzeit im Dock antreffen.\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglichu{00A0}🔒" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "¡Ya está todo listo! Puedes encontrarme en el Dock en cualquier momento.\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nNo pierdas de vista la barra de direcciones al navegar. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible{00A0}🔒" + } + }, + "fr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Tout est prêt ! Vous pouvez me trouver sur le Dock à tout moment.\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\nContinuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Tutto pronto! Puoi trovarmi nel dock in qualsiasi momento.\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆\n\nContinua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile{00A0} 🔒" + } + }, + "nl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Je bent helemaal klaar! Je kunt me altijd vinden in het Dock.\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆\n\nKijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Wszystko gotowe! W każdej chwili możesz mnie znaleźć w Docku.\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" + } + }, + "pt" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Está tudo pronto! Podes encontrar-me na Dock em qualquer altura.\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" + } + }, + "ru" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" + } + } + } + }, "onboarding.startbrowsing.text" : { "comment" : "Call to action to start using the app as a browser", "extractionState" : "extracted_with_value", @@ -43887,6 +44307,66 @@ } } }, + "preferences.add-to-dock" : { + "comment" : "Action button to add the app to the Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Dock hinzufügen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add to Dock…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir al Dock…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter au Dock…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi al dock…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "App toevoegen aan je dock…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj do Docka…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar à Dock…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить на док-панель…" + } + } + } + }, "preferences.always-on" : { "comment" : "Status indicator of a browser privacy protection feature.", "extractionState" : "extracted_with_value", @@ -45267,6 +45747,66 @@ } } }, + "preferences.is-added-to-dock" : { + "comment" : "Indicates that the browser is added to the macOS system Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo wird zum Dock hinzugefügt." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo is added to the Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo se ha añadido al Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo a été ajouté au Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo è stato aggiunto al dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo is toegevoegd aan het Dock." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądarka DuckDuckGo została dodana do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo foi adicionado à Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык DuckDuckGo добавлен на док-панель." + } + } + } + }, "preferences.main-settings" : { "comment" : "Section header in Preferences for main settings", "extractionState" : "extracted_with_value", @@ -45327,6 +45867,66 @@ } } }, + "preferences.not-added-to-dock" : { + "comment" : "Indicate that the browser is not added to macOS system Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo wird nicht zum Dock hinzugefügt." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo is not added to the Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo no se ha añadido al Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo n'a pas été ajouté au Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo non è stato aggiunto al dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo is niet toegevoegd aan het Dock." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądarka DuckDuckGo nie została dodana do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo não foi adicionado à Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык DuckDuckGo не добавлен на док-панель." + } + } + } + }, "preferences.off" : { "comment" : "Status indicator of a browser privacy protection feature.", "extractionState" : "extracted_with_value", @@ -45687,6 +46287,66 @@ } } }, + "preferences.shortcuts" : { + "comment" : "Name of the preferences section related to shortcuts", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcuts" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Shortcuts" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesos directos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raccourcis" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scorciatoie" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sneltoetsen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skróty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atalhos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлыки" + } + } + } + }, "preferences.show-home" : { "comment" : "Option to control session startup", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Onboarding/View/OnboardingFlow.swift b/DuckDuckGo/Onboarding/View/OnboardingFlow.swift index 721f60bb88..23b2179eaf 100644 --- a/DuckDuckGo/Onboarding/View/OnboardingFlow.swift +++ b/DuckDuckGo/Onboarding/View/OnboardingFlow.swift @@ -30,6 +30,14 @@ struct OnboardingFlow: View { @State var daxInSpeechPosition = false @State var showDialogs = false + var startBrowsingText: String { + if model.addToDockPressed { + return UserText.onboardingStartBrowsingAddedToDockText + } else { + return UserText.onboardingStartBrowsingText + } + } + var body: some View { VStack(alignment: daxInSpeechPosition ? .leading : .center) { @@ -62,7 +70,14 @@ struct OnboardingFlow: View { model.onSetDefaultSkipped() }.visibility(model.state == .setDefault ? .visible : .gone) - DaxSpeech(text: UserText.onboardingStartBrowsingText, onTypingFinished: nil) + ActionSpeech(text: UserText.onboardingAddToDockText, + actionName: UserText.onboardingAddToDockButton) { + model.onAddToDockPressed() + } skip: { + model.onAddToDockSkipped() + }.visibility(model.state == .addToDock ? .visible : .gone) + + DaxSpeech(text: startBrowsingText, onTypingFinished: nil) .visibility(model.state == .startBrowsing ? .visible : .gone) }.visibility(showDialogs ? .visible : .gone) diff --git a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift index e9448763fc..8a38258b23 100644 --- a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift +++ b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift @@ -17,6 +17,7 @@ // import SwiftUI +import PixelKit protocol OnboardingDelegate: NSObjectProtocol { @@ -26,6 +27,9 @@ protocol OnboardingDelegate: NSObjectProtocol { /// Request set default should be launched. Whatever happens, call the completion to move on to the next screen. func onboardingDidRequestSetDefault(completion: @escaping () -> Void) + /// Adding to the Dock should be launched. Whatever happens, call the completion to move on to the next screen. + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) + /// Has finished, but still showing a screen. This is when to re-enable the UI. func onboardingHasFinished() @@ -39,16 +43,27 @@ final class OnboardingViewModel: ObservableObject { case welcome case importData case setDefault + case addToDock case startBrowsing } var typingDisabled = false + var addToDockPressed = false @Published var skipTypingRequested = false @Published var state: OnboardingPhase = .startFlow { didSet { skipTypingRequested = false + + if state == .addToDock { + PixelKit.fire(GeneralPixel.addToDockOnboardingStepPresented, + includeAppVersionParameter: false) + } + if state == .startBrowsing { + PixelKit.fire(GeneralPixel.startBrowsingOnboardingStepPresented, + includeAppVersionParameter: false) + } } } @@ -105,14 +120,43 @@ final class OnboardingViewModel: ObservableObject { @MainActor func onSetDefaultPressed() { delegate?.onboardingDidRequestSetDefault { [weak self] in +#if !APPSTORE + self?.state = .addToDock +#else self?.state = .startBrowsing Self.isOnboardingFinished = true self?.delegate?.onboardingHasFinished() +#endif } } @MainActor func onSetDefaultSkipped() { +#if !APPSTORE + state = .addToDock +#else + state = .startBrowsing + Self.isOnboardingFinished = true + delegate?.onboardingHasFinished() +#endif + } + + @MainActor + func onAddToDockPressed() { + PixelKit.fire(GeneralPixel.userAddedToDockDuringOnboarding, + includeAppVersionParameter: false) + addToDockPressed = true + delegate?.onboardingDidRequestAddToDock { [weak self] in + self?.state = .startBrowsing + Self.isOnboardingFinished = true + self?.delegate?.onboardingHasFinished() + } + } + + @MainActor + func onAddToDockSkipped() { + PixelKit.fire(GeneralPixel.userSkippedAddingToDockFromOnboarding, + includeAppVersionParameter: false) state = .startBrowsing Self.isOnboardingFinished = true delegate?.onboardingHasFinished() diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index a5495827fe..45d7bf74f7 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -24,6 +24,7 @@ import PixelKit protocol DefaultBrowserProvider { var bundleIdentifier: String { get } + var defaultBrowserURL: URL? { get } var isDefault: Bool { get } func presentDefaultBrowserPrompt() throws func openSystemPreferences() @@ -37,8 +38,12 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { let bundleIdentifier: String + var defaultBrowserURL: URL? { + return NSWorkspace.shared.urlForApplication(toOpen: URL(string: "http://")!) + } + var isDefault: Bool { - guard let defaultBrowserURL = NSWorkspace.shared.urlForApplication(toOpen: URL(string: "http://")!), + guard let defaultBrowserURL = defaultBrowserURL, let ddgBrowserURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return false diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 41e92b6886..956df10da6 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -21,6 +21,7 @@ import Combine import PreferencesViews import SwiftUI import SwiftUIExtensions +import PixelKit extension Preferences { @@ -31,11 +32,48 @@ extension Preferences { @ObservedObject var tabsModel: TabsPreferences @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false + @State private var isAddedToDock = false + var dockCustomizer: DockCustomizer var body: some View { PreferencePane(UserText.general) { - // SECTION 1: On Startup + // SECTION 1: Shortcuts +#if !APPSTORE + PreferencePaneSection(UserText.shortcuts, spacing: 4) { + PreferencePaneSubSection { + HStack { + if isAddedToDock || dockCustomizer.isAddedToDock { + HStack { + Image(.successCheckmark) + Text(UserText.isAddedToDock) + } + .transition(.opacity) + .padding(.trailing, 8) + } else { + HStack { + Image(.warning).foregroundColor(Color(.linkBlue)) + Text(UserText.isNotAddedToDock) + } + .padding(.trailing, 8) + Button(action: { + withAnimation { + PixelKit.fire(GeneralPixel.userAddedToDockFromSettings, + includeAppVersionParameter: false) + dockCustomizer.addToDock() + isAddedToDock = true + } + }) { + Text(UserText.addToDock) + .fixedSize(horizontal: true, vertical: false) + .multilineTextAlignment(.center) + } + } + } + } + } +#endif + // SECTION 2: On Startup PreferencePaneSection(UserText.onStartup) { PreferencePaneSubSection { @@ -61,7 +99,7 @@ extension Preferences { } } - // SECTION 2: Tabs + // SECTION 3: Tabs PreferencePaneSection(UserText.tabs) { PreferencePaneSubSection { ToggleMenuItem(UserText.preferNewTabsToWindows, isOn: $tabsModel.preferNewTabsToWindows) @@ -80,7 +118,7 @@ extension Preferences { } } - // SECTION 3: Home Page + // SECTION 4: Home Page PreferencePaneSection(UserText.homePage) { PreferencePaneSubSection { @@ -124,12 +162,12 @@ extension Preferences { CustomHomePageSheet(startupModel: startupModel, isSheetPresented: $showingCustomHomePageSheet) } - // SECTION 4: Search Settings + // SECTION 5: Search Settings PreferencePaneSection(UserText.privateSearch) { ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions).accessibilityIdentifier("PreferencesGeneralView.showAutocompleteSuggestions") } - // SECTION 5: Downloads + // SECTION 6: Downloads PreferencePaneSection(UserText.downloads) { PreferencePaneSubSection { ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 1f4fc03135..350eb84f96 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -89,7 +89,8 @@ enum Preferences { downloadsModel: DownloadsPreferences.shared, searchModel: SearchPreferences.shared, tabsModel: TabsPreferences.shared, - dataClearingModel: DataClearingPreferences.shared) + dataClearingModel: DataClearingPreferences.shared, + dockCustomizer: DockCustomizer()) case .sync: SyncView() case .appearance: diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 84219ba446..19e7c70108 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -62,6 +62,7 @@ final class StatisticsLoader { } PixelKit.fire(GeneralPixel.serp) self.fireDailyOsVersionCounterPixel() + self.fireDockPixel() } else if !self.statisticsStore.isAppRetentionFiredToday { self.refreshAppRetentionAtb(completion: completion) } else { @@ -231,4 +232,13 @@ final class StatisticsLoader { } } + private func fireDockPixel() { + DispatchQueue.global().asyncAfter(deadline: .now() + Double.random(in: 0.5...5)) { + if DockCustomizer().isAddedToDock { + PixelKit.fire(GeneralPixel.serpAddedToDock, + includeAppVersionParameter: false) + } + } + } + } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 0380cedd98..d5846e38a7 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -146,6 +146,16 @@ enum GeneralPixel: PixelKitEventV2 { case defaultRequestedFromSettings case defaultRequestedFromOnboarding + // Adding to the Dock + case addToDockOnboardingStepPresented + case userAddedToDockDuringOnboarding + case userSkippedAddingToDockFromOnboarding + case startBrowsingOnboardingStepPresented + case addToDockNewTabPageCardPresented + case userAddedToDockFromNewTabPageCard + case userAddedToDockFromSettings + case serpAddedToDock + case protectionToggledOffBreakageReport case toggleProtectionsDailyCount case toggleReportDoNotSend @@ -530,6 +540,15 @@ enum GeneralPixel: PixelKitEventV2 { case .defaultRequestedFromSettings: return "m_mac_default_requested_from_settings" case .defaultRequestedFromOnboarding: return "m_mac_default_requested_from_onboarding" + case .addToDockOnboardingStepPresented: return "m_mac_add_to_dock_onboarding_step_presented" + case .userAddedToDockDuringOnboarding: return "m_mac_user_added_to_dock_during_onboarding" + case .userSkippedAddingToDockFromOnboarding: return "m_mac_user_skipped_adding_to_dock_from_onboarding" + case .startBrowsingOnboardingStepPresented: return "m_mac_start_browsing_onboarding_step_presented" + case .addToDockNewTabPageCardPresented: return "m_mac_add_to_dock_new_tab_page_card_presented_u" + case .userAddedToDockFromNewTabPageCard: return "m_mac_user_added_to_dock_from_new_tab_page_card" + case .userAddedToDockFromSettings: return "m_mac_user_added_to_dock_from_settings" + case .serpAddedToDock: return "m_mac_serp_added_to_dock" + case .protectionToggledOffBreakageReport: return "m_mac_protection-toggled-off-breakage-report" case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" case .toggleReportDoNotSend: return "m_mac_toggle-report-do-not-send" diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index dde4a10a29..b785424d39 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -41,6 +41,7 @@ final class BrowserTabViewController: NSViewController { private let tabCollectionViewModel: TabCollectionViewModel private let bookmarkManager: BookmarkManager + private let dockCustomizer = DockCustomizer() private var tabViewModelCancellables = Set() private var activeUserDialogCancellable: Cancellable? @@ -1139,6 +1140,11 @@ extension BrowserTabViewController: OnboardingDelegate { } } + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) { + dockCustomizer.addToDock() + completion() + } + func onboardingHasFinished() { (view.window?.windowController as? MainWindowController)?.userInteraction(prevented: false) } diff --git a/UnitTests/App/DockCustomizerMock.swift b/UnitTests/App/DockCustomizerMock.swift new file mode 100644 index 0000000000..5e8812bad0 --- /dev/null +++ b/UnitTests/App/DockCustomizerMock.swift @@ -0,0 +1,39 @@ +// +// DockCustomizerMock.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 + +@testable import DuckDuckGo_Privacy_Browser + +class DockCustomizerMock: DockCustomization { + private var dockStatus: Bool = false + + var isAddedToDock: Bool { + return dockStatus + } + + @discardableResult + func addToDock() -> Bool { + if !dockStatus { + dockStatus = true + return true + } else { + return false + } + } +} diff --git a/UnitTests/App/DockPositionProviderTests.swift b/UnitTests/App/DockPositionProviderTests.swift new file mode 100644 index 0000000000..d7eb2895cb --- /dev/null +++ b/UnitTests/App/DockPositionProviderTests.swift @@ -0,0 +1,48 @@ +// +// DockPositionProviderTests.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 +@testable import DuckDuckGo_Privacy_Browser + +class DockPositionProviderTests: XCTestCase { + + var provider: DockPositionProvider! + var mockBrowserProvider: DefaultBrowserProviderMock! + + override func setUp() { + super.setUp() + mockBrowserProvider = DefaultBrowserProviderMock() + provider = DockPositionProvider(defaultBrowserProvider: mockBrowserProvider) + } + + override func tearDown() { + provider = nil + mockBrowserProvider = nil + super.tearDown() + } + + func testWhenNotDefaultBrowser_ThenIndexIsNextToDefault() { + mockBrowserProvider.isDefault = false + mockBrowserProvider.defaultBrowserURL = URL(string: "file:///Applications/Firefox.app/")! + let currentApps = [URL(string: "file:///Applications/Safari.app/")!, URL(string: "file:///Applications/Firefox.app/")!, URL(string: "file:///Applications/Arc.app/")!] + let index = provider.newDockIndex(from: currentApps) + + XCTAssertEqual(index, 2, "The new app should be placed next to default browser.") + } + +} diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index f7c2060ffc..9d49ab4073 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -69,6 +69,7 @@ final class ContinueSetUpModelTests: XCTestCase { var coookiePopupProtectionPreferences: MockCookiePopupProtectionPreferencesPersistor! var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! + var dockCustomizer: DockCustomization! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @MainActor override func setUp() { @@ -86,6 +87,7 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() + dockCustomizer = DockCustomizerMock() #if DBP let messaging = HomePageRemoteMessaging( @@ -103,6 +105,7 @@ final class ContinueSetUpModelTests: XCTestCase { vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -146,6 +149,7 @@ final class ContinueSetUpModelTests: XCTestCase { vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -226,6 +230,7 @@ final class ContinueSetUpModelTests: XCTestCase { @MainActor func testWhenAskedToPerformActionForImportPromptThrowsThenItOpensImportWindow() { let numberOfFeatures = HomePage.Models.FeatureType.allCases.count - 1 + vm.shouldShowAllFeatures = true XCTAssertEqual(vm.visibleFeaturesMatrix.flatMap { $0 }.count, numberOfFeatures) @@ -353,10 +358,12 @@ final class ContinueSetUpModelTests: XCTestCase { emailStorage.isEmailProtectionEnabled = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true capturingDataImportProvider.didImport = true + dockCustomizer.addToDock() userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -385,6 +392,11 @@ final class ContinueSetUpModelTests: XCTestCase { vm.removeItem(for: .emailProtection) XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.emailProtection)) +#if !APPSTORE + vm.removeItem(for: .dock) + XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.dock)) +#endif + let vm2 = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) XCTAssertTrue(vm2.visibleFeaturesMatrix.flatMap { $0 }.isEmpty) } @@ -461,6 +473,7 @@ final class ContinueSetUpModelTests: XCTestCase { userDefaults.set(true, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) let vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -512,6 +525,27 @@ final class ContinueSetUpModelTests: XCTestCase { #endif } + @MainActor func test_WhenUserDoesntHaveApplicationInTheDock_ThenAddToDockCardIsDisplayed() { +#if !APPSTORE + let dockCustomizer = DockCustomizerMock() + + let vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, dockCustomizer: dockCustomizer) + vm.shouldShowAllFeatures = true + + XCTAssert(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.dock)) +#endif + } + + @MainActor func test_WhenUserHasApplicationInTheDock_ThenAddToDockCardIsNotDisplayed() { + let dockCustomizer = DockCustomizerMock() + dockCustomizer.addToDock() + + let vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, dockCustomizer: dockCustomizer) + vm.shouldShowAllFeatures = true + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.dock)) + } + } extension HomePage.Models.ContinueSetUpModel { @@ -523,7 +557,8 @@ extension HomePage.Models.ContinueSetUpModel { privacyConfig: MockPrivacyConfiguration = MockPrivacyConfiguration(), appGroupUserDefaults: UserDefaults, permanentSurveyManager: MockPermanentSurveyManager = MockPermanentSurveyManager(), - randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator() + randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator(), + dockCustomizer: DockCustomization = DockCustomizerMock() ) -> HomePage.Models.ContinueSetUpModel { privacyConfig.featureSettings = [ "networkProtection": "disabled" @@ -547,6 +582,7 @@ extension HomePage.Models.ContinueSetUpModel { return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: defaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: dataImportProvider, tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, diff --git a/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift b/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift index 9931c9de94..dc544f2e5b 100644 --- a/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift +++ b/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift @@ -21,6 +21,8 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser class CapturingDefaultBrowserProvider: DefaultBrowserProvider { + var defaultBrowserURL: URL? + var presentDefaultBrowserPromptCalled = false var openSystemPreferencesCalled = false var throwError = false diff --git a/UnitTests/Onboarding/OnboardingTests.swift b/UnitTests/Onboarding/OnboardingTests.swift index f0366d6498..a123262061 100644 --- a/UnitTests/Onboarding/OnboardingTests.swift +++ b/UnitTests/Onboarding/OnboardingTests.swift @@ -43,13 +43,23 @@ class OnboardingTests: XCTestCase { assertStateChange(model, .startFlow, .welcome, model.onSplashFinished) assertStateChange(model, .welcome, .importData, model.onStartPressed) assertStateChange(model, .importData, .setDefault, model.onImportPressed) +#if APPSTORE assertStateChange(model, .setDefault, .startBrowsing, model.onSetDefaultPressed) +#else + assertStateChange(model, .setDefault, .addToDock, model.onSetDefaultPressed) + assertStateChange(model, .addToDock, .startBrowsing, model.onAddToDockPressed) +#endif model.state = .importData assertStateChange(model, .importData, .setDefault, model.onImportSkipped) model.state = .setDefault +#if APPSTORE assertStateChange(model, .setDefault, .startBrowsing, model.onSetDefaultSkipped) +#else + assertStateChange(model, .setDefault, .addToDock, model.onSetDefaultSkipped) + assertStateChange(model, .addToDock, .startBrowsing, model.onAddToDockSkipped) +#endif } func testWhenImportPressedDelegateIsCalled() { @@ -79,12 +89,37 @@ class OnboardingTests: XCTestCase { model.onSetDefaultSkipped() XCTAssertEqual(0, delegate.didRequestImportDataCalled) XCTAssertEqual(0, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) +#if APPSTORE XCTAssertEqual(1, delegate.hasFinishedCalled) +#else + XCTAssertEqual(0, delegate.hasFinishedCalled) +#endif model.onSetDefaultPressed() XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) +#if APPSTORE XCTAssertEqual(2, delegate.hasFinishedCalled) +#else + XCTAssertEqual(0, delegate.hasFinishedCalled) +#endif + +#if !APPSTORE + model.onAddToDockSkipped() + XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) + XCTAssertEqual(1, delegate.hasFinishedCalled) +#endif + +#if !APPSTORE + model.onAddToDockPressed() + XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(2, delegate.hasFinishedCalled) +#endif XCTAssertTrue(onboardingFinished) } @@ -114,6 +149,7 @@ class OnboardingTests: XCTestCase { final class MockOnboardingDelegate: NSObject, OnboardingDelegate { var didRequestImportDataCalled = 0 var didRequestSetDefaultCalled = 0 + var didRequestAddToDockCalled = 0 var hasFinishedCalled = 0 func onboardingDidRequestImportData(completion: @escaping () -> Void) { @@ -126,6 +162,11 @@ final class MockOnboardingDelegate: NSObject, OnboardingDelegate { completion() } + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) { + didRequestAddToDockCalled += 1 + completion() + } + func onboardingHasFinished() { hasFinishedCalled += 1 } diff --git a/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift b/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift index c7e2aae416..d8106d7574 100644 --- a/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift +++ b/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift @@ -20,11 +20,13 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser final class DefaultBrowserProviderMock: DefaultBrowserProvider { + enum MockError: Error { case generic } var bundleIdentifier: String = "com.duckduckgo.DefaultBrowserPreferencesTests" + var defaultBrowserURL: URL? var isDefault: Bool = false var _presentDefaultBrowserPrompt: () throws -> Void = {} var _openSystemPreferences: () -> Void = {}