From 8990d1ad3a39c4d975f3eefb41f67c42e46a503e Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 3 Jun 2024 06:26:44 -0700 Subject: [PATCH 01/35] Update BSK for iOS RMF changes (#2831) Task/Issue URL: https://app.asana.com/0/414235014887631/1207409447873013/f Tech Design URL: CC: Description: This PR updates macOS with iOS BSK changes. Nothing about these changes will affect macOS. --- 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 2e4bcb2720a..678168e0eb0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12993,7 +12993,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 149.0.0; + version = 150.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 8da6589602e..8d596f9f944 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" : "cd7850dd115f4c896095f410c1049fc32bdf7b16", - "version" : "149.0.0" + "revision" : "03e6b719671c5baaa2afa474d447b707bf595820", + "version" : "150.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 29b4a480f9c..5e03b7e95cb 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: "149.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "150.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 25b0391e25f..bf3e2340cf6 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: "149.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "150.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 5cf819c1193..3e38b79343a 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: "149.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "150.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 31ac5d3fb593735cdb6e9e4b7329211f04a3ad72 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Mon, 3 Jun 2024 09:12:20 -0500 Subject: [PATCH 02/35] Add optional "multiple" property to PageElement (#2827) Task/Issue URL: https://app.asana.com/0/1206873150423133/1207457690535593/f Tech Design URL: CC: **Description**: See https://app.asana.com/0/1206873150423133/1206998874293254 for more context, allows multiple items to be clicked during a single "click" action. --- .../Sources/DataBrokerProtection/Model/PageElement.swift | 1 + .../DataBrokerOperationActionTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/PageElement.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/PageElement.swift index 76b5c5dbf06..ee049d39084 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/PageElement.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/PageElement.swift @@ -21,6 +21,7 @@ struct PageElement: Codable, Sendable { let type: String let selector: String let parent: ParentElement? + let multiple: Bool? } struct ProfileMatch: Codable, Sendable { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index 4f0ea9f97a5..e04983d865e 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -128,7 +128,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenActionNeedsEmail_thenExtractedProfileEmailIsSet() async { - let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "#test", elements: [.init(type: "email", selector: "#email", parent: nil)], dataSource: nil) + let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "#test", elements: [.init(type: "email", selector: "#email", parent: nil, multiple: nil)], dataSource: nil) let step = Step(type: .optOut, actions: [fillFormAction]) let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), @@ -152,7 +152,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenGetEmailServiceFails_thenOperationThrows() async { - let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "#test", elements: [.init(type: "email", selector: "#email", parent: nil)], dataSource: nil) + let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "#test", elements: [.init(type: "email", selector: "#email", parent: nil, multiple: nil)], dataSource: nil) let step = Step(type: .optOut, actions: [fillFormAction]) let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), From 447a20d6848811626f42dbc5f8ebdbbeb8c1a664 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Tue, 4 Jun 2024 10:09:05 +0100 Subject: [PATCH 03/35] DuckPlayer PiP settings (#2830) Task/Issue URL: https://app.asana.com/0/1204167627774280/1207452548013008/f **Description**: Adds settings for C-S-S to enable/disable PiP ps. This adds no changes to the user experience yet, the PiP button will be enabled at a later time via C-S-S --- .../WKWebViewConfigurationExtensions.swift | 15 +++++++- DuckDuckGo/YoutubePlayer/DuckPlayer.swift | 37 +++++++++++++++++++ .../YoutubePlayerUserScript.swift | 3 ++ UnitTests/YoutubePlayer/DuckPlayerTests.swift | 14 +++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index afb6574717b..8fbc018684c 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -23,6 +23,15 @@ import WebKit extension WKWebViewConfiguration { + var allowsPictureInPictureMediaPlayback: Bool { + get { + return preferences.value(forKey: "allowsPictureInPictureMediaPlayback") as? Bool ?? false + } + set { + preferences.setValue(newValue, forKey: "allowsPictureInPictureMediaPlayback") + } + } + @MainActor func applyStandardConfiguration(contentBlocking: some ContentBlockingProtocol, burnerMode: BurnerMode) { if case .burner(let websiteDataStore) = burnerMode { @@ -34,7 +43,11 @@ extension WKWebViewConfiguration { } else { preferences.setValue(true, forKey: "fullScreenEnabled") } - preferences.setValue(true, forKey: "allowsPictureInPictureMediaPlayback") + +#if !APPSTORE + allowsPictureInPictureMediaPlayback = true +#endif + preferences.setValue(true, forKey: "developerExtrasEnabled") preferences.setValue(false, forKey: "backspaceKeyNavigationEnabled") preferences.javaScriptCanOpenWindowsAutomatically = true diff --git a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift index 404ff6f265e..b6d24743d3f 100644 --- a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift +++ b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift @@ -53,6 +53,25 @@ enum DuckPlayerMode: Equatable, Codable { } /// Values that the Frontend can use to determine the current state. +struct InitialSetupSettings: Codable { + struct PlayerSettings: Codable { + let pip: PIP + } + + struct PIP: Codable { + let status: Status + } + + enum Status: String, Codable { + case enabled + case disabled + } + + let userValues: UserValues + let settings: PlayerSettings +} + +/// Values that the Frontend can use to determine user settings public struct UserValues: Codable { enum CodingKeys: String, CodingKey { case duckPlayerMode = "privatePlayerMode" @@ -168,6 +187,12 @@ final class DuckPlayer { encodeUserValues() } + public func initialSetup(with webView: WKWebView?) -> (_ params: Any, _ message: UserScriptMessage) async -> Encodable? { + return { _, _ in + return await self.encodedSettings(with: webView) + } + } + private func encodeUserValues() -> UserValues { UserValues( duckPlayerMode: self.preferences.duckPlayerMode, @@ -175,6 +200,17 @@ final class DuckPlayer { ) } + @MainActor + private func encodedSettings(with webView: WKWebView?) async -> InitialSetupSettings { + let isPiPEnabled = webView?.configuration.allowsPictureInPictureMediaPlayback == true + let pip = InitialSetupSettings.PIP(status: isPiPEnabled ? .enabled : .disabled) + + let playerSettings = InitialSetupSettings.PlayerSettings(pip: pip) + let userValues = encodeUserValues() + + return InitialSetupSettings(userValues: userValues, settings: playerSettings) + } + // MARK: - Private private static let websiteTitlePrefix = "\(commonName) - " @@ -253,6 +289,7 @@ extension DuckPlayer { } return actualTitle.dropping(prefix: Self.websiteTitlePrefix) } + } #if DEBUG diff --git a/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift b/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift index 0d4a7dd9b49..2a0af3883ca 100644 --- a/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift @@ -42,6 +42,7 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { enum MessageNames: String, CaseIterable { case setUserValues case getUserValues + case initialSetup } func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { @@ -53,6 +54,8 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { return DuckPlayer.shared.handleGetUserValues case .setUserValues: return DuckPlayer.shared.handleSetUserValuesMessage(from: .duckPlayer) + case .initialSetup: + return DuckPlayer.shared.initialSetup(with: webView) default: assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)") return nil diff --git a/UnitTests/YoutubePlayer/DuckPlayerTests.swift b/UnitTests/YoutubePlayer/DuckPlayerTests.swift index dee856c6ee8..1cc4860dc5b 100644 --- a/UnitTests/YoutubePlayer/DuckPlayerTests.swift +++ b/UnitTests/YoutubePlayer/DuckPlayerTests.swift @@ -19,6 +19,7 @@ import BrowserServicesKit import Combine import XCTest +import Common @testable import DuckDuckGo_Privacy_Browser @@ -89,6 +90,19 @@ final class DuckPlayerTests: XCTestCase { XCTAssertNil(duckPlayer.title(for: feedItem)) } + @MainActor + func testEnabledPiPFlag() async { + let configuration = WKWebViewConfiguration() + + configuration.applyStandardConfiguration(contentBlocking: ContentBlockingMock(), + burnerMode: .regular) +#if APPSTORE + XCTAssertFalse(configuration.allowsPictureInPictureMediaPlayback) +#else + XCTAssertTrue(configuration.allowsPictureInPictureMediaPlayback) +#endif + } + func testThatTitleForRecentlyVisitedPageIsNotAdjustedForNonDuckPlayerFeedItems() { let feedItem = HomePage.Models.RecentlyVisitedPageModel( actualTitle: "Duck Player - A sample video title", From a390dfd0692d178627ac6463f46c1472c8b9888a Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:34:35 -0400 Subject: [PATCH 04/35] Surface specific XPC & login item errors (#2773) Task/Issue URL: https://app.asana.com/0/72649045549333/1207013105069620/f Tech Design URL: CC: BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/819 **Description**: **Steps to test this PR**: 1. For the UI, go to Debug > VPN > Simulate known failure and check the warning banner in the VPN popover 2. To check if XPC errors are surfaced, go to `NetworkProtectionIPCTunnelController.start()`, and add a simulated error like `handleFailure(NSError(domain: "SMAppServiceErrorDomain", code: 1))` inside `handleFailure(_:)` 3. To check if login item version mismatch is handled, go to `TunnelControllerIPCService.register(version:bundlePath:completion)` and change the `DefaultIPCMetadataCollector.version != version` to `DefaultIPCMetadataCollector.version == version` 4. To check if ClientError code 5 is handled, block access to NetP endpoint (at router level or using Proxyman), then try to start the VPN. 5. Use Debug > VPN > Log metadata to console to check if lastKnownFailureDescription is recorded --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../UserText+NetworkProtection.swift | 2 +- .../MainWindow/MainViewController.swift | 7 ++- .../View/NetPPopoverManagerMock.swift | 6 ++ .../NetworkProtectionDebugMenu.swift | 7 +++ ...etworkProtectionNavBarPopoverManager.swift | 3 +- .../NetworkProtectionTunnelController.swift | 3 + .../TunnelControllerProvider.swift | 5 +- ...NetworkProtectionIPCTunnelController.swift | 8 ++- .../VPNMetadataCollector.swift | 8 ++- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 3 +- .../TunnelControllerIPCService.swift | 55 +++++++++++++++++- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../IPCMetadataCollector.swift | 44 +++++++++++++++ .../KnownFailureObserverThroughIPC.swift | 39 +++++++++++++ .../TunnelControllerIPCClient.swift | 56 +++++++++++++------ .../TunnelControllerIPCServer.swift | 30 ++++++++-- .../UserText+NetworkProtectionUI.swift | 4 ++ .../NetworkProtectionStatusView.swift | 32 ++--------- .../NetworkProtectionStatusViewModel.swift | 50 +++++++++++++++++ .../Views/WarningView/WarningView.swift | 54 ++++++++++++++++++ .../Views/WarningView/WarningViewModel.swift | 35 ++++++++++++ .../TunnelControllerViewModelTests.swift | 8 ++- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../SwiftUIExtensions/ButtonStyles.swift | 9 ++- .../VPNFeedbackFormViewModelTests.swift | 1 + 28 files changed, 412 insertions(+), 69 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/IPCMetadataCollector.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/KnownFailureObserverThroughIPC.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningView.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningViewModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 678168e0eb0..537548fe8ae 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12993,7 +12993,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 150.0.0; + version = 150.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 8d596f9f944..6fa7ef0118c 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" : "03e6b719671c5baaa2afa474d447b707bf595820", - "version" : "150.0.0" + "revision" : "79fe0c99e43c6c1bf2c0a4d397368033fd37eae9", + "version" : "150.1.0" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index d908735e237..27cb8280254 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -65,7 +65,7 @@ extension UserText { } // "network.protection.system.extension.unknown.activation.error" - Message shown to users when they try to enable NetP and there is an unexpected activation error. - static let networkProtectionUnknownActivationError = "There as an unexpected error. Please try again." + static let networkProtectionUnknownActivationError = "There was an unexpected error. Please try again." // "network.protection.system.extension.please.reboot" - Message shown to users when they try to enable NetP and they need to reboot the computer to complete the installation static let networkProtectionPleaseReboot = "VPN update available. Restart your Mac to reconnect." } diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 8d4c5852a92..4624b91d14c 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -71,7 +71,9 @@ final class MainViewController: NSViewController { #endif let ipcClient = TunnelControllerIPCClient() - ipcClient.register() + ipcClient.register { error in + NetworkProtectionKnownFailureStore().lastKnownFailure = KnownFailure(error) + } let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) return NetworkProtectionNavBarPopoverManager( @@ -97,7 +99,8 @@ final class MainViewController: NSViewController { connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: connectivityIssuesObserver, controllerErrorMessageObserver: controllerErrorMessageObserver, - dataVolumeObserver: ipcClient.ipcDataVolumeObserver + dataVolumeObserver: ipcClient.ipcDataVolumeObserver, + knownFailureObserver: KnownFailureObserverThroughDistributedNotifications() ) }() diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index 033f7950051..3b0d6efd9a5 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -74,6 +74,12 @@ final class IPCClientMock: NetworkProtectionIPCClient { } var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver = DataVolumeObserverMock() + final class KnownFailureObserverMock: NetworkProtection.KnownFailureObserver { + var publisher: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + var recentValue: KnownFailure? + } + var ipcKnownFailureObserver: any NetworkProtection.KnownFailureObserver = KnownFailureObserverMock() + func start(completion: @escaping (Error?) -> Void) { completion(nil) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 23fb71b7abd..418ba71d9f7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -405,6 +405,13 @@ final class NetworkProtectionDebugMenu: NSMenu { } + func menuItem(title: String, action: Selector, representedObject: Any?) -> NSMenuItem { + let menuItem = NSMenuItem(title: title, action: action, keyEquivalent: "") + menuItem.target = self + menuItem.representedObject = representedObject + return menuItem + } + // MARK: - Menu State Update override func update() { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 82cd98ab95a..9b4946913e4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -69,7 +69,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), - dataVolumeObserver: ipcClient.ipcDataVolumeObserver + dataVolumeObserver: ipcClient.ipcDataVolumeObserver, + knownFailureObserver: KnownFailureObserverThroughDistributedNotifications() ) let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 8610b020c75..62e32dc0a65 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -68,6 +68,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() + private let knownFailureStore = NetworkProtectionKnownFailureStore() + // MARK: - VPN Tunnel & Configuration /// Auth token store @@ -597,6 +599,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } catch { VPNOperationErrorRecorder().recordControllerStartFailure(error) + knownFailureStore.lastKnownFailure = KnownFailure(error) if case StartError.cancelled = error { PixelKit.fire( diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift index 1d0c8efa53e..1b50a1156cd 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift @@ -17,6 +17,7 @@ // import Foundation +import NetworkProtection import NetworkProtectionIPC final class TunnelControllerProvider { @@ -26,7 +27,9 @@ final class TunnelControllerProvider { private init() { let ipcClient = TunnelControllerIPCClient() - ipcClient.register() + ipcClient.register { error in + NetworkProtectionKnownFailureStore().lastKnownFailure = KnownFailure(error) + } tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index 028b3ba4cc6..9a883ab5881 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -52,18 +52,21 @@ final class NetworkProtectionIPCTunnelController { private let ipcClient: NetworkProtectionIPCClient private let pixelKit: PixelFiring? private let errorRecorder: VPNOperationErrorRecorder + private let knownFailureStore: NetworkProtectionKnownFailureStore init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, pixelKit: PixelFiring? = PixelKit.shared, - errorRecorder: VPNOperationErrorRecorder = VPNOperationErrorRecorder()) { + errorRecorder: VPNOperationErrorRecorder = VPNOperationErrorRecorder(), + knownFailureStore: NetworkProtectionKnownFailureStore = NetworkProtectionKnownFailureStore()) { self.featureVisibility = featureVisibility self.loginItemsManager = loginItemsManager self.ipcClient = ipcClient self.pixelKit = pixelKit self.errorRecorder = errorRecorder + self.knownFailureStore = knownFailureStore } // MARK: - Login Items Manager @@ -91,6 +94,7 @@ extension NetworkProtectionIPCTunnelController: TunnelController { pixelKit?.fire(StartAttempt.begin) func handleFailure(_ error: Error) { + knownFailureStore.lastKnownFailure = KnownFailure(error) errorRecorder.recordIPCStartFailure(error) log(error) pixelKit?.fire(StartAttempt.failure(error), frequency: .dailyAndCount) @@ -99,6 +103,8 @@ extension NetworkProtectionIPCTunnelController: TunnelController { do { try await enableLoginItems() + knownFailureStore.reset() + ipcClient.start { [pixelKit] error in if let error { handleFailure(error) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index e69c0c4a418..51f64b8912a 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -52,6 +52,7 @@ struct VPNMetadata: Encodable { let connectionState: String let lastStartErrorDescription: String let lastTunnelErrorDescription: String + let lastKnownFailureDescription: String let connectedServer: String let connectedServerIP: String } @@ -127,7 +128,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { init(defaults: UserDefaults = .netP, accountManager: AccountManaging) { let ipcClient = TunnelControllerIPCClient() - ipcClient.register() + ipcClient.register { _ in } self.accountManager = accountManager self.ipcClient = ipcClient self.defaults = defaults @@ -138,7 +139,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { connectionErrorObserver: ipcClient.connectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), - dataVolumeObserver: ipcClient.dataVolumeObserver + dataVolumeObserver: ipcClient.dataVolumeObserver, + knownFailureObserver: KnownFailureObserverThroughDistributedNotifications() ) // Force refresh just in case. A refresh is requested when the IPC client is created, but distributed notifications don't guarantee delivery @@ -266,12 +268,14 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { let connectionState = String(describing: statusReporter.statusObserver.recentValue) let lastTunnelErrorDescription = await errorHistory.lastTunnelErrorDescription + let lastKnownFailureDescription = NetworkProtectionKnownFailureStore().lastKnownFailure?.description ?? "none" let connectedServer = statusReporter.serverInfoObserver.recentValue.serverLocation?.serverLocation ?? "none" let connectedServerIP = statusReporter.serverInfoObserver.recentValue.serverAddress ?? "none" return .init(onboardingState: onboardingState, connectionState: connectionState, lastStartErrorDescription: errorHistory.lastStartErrorDescription, lastTunnelErrorDescription: lastTunnelErrorDescription, + lastKnownFailureDescription: lastKnownFailureDescription, connectedServer: connectedServer, connectedServerIP: connectedServerIP) } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index de78d86b57d..406272e7330 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -266,7 +266,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { connectionErrorObserver: errorObserver, connectivityIssuesObserver: DisabledConnectivityIssueObserver(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), - dataVolumeObserver: dataVolumeObserver + dataVolumeObserver: dataVolumeObserver, + knownFailureObserver: KnownFailureObserverThroughDistributedNotifications() ) }() diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index 2b23d6c6584..c79aedc9861 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -37,6 +37,16 @@ final class TunnelControllerIPCService { private var cancellables = Set() private let defaults: UserDefaults + enum IPCError: SilentErrorConvertible { + case versionMismatched + + var asSilentError: KnownFailure.SilentError? { + switch self { + case .versionMismatched: return .loginItemVersionMismatched + } + } + } + init(tunnelController: NetworkProtectionTunnelController, uninstaller: VPNUninstalling, networkExtensionController: NetworkExtensionController, @@ -53,6 +63,7 @@ final class TunnelControllerIPCService { subscribeToErrorChanges() subscribeToStatusUpdates() subscribeToServerChanges() + subscribeToKnownFailureUpdates() subscribeToDataVolumeUpdates() server.serverDelegate = self @@ -89,6 +100,15 @@ final class TunnelControllerIPCService { .store(in: &cancellables) } + private func subscribeToKnownFailureUpdates() { + statusReporter.knownFailureObserver.publisher + .subscribe(on: DispatchQueue.main) + .sink { [weak self] failure in + self?.server.knownFailureUpdated(failure) + } + .store(in: &cancellables) + } + private func subscribeToDataVolumeUpdates() { statusReporter.dataVolumeObserver.publisher .subscribe(on: DispatchQueue.main) @@ -103,9 +123,20 @@ final class TunnelControllerIPCService { extension TunnelControllerIPCService: IPCServerInterface { - func register() { + func register(completion: @escaping (Error?) -> Void) { + register(version: version, bundlePath: bundlePath, completion: completion) + } + + func register(version: String, bundlePath: String, completion: @escaping (Error?) -> Void) { server.serverInfoChanged(statusReporter.serverInfoObserver.recentValue) server.statusChanged(statusReporter.statusObserver.recentValue) + if self.version != version { + let error = TunnelControllerIPCService.IPCError.versionMismatched + NetworkProtectionKnownFailureStore().lastKnownFailure = KnownFailure(error) + completion(error) + } else { + completion(nil) + } } func start(completion: @escaping (Error?) -> Void) { @@ -169,3 +200,25 @@ extension TunnelControllerIPCService: IPCServerInterface { } } } + +// MARK: - Error Handling + +extension TunnelControllerIPCService.IPCError: LocalizedError, CustomNSError { + var errorDescription: String? { + switch self { + case .versionMismatched: return "Login item version mismatched" + } + } + + var errorCode: Int { + switch self { + case .versionMismatched: return 0 + } + } + + var errorUserInfo: [String: Any] { + switch self { + case .versionMismatched: return [:] + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5e03b7e95cb..745a4c468dc 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: "150.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "150.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index bf3e2340cf6..12be7911f0f 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: "150.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "150.1.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/IPCMetadataCollector.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/IPCMetadataCollector.swift new file mode 100644 index 00000000000..c1b51293703 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/IPCMetadataCollector.swift @@ -0,0 +1,44 @@ +// +// IPCMetadataCollector.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 IPCMetadataCollector { + static var version: String { get } + static var bundlePath: String { get } +} + +final public class DefaultIPCMetadataCollector: IPCMetadataCollector { + public static var version: String { + shortVersion + "/" + buildNumber + } + + public static var bundlePath: String { + Bundle.main.bundlePath + } + + // swiftlint:disable force_cast + private static var shortVersion: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + } + + private static var buildNumber: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String + } + // swiftlint:enable force_cast +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/KnownFailureObserverThroughIPC.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/KnownFailureObserverThroughIPC.swift new file mode 100644 index 00000000000..ab95ad66fe2 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/KnownFailureObserverThroughIPC.swift @@ -0,0 +1,39 @@ +// +// KnownFailureObserverThroughIPC.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import NetworkProtection + +public final class KnownFailureObserverThroughIPC: KnownFailureObserver { + private let subject = CurrentValueSubject(nil) + + // MARK: - KnownFailureObserver + + public lazy var publisher = subject.eraseToAnyPublisher() + + public var recentValue: KnownFailure? { + subject.value + } + + // MARK: - Publishing Updates + + func publish(_ error: KnownFailure?) { + subject.send(error) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index 58e060f78a8..84a68750d0e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -27,6 +27,7 @@ public protocol IPCClientInterface: AnyObject { func serverInfoChanged(_ serverInfo: NetworkProtectionStatusServerInfo) func statusChanged(_ status: ConnectionStatus) func dataVolumeUpdated(_ dataVolume: DataVolume) + func knownFailureUpdated(_ failure: KnownFailure?) } /// This is the XPC interface with parameters that can be packed properly @@ -36,6 +37,7 @@ protocol XPCClientInterface { func serverInfoChanged(payload: Data) func statusChanged(payload: Data) func dataVolumeUpdated(payload: Data) + func knownFailureUpdated(failure: KnownFailure?) } public final class TunnelControllerIPCClient { @@ -50,6 +52,7 @@ public final class TunnelControllerIPCClient { public var connectionErrorObserver = ConnectionErrorObserverThroughIPC() public var connectionStatusObserver = ConnectionStatusObserverThroughIPC() public var dataVolumeObserver = DataVolumeObserverThroughIPC() + public var knownFailureObserver = KnownFailureObserverThroughIPC() /// The delegate. /// @@ -69,7 +72,8 @@ public final class TunnelControllerIPCClient { serverInfoObserver: self.serverInfoObserver, connectionErrorObserver: self.connectionErrorObserver, connectionStatusObserver: self.connectionStatusObserver, - dataVolumeObserver: self.dataVolumeObserver + dataVolumeObserver: self.dataVolumeObserver, + knownFailureObserver: self.knownFailureObserver ) xpc = XPCClient( @@ -87,11 +91,11 @@ public final class TunnelControllerIPCClient { // By calling register we make sure that XPC will connect as soon as it // becomes available again, as requests are queued. This helps ensure // that the client app will always be connected to XPC. - self.register() + self.register { _ in } } } - self.register() + self.register { _ in } } } @@ -102,17 +106,20 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { let connectionErrorObserver: ConnectionErrorObserverThroughIPC let connectionStatusObserver: ConnectionStatusObserverThroughIPC let dataVolumeObserver: DataVolumeObserverThroughIPC + let knownFailureObserver: KnownFailureObserverThroughIPC init(clientDelegate: IPCClientInterface?, serverInfoObserver: ConnectionServerInfoObserverThroughIPC, connectionErrorObserver: ConnectionErrorObserverThroughIPC, connectionStatusObserver: ConnectionStatusObserverThroughIPC, - dataVolumeObserver: DataVolumeObserverThroughIPC) { + dataVolumeObserver: DataVolumeObserverThroughIPC, + knownFailureObserver: KnownFailureObserverThroughIPC) { self.clientDelegate = clientDelegate self.serverInfoObserver = serverInfoObserver self.connectionErrorObserver = connectionErrorObserver self.connectionStatusObserver = connectionStatusObserver self.dataVolumeObserver = dataVolumeObserver + self.knownFailureObserver = knownFailureObserver } func errorChanged(error: String?) { @@ -146,30 +153,43 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { dataVolumeObserver.publish(dataVolume) clientDelegate?.dataVolumeUpdated(dataVolume) } + + func knownFailureUpdated(failure: KnownFailure?) { + knownFailureObserver.publish(failure) + clientDelegate?.knownFailureUpdated(failure) + } } // MARK: - Outgoing communication to the server extension TunnelControllerIPCClient: IPCServerInterface { - public func register() { + public func register(completion: @escaping (Error?) -> Void) { + register(version: version, bundlePath: bundlePath, completion: self.onComplete(completion)) + } + + public func register(version: String, bundlePath: String, completion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.register() - }, xpcReplyErrorHandler: { _ in - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! - }) + server.register(version: version, bundlePath: bundlePath, completion: self.onComplete(completion)) + }, xpcReplyErrorHandler: self.onComplete(completion)) + } + + public func onComplete(_ completion: @escaping (Error?) -> Void) -> (Error?) -> Void { + { [weak self] error in + self?.xpcDelegate.knownFailureUpdated(failure: .init(error)) + completion(error) + } } public func start(completion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.start(completion: completion) - }, xpcReplyErrorHandler: completion) + server.start(completion: self.onComplete(completion)) + }, xpcReplyErrorHandler: self.onComplete(completion)) } public func stop(completion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.stop(completion: completion) - }, xpcReplyErrorHandler: completion) + server.stop(completion: self.onComplete(completion)) + }, xpcReplyErrorHandler: self.onComplete(completion)) } public func fetchLastError(completion: @escaping (Error?) -> Void) { @@ -185,16 +205,16 @@ extension TunnelControllerIPCClient: IPCServerInterface { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in xpc.execute(call: { server in - server.command(payload) { error in + server.command(payload) { [weak self] error in if let error { + self?.xpcDelegate.knownFailureUpdated(failure: .init(error)) continuation.resume(throwing: error) } else { continuation.resume() } } - }, xpcReplyErrorHandler: { error in - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! + }, xpcReplyErrorHandler: { [weak self] error in + self?.xpcDelegate.knownFailureUpdated(failure: .init(error)) continuation.resume(throwing: error) }) } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index e1103a3592c..576e270415d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -23,11 +23,16 @@ import XPCHelper /// This protocol describes the server-side IPC interface for controlling the tunnel /// public protocol IPCServerInterface: AnyObject { + var version: String { get } + var bundlePath: String { get } + /// Registers a connection with the server. /// /// This is the point where the server will start sending status updates to the client. /// - func register() + func register(completion: @escaping (Error?) -> Void) + + func register(version: String, bundlePath: String, completion: @escaping (Error?) -> Void) /// Start the VPN tunnel. /// @@ -54,6 +59,11 @@ public protocol IPCServerInterface: AnyObject { func command(_ command: VPNCommand) async throws } +public extension IPCServerInterface { + var version: String { DefaultIPCMetadataCollector.version } + var bundlePath: String { DefaultIPCMetadataCollector.bundlePath } +} + /// This protocol describes the server-side XPC interface. /// /// The object that implements this interface takes care of unpacking any encoded data and forwarding @@ -65,7 +75,9 @@ protocol XPCServerInterface { /// /// This is the point where the server will start sending status updates to the client. /// - func register() + func register(completion: @escaping (Error?) -> Void) + + func register(version: String, bundlePath: String, completion: @escaping (Error?) -> Void) /// Start the VPN tunnel. /// @@ -165,13 +177,23 @@ extension TunnelControllerIPCServer: IPCClientInterface { client.dataVolumeUpdated(payload: payload) } } + + public func knownFailureUpdated(_ failure: KnownFailure?) { + xpc.forEachClient { client in + client.knownFailureUpdated(failure: failure) + } + } } // MARK: - Incoming communication from a client extension TunnelControllerIPCServer: XPCServerInterface { - func register() { - serverDelegate?.register() + func register(completion: @escaping (Error?) -> Void) { + serverDelegate?.register(completion: completion) + } + + func register(version: String, bundlePath: String, completion: @escaping (Error?) -> Void) { + serverDelegate?.register(version: version, bundlePath: bundlePath, completion: completion) } func start(completion: @escaping (Error?) -> Void) { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 0236302a96f..7332cefe866 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -33,6 +33,10 @@ final class UserText { static let vpnLocationConnected = NSLocalizedString("network.protection.vpn.location.connected", value: "Connected Location", comment: "Description of the location type in the VPN status view") static let vpnLocationSelected = NSLocalizedString("network.protection.vpn.location.selected", value: "Selected Location", comment: "Description of the location type in the VPN status view") static let vpnDataVolume = NSLocalizedString("network.protection.vpn.data-volume", value: "Data Volume", comment: "Title for the data volume section in the VPN status view") + static let vpnShareFeedback = NSLocalizedString("network.protection.vpn.share-feedback", value: "Share VPN Feedback…", comment: "Action button title for the Share VPN feedback option") + static let vpnOperationNotPermittedMessage = NSLocalizedString("network.protection.vpn.failure.operation-not-permitted", value: "Unable to connect due to an unexpected error. Restarting your Mac can usually fix the issue.", comment: "Error message for the Operation not permitted error") + static let vpnLoginItemVersionMismatchedMessage = NSLocalizedString("network.protection.vpn.failure.login-item-version-mismatched", value: "Unable to connect due to versioning conflict. If you have multiple versions of the browser installed, remove all but the most recent version of DuckDuckGo and restart your Mac.", comment: "Error message for the Login item version mismatched error") + static let vpnRegisteredServerFetchingFailedMessage = NSLocalizedString("network.protection.vpn.failure.registered-server-fetching-failed", value: "Unable to connect. Double check your internet connection. Make sure other software or services aren't blocking DuckDuckGo VPN servers.", comment: "Error message for the Failed to fetch registered server error") // MARK: - Onboarding diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index bc3caa84c3a..39a63eec043 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -50,6 +50,11 @@ public struct NetworkProtectionStatusView: View { public var body: some View { VStack(spacing: 0) { + if let warning = model.warningViewModel { + WarningView(model: warning) + .transition(.slide) + } + if model.shouldShowSubscriptionExpired { SubscriptionExpiredView { model.openPrivacyPro() @@ -62,11 +67,6 @@ public struct NetworkProtectionStatusView: View { .padding(.horizontal, 5) .padding(.top, 5) .transition(.slide) - } else { - if let healthWarning = model.issueDescription { - connectionHealthWarningView(message: healthWarning) - .transition(.slide) - } } Spacer() @@ -88,28 +88,6 @@ public struct NetworkProtectionStatusView: View { // MARK: - Composite Views - private func connectionHealthWarningView(message: String) -> some View { - VStack(spacing: 0) { - HStack(alignment: .top, spacing: 12) { - Image(.warningColored) - - /// Text elements in SwiftUI don't expand horizontally more than needed, so we're adding an "optional" spacer at the end so that - /// the alert bubble won't shrink if there's not enough text. - HStack(spacing: 0) { - Text(message) - .makeSelectable() - .multilineText() - .foregroundColor(Color(.defaultText)) - - Spacer() - } - } - .padding(16) - .background(RoundedRectangle(cornerRadius: 8).fill(Color(.alertBubbleBackground))) - } - .padding(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8)) - } - private func bottomMenuView() -> some View { VStack(spacing: 0) { ForEach(model.menuItems(), id: \.name) { menuItem in diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index eb5509a68f6..5e6fc27f86b 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -22,6 +22,7 @@ import NetworkExtension import NetworkProtection import ServiceManagement import SwiftUI +import Common /// This view can be shown from any location where we want the user to be able to interact with VPN. /// This view shows status information about the VPN, and offers a chance to toggle it ON and OFF. @@ -106,6 +107,7 @@ extension NetworkProtectionStatusView { private static let serverInfoDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.serverInfoDispatchQueue", qos: .userInteractive) private static let tunnelErrorDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.tunnelErrorDispatchQueue", qos: .userInteractive) private static let controllerErrorDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.controllerErrorDispatchQueue", qos: .userInteractive) + private static let knownFailureDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.knownFailureDispatchQueue", qos: .userInteractive) // MARK: - Initialization & Deinitialization @@ -144,6 +146,7 @@ extension NetworkProtectionStatusView { isHavingConnectivityIssues = statusReporter.connectivityIssuesObserver.recentValue lastTunnelErrorMessage = statusReporter.connectionErrorObserver.recentValue lastControllerErrorMessage = statusReporter.controllerErrorMessageObserver.recentValue + knownFailure = statusReporter.knownFailureObserver.recentValue showDebugInformation = false // Particularly useful when unit testing with an initial status of our choosing. @@ -151,6 +154,7 @@ extension NetworkProtectionStatusView { subscribeToConnectivityIssues() subscribeToTunnelErrorMessages() subscribeToControllerErrorMessages() + subscribeToKnownFailures() subscribeToDebugInformationChanges() refreshLoginItemStatus() @@ -187,6 +191,12 @@ extension NetworkProtectionStatusView { } } + func openFeedbackForm() { + Task { + await appLauncher.launchApp(withCommand: .shareFeedback) + } + } + func uninstallVPN() { Task { await uninstallHandler() @@ -237,6 +247,14 @@ extension NetworkProtectionStatusView { }.store(in: &cancellables) } + private func subscribeToKnownFailures() { + statusReporter.knownFailureObserver.publisher + .removeDuplicates() + .subscribe(on: Self.knownFailureDispatchQueue) + .assign(to: \.knownFailure, onWeaklyHeld: self) + .store(in: &cancellables) + } + private func subscribeToDebugInformationChanges() { debugInformationPublisher .removeDuplicates() @@ -341,5 +359,37 @@ extension NetworkProtectionStatusView { } } + + @Published + private var knownFailure: KnownFailure? + + var warningViewModel: WarningView.Model? { + if let warningMessage = warningMessage(for: knownFailure) { + return WarningView.Model(message: warningMessage, + actionTitle: UserText.vpnShareFeedback, + action: openFeedbackForm) + } + + if let issueDescription { + return WarningView.Model(message: issueDescription, actionTitle: nil, action: nil) + } + + return nil + } + + func warningMessage(for knownFailure: KnownFailure?) -> String? { + guard let knownFailure else { return nil } + + switch KnownFailure.SilentError(rawValue: knownFailure.error) { + case .operationNotPermitted: + return UserText.vpnOperationNotPermittedMessage + case .loginItemVersionMismatched: + return UserText.vpnLoginItemVersionMismatchedMessage + case .registeredServerFetchingFailed: + return UserText.vpnRegisteredServerFetchingFailedMessage + default: + return nil + } + } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningView.swift new file mode 100644 index 00000000000..b6e48ba8363 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningView.swift @@ -0,0 +1,54 @@ +// +// WarningView.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 WarningView: View { + let model: Model + + public var body: some View { + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 12) { + Image(.warningColored) + + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 5) { + Text(model.message) + .makeSelectable() + .multilineText() + .foregroundColor(Color(.defaultText)) + + if let actionTitle = model.actionTitle, + let action = model.action { + Button(actionTitle, action: action) + .buttonStyle(DismissActionButtonStyle(textColor: Color(.defaultText))) + .keyboardShortcut(.defaultAction) + .padding(.top, 3) + } + } + + Spacer() + } + } + .padding(16) + .background(RoundedRectangle(cornerRadius: 8).fill(Color(.alertBubbleBackground))) + } + .padding(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningViewModel.swift new file mode 100644 index 00000000000..1e6e720904e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/WarningView/WarningViewModel.swift @@ -0,0 +1,35 @@ +// +// WarningViewModel.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 + +extension WarningView { + final class Model: ObservableObject { + var message: String + var actionTitle: String? + var action: (() -> Void)? + + init(message: String, + actionTitle: String? = nil, + action: (() -> Void)?) { + self.message = message + self.actionTitle = actionTitle + self.action = action + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index e7785da3f4c..b6853541fb6 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -37,13 +37,15 @@ final class TunnelControllerViewModelTests: XCTestCase { let connectivityIssuesObserver: ConnectivityIssueObserver let controllerErrorMessageObserver: ControllerErrorMesssageObserver let dataVolumeObserver: DataVolumeObserver + let knownFailureObserver: KnownFailureObserver init(status: ConnectionStatus, isHavingConnectivityIssues: Bool = false, serverInfo: NetworkProtectionStatusServerInfo = MockStatusReporter.defaultServerInfo, tunnelErrorMessage: String? = nil, controllerErrorMessage: String? = nil, - dataVolume: DataVolume = .init()) { + dataVolume: DataVolume = .init(), + failure: KnownFailure? = nil) { let mockStatusObserver = MockConnectionStatusObserver() mockStatusObserver.subject.send(status) @@ -68,6 +70,10 @@ final class TunnelControllerViewModelTests: XCTestCase { let mockDataVolumeObserver = MockDataVolumeObserver() mockDataVolumeObserver.subject.send(dataVolume) dataVolumeObserver = mockDataVolumeObserver + + let mockKnownFailureObserver = MockKnownFailureObserver() + mockKnownFailureObserver.subject.send(failure) + knownFailureObserver = mockKnownFailureObserver } func forceRefresh() { diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 3e38b79343a..d57838d088b 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: "150.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "150.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift index e5c7c83424b..1254693f512 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/ButtonStyles.swift @@ -100,11 +100,14 @@ public struct TransparentActionButtonStyle: ButtonStyle { public struct DismissActionButtonStyle: ButtonStyle { @Environment(\.colorScheme) var colorScheme - public init() {} + public let textColor: Color + + public init(textColor: Color = .primary) { + self.textColor = textColor + } public func makeBody(configuration: Self.Configuration) -> some View { let backgroundColor = configuration.isPressed ? Color(.windowBackgroundColor) : Color(.controlColor) - let labelColor = Color.primary let outerShadowOpacity = colorScheme == .dark ? 0.8 : 0.0 configuration.label @@ -124,7 +127,7 @@ public struct DismissActionButtonStyle: ButtonStyle { RoundedRectangle(cornerRadius: 5) .stroke(Color.black.opacity(0.1), lineWidth: 1) ) - .foregroundColor(labelColor) + .foregroundColor(textColor) } } diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index d9a45fda0c7..746ebca9eef 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -105,6 +105,7 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { connectionState: "connected", lastStartErrorDescription: "none", lastTunnelErrorDescription: "none", + lastKnownFailureDescription: "none", connectedServer: "Paoli, PA", connectedServerIP: "123.123.123.123" ) From 5b73e415393850574b9679056e36394e6269668b Mon Sep 17 00:00:00 2001 From: amddg44 Date: Tue, 4 Jun 2024 18:37:25 +0200 Subject: [PATCH 05/35] Removing temporary password manager survey code (#2834) Task/Issue URL: https://app.asana.com/0/414235014887631/1207424646529839/f Tech Design URL: CC: **Description**: Cleaning out all (now obsolete) code related to the temporary password manager survey --- .../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 | 42 ------------------- .../AutofillPreferencesModelTests.swift | 1 - 8 files changed, 123 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg 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 deleted file mode 100644 index 1c9ce942d36..00000000000 --- a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index fefd0c68869..00000000000 --- a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift index 7392cb7f75e..380299afe4c 100644 --- a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift +++ b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift @@ -18,7 +18,6 @@ import Foundation import Common -import BrowserServicesKit import Subscription final class SurveyURLBuilder { @@ -159,23 +158,6 @@ 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) @@ -186,15 +168,6 @@ 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 - } - private func daysSince(date storedDate: Date) -> Int? { if let days = Calendar.current.dateComponents([.day], from: storedDate, to: Date()).day { return abs(days) diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 13193f793f0..9867b97a95f 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -74,7 +74,6 @@ 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 c7e3b6d6ae6..181a25e2163 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -584,7 +584,6 @@ 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)) @@ -737,10 +736,6 @@ 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 78920dd0c05..58a55e86f16 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift @@ -27,7 +27,6 @@ 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 { @@ -150,9 +149,6 @@ 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 39914a5dad1..4a22e97dfcd 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -17,8 +17,6 @@ // import Foundation -import BrowserServicesKit -import Common final class AutofillPreferencesModel: ObservableObject { @@ -61,12 +59,6 @@ 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 { @@ -161,7 +153,6 @@ final class AutofillPreferencesModel: ObservableObject { autolockLocksFormFilling = persistor.autolockLocksFormFilling passwordManager = persistor.passwordManager hasNeverPromptWebsites = !neverPromptWebsitesManager.neverPromptWebsites.isEmpty - autofillSurveyEnabled = persistor.autofillSurveyEnabled } private var persistor: AutofillPreferencesPersistor @@ -200,37 +191,4 @@ final class AutofillPreferencesModel: ObservableObject { func openSettings() { 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, - subscription: nil, - daysSinceVPNActivated: nil, - daysSinceVPNLastActive: nil, - daysSincePIRActivated: nil, - daysSincePIRLastActive: nil - ) - - 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/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index ef02dc27f1e..9851f65f721 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -29,7 +29,6 @@ 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 d705b7bf274aa14e62269ce7906a6b9269c01bb4 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Tue, 4 Jun 2024 14:56:16 -0500 Subject: [PATCH 06/35] Display the addresses in the Debugger UI (#2828) --- .../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 78fb1ba4ad9..8207aebaa84 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift @@ -117,7 +117,7 @@ struct DataBrokerRunCustomJSONView: View { Text(scanResult.extractedProfile.name ?? "No name") .padding(.horizontal, 10) Divider() - Text(scanResult.extractedProfile.addresses?.first?.fullAddress ?? "No address") + Text(scanResult.extractedProfile.addresses?.map { $0.fullAddress }.joined(separator: ", ") ?? "No address") .padding(.horizontal, 10) Divider() Text(scanResult.extractedProfile.relatives?.joined(separator: ",") ?? "No relatives") From 62d205927ff120a5ba1a5fb11e31a707b230db6d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 4 Jun 2024 22:00:38 +0200 Subject: [PATCH 07/35] Update UI Tests CI workflows for macOS 13/14 (#2835) Task/Issue URL: https://app.asana.com/0/72649045549333/1207244139871302/f Description: This change fixes UI Tests runs in CI on macOS 13 and 14 by adjusting the code responsible for recognizing RunType.uiTests in CI (which is now also based on CI environment variable). --- .github/workflows/pr.yml | 4 +-- .github/workflows/sync_end_to_end.yml | 2 +- .../workflows/sync_end_to_end_legacy_os.yml | 2 +- .github/workflows/ui_tests.yml | 32 ++++++++++++++----- .../Extensions/NSApplicationExtension.swift | 3 +- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fd2341e1b69..2a28baa883a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -140,7 +140,7 @@ jobs: - name: Cache SPM if: env.cache_key_hash - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: DerivedData/SourcePackages key: ${{ runner.os }}-spm-${{ matrix.cache-key }}${{ env.cache_key_hash }} @@ -323,7 +323,7 @@ jobs: - name: Cache SPM if: env.cache_key_hash - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: DerivedData/SourcePackages key: ${{ runner.os }}-spm-test-release-${{ env.cache_key_hash }} diff --git a/.github/workflows/sync_end_to_end.yml b/.github/workflows/sync_end_to_end.yml index f26a4e27dd8..6138967f4d2 100644 --- a/.github/workflows/sync_end_to_end.yml +++ b/.github/workflows/sync_end_to_end.yml @@ -33,7 +33,7 @@ jobs: - name: Cache SPM if: env.cache_key_hash - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: DerivedData/SourcePackages key: ${{ runner.os }}-spm-${{ env.cache_key_hash }} diff --git a/.github/workflows/sync_end_to_end_legacy_os.yml b/.github/workflows/sync_end_to_end_legacy_os.yml index e6614a1ef06..dc5bd6eb6dd 100644 --- a/.github/workflows/sync_end_to_end_legacy_os.yml +++ b/.github/workflows/sync_end_to_end_legacy_os.yml @@ -71,7 +71,7 @@ jobs: - name: Cache SPM if: env.cache_key_hash - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: DerivedData/SourcePackages key: ${{ runner.os }}-spm-${{ env.cache_key_hash }} diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml index 44bbfa84102..d01c95ed3db 100644 --- a/.github/workflows/ui_tests.yml +++ b/.github/workflows/ui_tests.yml @@ -33,7 +33,7 @@ jobs: - name: Cache SPM if: env.cache_key_hash - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: DerivedData/SourcePackages key: ${{ runner.os }}-spm-${{ env.cache_key_hash }} @@ -60,11 +60,22 @@ jobs: - name: Select Xcode run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer - - name: Build and run UI Testing + - name: Build for testing run: | defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 defaults write com.duckduckgo.macos.browser.review onboarding.finished -bool true - set -o pipefail && xcodebuild test \ + set -o pipefail && xcodebuild build-for-testing \ + -scheme "UI Tests" \ + -configuration Review \ + -derivedDataPath DerivedData \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + | tee xcodebuild.log \ + | xcbeautify + + - name: Run UI Tests + run: | + set -o pipefail && xcodebuild test-without-building \ -scheme "UI Tests" \ -configuration Review \ -derivedDataPath DerivedData \ @@ -72,10 +83,10 @@ jobs: -skipMacroValidation \ -test-iterations 2 \ -retry-tests-on-failure \ - | tee xcodebuild.log \ - | xcbeautify --report junit --report-path . --junit-report-filename ui-tests.xml + | tee -a xcodebuild.log \ + | tee ui-tests.log - # - name: Create Asana task when workflow failed + # - name: Create Asana task when workflow failed # if: ${{ failure() }} && github.ref == 'refs/heads/main' # run: | # curl -s "https://app.asana.com/api/1.0/tasks" \ @@ -84,6 +95,11 @@ jobs: # --header "Content-Type: application/json" \ # --data ' { "data": { "name": "GH Workflow Failure - UI Tests", "projects": [ "${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }' + - name: Prepare test report + if: always() + run: | + xcbeautify --report junit --report-path . --junit-report-filename ui-tests.xml < ui-tests.log + - name: Publish tests report uses: mikepenz/action-junit-report@v4 if: always() @@ -93,11 +109,11 @@ jobs: - name: Upload logs when workflow failed uses: actions/upload-artifact@v4 - if: failure() + if: failure() || cancelled() with: name: "BuildLogs ${{ matrix.runner }}" path: | xcodebuild.log DerivedData/Logs/Test/*.xcresult ~/Library/Logs/DiagnosticReports/* - retention-days: 7 \ No newline at end of file + retention-days: 1 diff --git a/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift b/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift index c1c9e9b5925..d549fdb3f23 100644 --- a/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift @@ -60,7 +60,8 @@ extension NSApplication { return .normal } #elseif REVIEW - if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" { + // UITEST_MODE is set from UI Tests code, CI is always set in CI + if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" || ProcessInfo.processInfo.environment["CI"] != nil { return .uiTests } return .normal From 05bc6efa9391e92030c89e86836ca64ee1ce1018 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 4 Jun 2024 22:13:32 -0300 Subject: [PATCH 08/35] DBP: Implement exponential backoff for optout retries (#2815) --- .../OperationPreferredDateCalculator.swift | 10 +- ...perationPreferredDateCalculatorTests.swift | 115 +++++++++++++++++- 2 files changed, 118 insertions(+), 7 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift index ca42eaedda7..06b9f0b7e91 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift @@ -35,7 +35,6 @@ struct OperationPreferredDateCalculator { extractedProfileID: Int64?, schedulingConfig: DataBrokerScheduleConfig, isDeprecated: Bool = false) throws -> Date? { - guard let lastEvent = historyEvents.last else { throw DataBrokerProtectionError.cantCalculatePreferredRunDate } @@ -63,7 +62,6 @@ struct OperationPreferredDateCalculator { extractedProfileID: Int64?, schedulingConfig: DataBrokerScheduleConfig, date: DateProtocol = SystemDate()) throws -> Date? { - guard let lastEvent = historyEvents.last else { throw DataBrokerProtectionError.cantCalculatePreferredRunDate } @@ -78,7 +76,7 @@ struct OperationPreferredDateCalculator { return currentPreferredRunDate } case .error: - return date.now.addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) + return date.now.addingTimeInterval(calculateNextRunDateOnError(schedulingConfig: schedulingConfig, historyEvents: historyEvents)) case .optOutStarted, .scanStarted, .noMatchFound: return currentPreferredRunDate case .optOutConfirmed, .optOutRequested: @@ -97,4 +95,10 @@ struct OperationPreferredDateCalculator { let lastRemovalEventDate = lastRemovalEvent.date.addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds) return lastRemovalEventDate < Date() } + + private func calculateNextRunDateOnError(schedulingConfig: DataBrokerScheduleConfig, + historyEvents: [HistoryEvent]) -> TimeInterval { + let pastTries = historyEvents.filter { $0.isError }.count + return min(Int(pow(2.0, Double(pastTries))), schedulingConfig.retryError).hoursToSeconds + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift index 959f9555b45..f491a389c93 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift @@ -23,7 +23,7 @@ import XCTest final class OperationPreferredDateCalculatorTests: XCTestCase { private let schedulingConfig = DataBrokerScheduleConfig( - retryError: 1000, + retryError: 48, confirmOptOutScan: 2000, maintenanceScan: 3000 ) @@ -322,17 +322,124 @@ final class OperationPreferredDateCalculatorTests: XCTestCase { XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) } - func testError_thenOptOutDateIsRetry() throws { - let expectedOptOutDate = Date().addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) - + func testWhenOptOutFailedOnce_thenWeRetryInTwoHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date())! let historyEvents = [ HistoryEvent(extractedProfileId: 1, brokerId: 1, profileQueryId: 1, type: .error(error: DataBrokerProtectionError.malformedURL))] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedTwice_thenWeRetryInFourHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 4, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedThreeTimes_thenWeRetryInEightHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 8, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedThreeTimes_thenWeRetryInSixteenHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 16, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedThreeTimes_thenWeRetryInThirtyTwoHours() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .hour, value: 32, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedSixTimes_thenWeRetryInTwoDays() throws { + let expectedOptOutDate = Calendar.current.date(byAdding: .day, value: 2, to: Date())! + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() + let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, + historyEvents: historyEvents, + extractedProfileID: nil, + schedulingConfig: schedulingConfig) + + XCTAssertTrue(areDatesEqualIgnoringSeconds(date1: expectedOptOutDate, date2: actualOptOutDate)) + } + + func testWhenOptOutFailedMoreThanTheThreshold_thenWeRetryAtTheSchedulingRetry() throws { + let expectedOptOutDate = Date().addingTimeInterval(schedulingConfig.retryError.hoursToSeconds) + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .malformedURL)) + ] + let calculator = OperationPreferredDateCalculator() let actualOptOutDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: nil, historyEvents: historyEvents, extractedProfileID: nil, From 62e901ea8515d3e16b0b21bf71cc38d1a5b588e5 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 4 Jun 2024 20:09:12 -0700 Subject: [PATCH 09/35] Remove temporary notification pixels (#2803) Task/Issue URL: https://app.asana.com/0/1193060753475688/1207145810980144/f Tech Design URL: CC: Description: Removes changes from #2681 --- DuckDuckGo.xcodeproj/project.pbxproj | 24 ---------- .../NetworkProtectionPixelEvent.swift | 36 +------------- ...rkProtectionUNNotificationsPresenter.swift | 15 ------ .../DuckDuckGoNotificationsAppDelegate.swift | 48 ------------------- 4 files changed, 2 insertions(+), 121 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 537548fe8ae..f74238f070b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1099,12 +1099,6 @@ 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 */; }; @@ -4272,9 +4266,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4B1EFF1C2BD71EEF007CC84F /* PixelKit in Frameworks */, 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */, - 4B1EFF212BD72189007CC84F /* Networking in Frameworks */, 37269F052B3332C2005E8E46 /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8544,8 +8536,6 @@ packageProductDependencies = ( 37269F042B3332C2005E8E46 /* Common */, 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */, - 4B1EFF1B2BD71EEF007CC84F /* PixelKit */, - 4B1EFF202BD72189007CC84F /* Networking */, ); productName = DuckDuckGoNotifications; productReference = 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */; @@ -10821,13 +10811,9 @@ 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; }; @@ -13225,16 +13211,6 @@ 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 e9b9f4c847c..a5fa99cb357 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -98,13 +98,6 @@ 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 @@ -284,21 +277,6 @@ 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" } } @@ -387,12 +365,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionRekeyAttempt, .networkProtectionRekeyCompleted, .networkProtectionRekeyFailure, - .networkProtectionSystemExtensionActivationFailure, - .networkProtectionConnectedNotificationDisplayed, - .networkProtectionDisconnectedNotificationDisplayed, - .networkProtectionReconnectingNotificationDisplayed, - .networkProtectionSupersededNotificationDisplayed, - .networkProtectionExpiredEntitlementNotificationDisplayed: + .networkProtectionSystemExtensionActivationFailure: return nil } } @@ -458,12 +431,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionWireguardErrorCannotStartWireguardBackend, .networkProtectionNoAuthTokenFoundError, .networkProtectionRekeyAttempt, - .networkProtectionRekeyCompleted, - .networkProtectionConnectedNotificationDisplayed, - .networkProtectionDisconnectedNotificationDisplayed, - .networkProtectionReconnectingNotificationDisplayed, - .networkProtectionSupersededNotificationDisplayed, - .networkProtectionExpiredEntitlementNotificationDisplayed: + .networkProtectionRekeyCompleted: return nil } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index 80ac5013fc6..3977fccef59 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -20,7 +20,6 @@ import Foundation import UserNotifications import NetworkProtection import NetworkProtectionUI -import PixelKit extension UNNotificationAction { @@ -160,20 +159,6 @@ 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 75b34716443..41677dc8f13 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -19,8 +19,6 @@ import Cocoa import Combine import Common -import Networking -import PixelKit import NetworkExtension import NetworkProtection @@ -71,38 +69,6 @@ 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") } @@ -191,17 +157,3 @@ 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 74005f378be1e173b0b673ade5c65a3ed2dc9485 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 5 Jun 2024 16:37:48 +0200 Subject: [PATCH 10/35] Update Sync end-to-end tests and make them run in CI (#2837) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207472935260046/f Description: This change updates Sync end-to-end tests to support recently changed bookmarks management UI. Additionally, some good practices from general UI tests were applied to Sync tests. Sync end-to-end tests workflow was updated similarly to general UI tests workflow to ensure it works fine in CI. Credentials sync can now be UI tested locally (while still being disabled in CI). --- .github/workflows/pr.yml | 4 +- .github/workflows/sync_end_to_end.yml | 36 +++-- .../workflows/sync_end_to_end_legacy_os.yml | 2 +- .github/workflows/ui_tests.yml | 6 +- DuckDuckGo.xcodeproj/project.pbxproj | 6 + .../xcschemes/sandbox-test-tool.xcscheme | 2 +- DuckDuckGo/Application/AppDelegate.swift | 4 +- ...okmarkManagementDetailViewController.swift | 3 + DuckDuckGo/Common/Database/Database.swift | 4 +- .../NSAccessibilityProtocolExtension.swift | 33 ++++ .../Extensions/NSApplicationExtension.swift | 20 ++- .../Extensions/NSMenuItemExtension.swift | 11 -- .../DeviceAuthenticator.swift | 2 +- DuckDuckGo/Menus/MainMenu.swift | 1 + .../NavigationBar/View/MoreOptionsMenu.swift | 2 + .../Preferences/Model/SyncPreferences.swift | 2 +- .../PasswordManagementViewController.swift | 2 +- DuckDuckGo/Sync/SyncDataProviders.swift | 4 +- DuckDuckGo/Sync/SyncDebugMenu.swift | 5 +- SyncE2EUITests/CriticalPathsTests.swift | 149 ++++++++++++------ 20 files changed, 204 insertions(+), 94 deletions(-) create mode 100644 DuckDuckGo/Common/Extensions/NSAccessibilityProtocolExtension.swift diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2a28baa883a..d81c71d2ede 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -78,7 +78,7 @@ jobs: run: bats --formatter junit scripts/tests/* > bats-tests.xml - name: Publish unit tests report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails with: check_name: "Test Report: Shell Scripts" @@ -209,7 +209,7 @@ jobs: fi - name: Publish unit tests report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails with: check_name: "Test Report: ${{ matrix.flavor }}" diff --git a/.github/workflows/sync_end_to_end.yml b/.github/workflows/sync_end_to_end.yml index 6138967f4d2..31f305bef35 100644 --- a/.github/workflows/sync_end_to_end.yml +++ b/.github/workflows/sync_end_to_end.yml @@ -2,8 +2,8 @@ name: Sync-End-to-End tests on: workflow_dispatch: - # schedule: - # - cron: '0 5 * * *' # run at 5 AM UTC + schedule: + - cron: '0 5 * * *' # run at 5 AM UTC jobs: sync-end-to-end-tests: @@ -66,14 +66,25 @@ jobs: with: debug: true - - name: Build and run Sync e2e tests + - name: Build Sync e2e tests + run: | + set -o pipefail && xcodebuild build-for-testing \ + -scheme "Sync End-to-End UI Tests" \ + -configuration Review \ + -derivedDataPath DerivedData \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + | tee xcodebuild.log \ + | xcbeautify + + - name: Run UI Tests env: CODE: ${{ steps.sync-recovery-code.outputs.recovery-code }} run: | - defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 defaults write com.duckduckgo.macos.browser.review sync.environment Development + defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 defaults write com.duckduckgo.macos.browser.review onboarding.finished -bool true - set -o pipefail && xcodebuild test \ + set -o pipefail && xcodebuild test-without-building \ -scheme "Sync End-to-End UI Tests" \ -configuration Review \ -derivedDataPath DerivedData \ @@ -81,9 +92,9 @@ jobs: -skipMacroValidation \ -test-iterations 2 \ -retry-tests-on-failure \ - | tee xcodebuild.log \ - | xcbeautify --report junit --report-path . --junit-report-filename ui-tests.xml \ - + | tee -a xcodebuild.log \ + | tee ui-tests.log + # - name: Create Asana task when workflow failed # if: ${{ failure() }} # run: | @@ -93,8 +104,13 @@ jobs: # --header "Content-Type: application/json" \ # --data ' { "data": { "name": "GH Workflow Failure - Sync End to end tests", "projects": [ "${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }' + - name: Prepare test report + if: always() + run: | + xcbeautify --report junit --report-path . --junit-report-filename ui-tests.xml < ui-tests.log + - name: Publish tests report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails with: check_name: "Test Report ${{ matrix.runner }}" @@ -102,7 +118,7 @@ jobs: - name: Upload logs when workflow failed uses: actions/upload-artifact@v4 - if: failure() + if: failure() || cancelled() with: name: "BuildLogs ${{ matrix.runner }}" path: | diff --git a/.github/workflows/sync_end_to_end_legacy_os.yml b/.github/workflows/sync_end_to_end_legacy_os.yml index dc5bd6eb6dd..125797a61b8 100644 --- a/.github/workflows/sync_end_to_end_legacy_os.yml +++ b/.github/workflows/sync_end_to_end_legacy_os.yml @@ -156,7 +156,7 @@ jobs: # --data ' { "data": { "name": "GH Workflow Failure - Sync End to end tests", "projects": [ "${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }' - name: Publish tests report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() with: check_name: "Test Report ${{ matrix.runner }}" diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml index d01c95ed3db..ab68232079a 100644 --- a/.github/workflows/ui_tests.yml +++ b/.github/workflows/ui_tests.yml @@ -62,8 +62,6 @@ jobs: - name: Build for testing run: | - defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 - defaults write com.duckduckgo.macos.browser.review onboarding.finished -bool true set -o pipefail && xcodebuild build-for-testing \ -scheme "UI Tests" \ -configuration Review \ @@ -75,6 +73,8 @@ jobs: - name: Run UI Tests run: | + defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 + defaults write com.duckduckgo.macos.browser.review onboarding.finished -bool true set -o pipefail && xcodebuild test-without-building \ -scheme "UI Tests" \ -configuration Review \ @@ -116,4 +116,4 @@ jobs: xcodebuild.log DerivedData/Logs/Test/*.xcresult ~/Library/Logs/DiagnosticReports/* - retention-days: 1 + retention-days: 7 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f74238f070b..64c3aebdaca 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1002,6 +1002,8 @@ 3776583127F8325B009A6B35 /* AutofillPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */; }; 377D801C2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D801B2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D801B2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; + 377D8D642C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */; }; + 377D8D652C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */; }; 378205F62837CBA800D1D4AA /* SavedStateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F52837CBA800D1D4AA /* SavedStateMock.swift */; }; 378205F8283BC6A600D1D4AA /* StartupPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */; }; 378205FB283C277800D1D4AA /* MainMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205FA283C277800D1D4AA /* MainMenuTests.swift */; }; @@ -3000,6 +3002,7 @@ 3776582E27F82E62009A6B35 /* AutofillPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillPreferences.swift; sourceTree = ""; }; 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesTests.swift; sourceTree = ""; }; 377D801B2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeSyncHandler.swift; sourceTree = ""; }; + 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAccessibilityProtocolExtension.swift; sourceTree = ""; }; 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAppStoreCI.entitlements; sourceTree = ""; }; 378205F52837CBA800D1D4AA /* SavedStateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedStateMock.swift; sourceTree = ""; }; 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupPreferencesTests.swift; sourceTree = ""; }; @@ -7450,6 +7453,7 @@ 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */, 4B8D9061276D1D880078DB17 /* LocaleExtension.swift */, B66B9C5B29A5EBAD0010E8F3 /* NavigationActionExtension.swift */, + 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */, 85308E24267FC9F2001ABD76 /* NSAlertExtension.swift */, F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */, AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */, @@ -10202,6 +10206,7 @@ 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, B60293E72BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, + 377D8D652C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */, 3706FC53293F65D500E42796 /* TabBarScrollView.swift in Sources */, B6104E9C2BA9C173008636B2 /* DownloadResumeData.swift in Sources */, 3706FC54293F65D500E42796 /* BookmarkListTreeControllerDataSource.swift in Sources */, @@ -11421,6 +11426,7 @@ 3775912D29AAC72700E26367 /* SyncPreferences.swift in Sources */, F1B33DF22BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */, F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */, + 377D8D642C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */, 1DB9618329F67F6200CF5568 /* FaviconNullStore.swift in Sources */, BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, B693954F26F04BEB0015B914 /* PaddedImageButton.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index 41730d70695..eb7e5e26bb6 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"> Self { + self.setAccessibilityIdentifier(accessibilityIdentifier) + return self + } + + func withAccessibilityValue(_ accessibilityValue: String) -> Self { + self.setAccessibilityValue(accessibilityValue) + return self + } +} diff --git a/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift b/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift index d549fdb3f23..e9faf4b0e8b 100644 --- a/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift @@ -30,19 +30,26 @@ extension NSApplication { case unitTests case integrationTests case uiTests + case uiTestsInCI case xcPreviews /// Defines if app run type requires loading full environment, i.e. databases, saved state, keychain etc. var requiresEnvironment: Bool { switch self { - case .normal, .integrationTests, .uiTests: + case .normal, .integrationTests, .uiTests, .uiTestsInCI: return true case .unitTests, .xcPreviews: return false } } + + var isUITests: Bool { + self == .uiTests || self == .uiTestsInCI + } } + static let runType: RunType = { + let isCI = ProcessInfo.processInfo.environment["CI"] != nil #if DEBUG if let testBundlePath = ProcessInfo().environment["XCTestBundlePath"] { if testBundlePath.contains("Unit") { @@ -50,19 +57,20 @@ extension NSApplication { } else if testBundlePath.contains("Integration") { return .integrationTests } else { - return .uiTests + return isCI ? .uiTestsInCI : .uiTests } } else if ProcessInfo().environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { return .xcPreviews } else if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" { - return .uiTests + return isCI ? .uiTestsInCI : .uiTests } else { return .normal } #elseif REVIEW - // UITEST_MODE is set from UI Tests code, CI is always set in CI - if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" || ProcessInfo.processInfo.environment["CI"] != nil { - return .uiTests + // UITEST_MODE is set from UI Tests code, but this check didn't work reliably + // in CI on its own, so we're defaulting all CI runs of the REVIEW app to UI Tests + if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" || isCI { + return isCI ? .uiTestsInCI : .uiTests } return .normal #else diff --git a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift index dfe76b50925..bfa2e24e090 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift @@ -104,17 +104,6 @@ extension NSMenuItem { return self } - @discardableResult - func withAccessibilityIdentifier(_ accessibilityIdentifier: String) -> NSMenuItem { - self.setAccessibilityIdentifier(accessibilityIdentifier) - return self - } - - func withAccessibilityValue(_ accessibilityValue: String) -> NSMenuItem { - self.setAccessibilityValue(accessibilityValue) - return self - } - @discardableResult func withImage(_ image: NSImage?) -> NSMenuItem { self.image = image diff --git a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift index 45219d5e5d1..5e7ae66fa4c 100644 --- a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift +++ b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift @@ -154,7 +154,7 @@ final class DeviceAuthenticator: UserAuthenticating { } func authenticateUser(reason: AuthenticationReason, result: @escaping (DeviceAuthenticationResult) -> Void) { - guard NSApp.runType != .uiTests else { + guard !NSApp.runType.isUITests else { result(.success) return } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 181a25e2163..6959171db9c 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -606,6 +606,7 @@ import SubscriptionUI } NSMenuItem(title: "Sync & Backup") .submenu(SyncDebugMenu()) + .withAccessibilityIdentifier("MainMenu.syncAndBackup") #if DBP NSMenuItem(title: "Personal Information Removal") diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index b4a1e23483b..426a8fc60f3 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -288,6 +288,7 @@ final class MoreOptionsMenu: NSMenu { .targetting(self) .withImage(.passwordManagement) .withSubmenu(loginsSubMenu) + .withAccessibilityIdentifier("MoreOptionsMenu.autofill") addItem(NSMenuItem.separator()) } @@ -740,6 +741,7 @@ final class LoginsSubMenu: NSMenu { private func updateMenuItems(with target: AnyObject) { addItem(withTitle: UserText.passwordManagementAllItems, action: #selector(MoreOptionsMenu.openAutofillWithAllItems), keyEquivalent: "") .targetting(target) + .withAccessibilityIdentifier("LoginsSubMenu.allItems") addItem(NSMenuItem.separator()) diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index f0b6a10fc54..f859088150c 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -398,7 +398,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { return } - guard [NSApplication.RunType.normal, .uiTests].contains(NSApp.runType) else { + guard [NSApplication.RunType.normal, .uiTests, .uiTestsInCI].contains(NSApp.runType) else { return } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index bb8bd460466..1e2671b633d 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -269,7 +269,7 @@ final class PasswordManagementViewController: NSViewController { } private func promptForAuthenticationIfNecessary() { - guard NSApp.runType != .uiTests else { + guard !NSApp.runType.isUITests else { toggleLockScreen(hidden: true) return } diff --git a/DuckDuckGo/Sync/SyncDataProviders.swift b/DuckDuckGo/Sync/SyncDataProviders.swift index f3a7054f7ea..3017100e9a6 100644 --- a/DuckDuckGo/Sync/SyncDataProviders.swift +++ b/DuckDuckGo/Sync/SyncDataProviders.swift @@ -44,8 +44,8 @@ final class SyncDataProviders: DataProvidersSource { metricsEventsHandler: metricsEventsHandler ) - // Credentials syncing is disabled in UI Tests until we figure out Secure Vault errors in CI - if NSApp.runType != .uiTests { + // Credentials syncing is disabled in UI Tests in CI until we figure out Secure Vault errors + if NSApp.runType != .uiTestsInCI { credentialsAdapter.setUpProviderIfNeeded( secureVaultFactory: secureVaultFactory, metadataStore: syncMetadata, diff --git a/DuckDuckGo/Sync/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift index db74c0125df..66d4f269486 100644 --- a/DuckDuckGo/Sync/SyncDebugMenu.swift +++ b/DuckDuckGo/Sync/SyncDebugMenu.swift @@ -31,6 +31,7 @@ final class SyncDebugMenu: NSMenu { buildItems { NSMenuItem(title: "Environment") .submenu(environmentMenu) + .withAccessibilityIdentifier("SyncDebugMenu.environment") NSMenuItem(title: "Reset Favicons Fetcher Onboarding Dialog", action: #selector(resetFaviconsFetcherOnboardingDialog)) .targetting(self) NSMenuItem(title: "Populate Stub objects", action: #selector(createStubsForDebug)) @@ -58,14 +59,14 @@ final class SyncDebugMenu: NSMenu { let statusMenuItem = NSMenuItem(title: "Current: \(currentEnvironment.description)", action: nil, keyEquivalent: "") statusMenuItem.isEnabled = false - environmentMenu.addItem(statusMenuItem) + environmentMenu.addItem(statusMenuItem.withAccessibilityIdentifier("SyncDebugMenu.currentEnvironment")) let toggleMenuItem = NSMenuItem( title: "Switch to \(anotherEnvironment.description)", action: #selector(switchSyncEnvironment), target: self, representedObject: anotherEnvironment) - environmentMenu.addItem(toggleMenuItem) + environmentMenu.addItem(toggleMenuItem.withAccessibilityIdentifier("SyncDebugMenu.switchEnvironment")) } @objc func switchSyncEnvironment(_ sender: NSMenuItem) { diff --git a/SyncE2EUITests/CriticalPathsTests.swift b/SyncE2EUITests/CriticalPathsTests.swift index 32a1115a2bd..a830183df4c 100644 --- a/SyncE2EUITests/CriticalPathsTests.swift +++ b/SyncE2EUITests/CriticalPathsTests.swift @@ -19,8 +19,26 @@ import XCTest import JavaScriptCore +extension XCUIElement { + /// Timeout constants for different test requirements + enum Timeouts { + /// Mostly, we use timeouts to wait for element existence. This is about 3x longer than needed, for CI resilience + static let elementExistence: Double = 5.0 + } + + @discardableResult + func assertExists(with timeout: TimeInterval = Timeouts.elementExistence) -> XCUIElement { + XCTAssertTrue(waitForExistence(timeout: timeout), "UI element didn't become available in a reasonable timeframe.") + return self + } +} + final class CriticalPathsTests: XCTestCase { + var isCI: Bool { + ProcessInfo.processInfo.environment["CI"] != nil + } + var app: XCUIApplication! var debugMenuBarItem: XCUIElement! var internaluserstateMenuItem: XCUIElement! @@ -33,43 +51,60 @@ final class CriticalPathsTests: XCTestCase { if app.windows.count == 0 { app.menuItems["newWindow:"].click() } - toggleInternalUserState() + selectDevelopmentEnvironment() cleanupAndResetData() } override func tearDown() { - toggleInternalUserState() cleanupAndResetData() - app.menuItems["closeAllWindows:"].click() + app.typeKey(",", modifierFlags: [.command, .option, .shift]) } private func accessSettings() { - app.menuItems["openPreferences:"].click() + app.typeKey(",", modifierFlags: .command) let settingsWindow = app.windows["Settings"] XCTAssertTrue(settingsWindow.exists, "Settings window is not visible") } - private func toggleInternalUserState() { + private func selectDevelopmentEnvironment() { let menuBarsQuery = app.menuBars debugMenuBarItem = menuBarsQuery.menuBarItems["Debug"] debugMenuBarItem.click() - internaluserstateMenuItem = menuBarsQuery.menuItems["internalUserState:"] - internaluserstateMenuItem.click() + + let syncAndBackupMenuItem = menuBarsQuery.menuItems["MainMenu.syncAndBackup"] + syncAndBackupMenuItem.assertExists().hover() + + let environmentMenuItem = syncAndBackupMenuItem.menuItems["SyncDebugMenu.environment"] + environmentMenuItem.assertExists().hover() + + let currentEnvironmentMenuItem = syncAndBackupMenuItem.menuItems["SyncDebugMenu.currentEnvironment"] + currentEnvironmentMenuItem.assertExists() + if !currentEnvironmentMenuItem.title.contains("Development") { + let switchEnvironmentMenuItem = syncAndBackupMenuItem.menuItems["SyncDebugMenu.switchEnvironment"] + switchEnvironmentMenuItem.assertExists() + guard switchEnvironmentMenuItem.title == "Switch to Development" else { + XCTFail("Failed to switch to Development Sync environment") + return + } + switchEnvironmentMenuItem.click() + } } private func cleanupAndResetData() { let menuBarsQuery = app.menuBars debugMenuBarItem = menuBarsQuery.menuBarItems["Debug"] - debugMenuBarItem.click() + debugMenuBarItem.assertExists().click() let resetDataMenuItem = menuBarsQuery.menuItems["MainMenu.resetData"] - resetDataMenuItem.hover() + resetDataMenuItem.assertExists().hover() let resetBookMarksData = resetDataMenuItem.menuItems["MainMenu.resetBookmarks"] + resetBookMarksData.assertExists().hover() resetBookMarksData.click() debugMenuBarItem.click() - resetDataMenuItem.hover() + resetDataMenuItem.assertExists().hover() let resetAutofillData = resetDataMenuItem.menuItems["MainMenu.resetSecureVaultData"] + resetAutofillData.assertExists().hover() resetAutofillData.click() } @@ -202,8 +237,12 @@ final class CriticalPathsTests: XCTestCase { // Add Bookmarks and Favorite addBookmarksAndFavorites() - // Add Login - addLogin() + // Temporarily skipping Logins testing in CI until we resolve the problem with encrypted value transformers + // See makeDatabase() in Database.swift + if !isCI { + // Add Login + addLogin() + } // Copy code to clipboard copyToClipboard(code: code) @@ -221,10 +260,13 @@ final class CriticalPathsTests: XCTestCase { checkFavoriteNonUnified() // Remove Bookmarks + let settingsWindow = app.windows["Settings"] + settingsWindow.popUpButtons["Settings"].click() + settingsWindow.menuItems["Bookmarks"].click() bookmarksWindow.staticTexts["www.spreadprivacy.com"].rightClick() - bookmarksWindow.menus.menuItems["deleteBookmark:"].click() + bookmarksWindow.menus.menuItems["ContextualMenu.deleteBookmark"].click() bookmarksWindow.staticTexts["www.duckduckgo.com"].rightClick() - bookmarksWindow.menus.menuItems["deleteBookmark:"].click() + bookmarksWindow.menus.menuItems["ContextualMenu.deleteBookmark"].click() // Log In bookmarksWindow.splitGroups.children(matching: .popUpButton).element.click() @@ -232,7 +274,6 @@ final class CriticalPathsTests: XCTestCase { logIn() // Toggle Unified Favorite - let settingsWindow = app.windows["Settings"] settingsWindow/*@START_MENU_TOKEN@*/.checkBoxes["Unify Favorites Across Devices"]/*[[".groups",".scrollViews.checkBoxes[\"Unify Favorites Across Devices\"]",".checkBoxes[\"Unify Favorites Across Devices\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.click() // Check Bookmarks @@ -241,8 +282,12 @@ final class CriticalPathsTests: XCTestCase { // Check Unified favorites checkUnifiedFavorites() - // Check Logins - // checkLogins() + // Temporarily skipping Logins testing in CI until we resolve the problem with encrypted value transformers + // See makeDatabase() in Database.swift + if !isCI { + // Check Logins + checkLogins() + } } private func logIn() { @@ -275,29 +320,37 @@ final class CriticalPathsTests: XCTestCase { } private func addBookmarksAndFavorites() { - app.menuItems["showManageBookmarks:"].click() + app.menuItems["MainMenu.manageBookmarksMenuItem"].click() let bookmarksWindow = app.windows["Bookmarks"] - bookmarksWindow.buttons[" New Bookmark"].click() + let newBookmarkButton = bookmarksWindow.buttons["BookmarkManagementDetailViewController.newBookmarkButton"].assertExists() + + newBookmarkButton.click() + let sheetsQuery = app.windows["Bookmarks"].sheets - sheetsQuery.textFields["Title Text Field"].click() - sheetsQuery.textFields["Title Text Field"].typeText("www.duckduckgo.com") - sheetsQuery.textFields["URL Text Field"].click() - sheetsQuery.textFields["URL Text Field"].typeText("www.duckduckgo.com") - sheetsQuery.buttons["Add"].click() - bookmarksWindow.buttons[" New Bookmark"].click() - sheetsQuery.textFields["Title Text Field"].click() - sheetsQuery.textFields["Title Text Field"].typeText("www.spreadprivacy.com") - sheetsQuery.textFields["URL Text Field"].click() - sheetsQuery.textFields["URL Text Field"].typeText("www.spreadprivacy.com") - sheetsQuery.buttons["Add"].click() - bookmarksWindow.staticTexts["www.spreadprivacy.com"].rightClick() - bookmarksWindow.menuItems["toggleBookmarkAsFavorite:"].click() + let titleTextField = sheetsQuery.textFields["bookmark.add.name.textfield"].assertExists() + let urlTextField = sheetsQuery.textFields["bookmark.add.url.textfield"].assertExists() + let addButton = sheetsQuery.buttons["BookmarkDialogButtonsView.defaultButton"].assertExists() + let favoriteCheckBox = sheetsQuery.checkBoxes["bookmark.add.add.to.favorites.button"].assertExists() + + titleTextField.click() + titleTextField.typeText("www.duckduckgo.com") + urlTextField.click() + urlTextField.typeText("www.duckduckgo.com") + addButton.click() + + newBookmarkButton.click() + titleTextField.click() + titleTextField.typeText("www.spreadprivacy.com") + urlTextField.click() + urlTextField.typeText("www.spreadprivacy.com") + favoriteCheckBox.click() + addButton.click() } private func addLogin() { let bookmarksWindow = app.windows["Bookmarks"] bookmarksWindow.buttons["NavigationBarViewController.optionsButton"].click() - bookmarksWindow.menuItems["Autofill"].click() + bookmarksWindow.menuItems["MoreOptionsMenu.autofill"].click() bookmarksWindow.popovers.buttons["add item"].click() bookmarksWindow.popovers.menuItems["createNewLogin"].click() let usernameTextfieldTextField = bookmarksWindow.popovers.textFields["Username TextField"] @@ -310,16 +363,13 @@ final class CriticalPathsTests: XCTestCase { } private func checkFavoriteNonUnified() { - let bookmarksWindow = app.windows["Bookmarks"] - let settingsWindow = app.windows["Settings"] - settingsWindow.popUpButtons["Settings"].click() - settingsWindow.menuItems["Bookmarks"].click() - bookmarksWindow.outlines.staticTexts["Favorites"].click() - let gitHub = bookmarksWindow.staticTexts["DuckDuckGo · GitHub"] - let spreadPrivacy = bookmarksWindow.staticTexts["www.spreadprivacy.com"] + app.typeKey("t", modifierFlags: [.command]) + let newTabPage = app.windows["New Tab"] + let gitHub = newTabPage.staticTexts["DuckDuckGo · GitHub"] + let spreadPrivacy = newTabPage.staticTexts["www.spreadprivacy.com"] XCTAssertFalse(gitHub.exists) XCTAssertTrue(spreadPrivacy.exists) - bookmarksWindow.outlines.staticTexts["Bookmarks"].click() + app.typeKey("w", modifierFlags: [.command]) } private func checkBookmarks() { @@ -351,19 +401,20 @@ final class CriticalPathsTests: XCTestCase { } private func checkUnifiedFavorites() { - let bookmarksWindow = app.windows["Bookmarks"] - let gitHub = bookmarksWindow.staticTexts["DuckDuckGo · GitHub"] - let spreadPrivacy = bookmarksWindow.staticTexts["www.spreadprivacy.com"] - bookmarksWindow.outlines.staticTexts["Favorites"].click() + app.typeKey("t", modifierFlags: [.command]) + let newTabPage = app.windows["New Tab"] + let gitHub = newTabPage.staticTexts["DuckDuckGo · GitHub"] + let spreadPrivacy = newTabPage.staticTexts["www.spreadprivacy.com"] XCTAssertTrue(gitHub.exists) XCTAssertTrue(spreadPrivacy.exists) + app.typeKey("w", modifierFlags: [.command]) } private func checkLogins() { - let bookmarksWindow = app.windows["Bookmarks"] - bookmarksWindow.buttons["NavigationBarViewController.optionsButton"].click() - bookmarksWindow.menuItems["Autofill"].click() - let elementsQuery = bookmarksWindow.popovers.scrollViews.otherElements + let currentWindow = app.windows.firstMatch + app.buttons["NavigationBarViewController.optionsButton"].click() + app.menuItems["MoreOptionsMenu.autofill"].click() + let elementsQuery = currentWindow.popovers.scrollViews.otherElements elementsQuery.buttons["Da, Dax Login, daxthetest"].click() elementsQuery.buttons["Gi, Github, githubusername"].click() elementsQuery.buttons["My, mywebsite.com, mywebsite"].click() From 827c3fdb8bc1c6f129a6f388dd1fdb4e0096ce0f Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:57:59 -0400 Subject: [PATCH 11/35] Remove usage of token store from iOS VPN (#2817) Task/Issue URL: https://app.asana.com/0/414235014887631/1207178910368608/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 64c3aebdaca..69719724b3f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12985,7 +12985,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 150.1.0; + version = 151.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 6fa7ef0118c..c78b729c67a 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" : "79fe0c99e43c6c1bf2c0a4d397368033fd37eae9", - "version" : "150.1.0" + "revision" : "2c08ce0fb8e2f7429fd57b1130dcb4a0c39e5c41", + "version" : "151.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 745a4c468dc..7262cd7b93c 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: "150.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "151.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 12be7911f0f..788623359cc 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: "150.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "151.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 d57838d088b..9b5afe9f089 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: "150.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "151.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 69fb56075f5b0a6efc5488cd540dc108ea8bc043 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 5 Jun 2024 08:46:13 -0700 Subject: [PATCH 12/35] Remove VPN waitlist feature flags (#2836) Task/Issue URL: https://app.asana.com/0/414235014887631/1207490696985210/f Tech Design URL: CC: Description: This PR updates BSK for the removed waitlist feature flags. The feature flag usage was already deleted in the past so nothing changes here, but I removed some old feature flag overrides. --- DuckDuckGo.xcodeproj/project.pbxproj | 12 +-- .../xcshareddata/swiftpm/Package.resolved | 4 +- ...erDefaults+NetworkProtectionWaitlist.swift | 84 ------------------- .../NetworkProtectionFeatureVisibility.swift | 3 - .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 7 files changed, 6 insertions(+), 103 deletions(-) delete mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionWaitlist.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 69719724b3f..bae3cb26f02 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1575,10 +1575,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 */; }; - 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 */; }; 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 */; }; @@ -3417,7 +3413,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 = ""; }; - 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 = ""; }; 8511E18325F82B34002F516B /* 01_Fire_really_small.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 01_Fire_really_small.json; sourceTree = ""; }; @@ -8242,7 +8237,6 @@ EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */, 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, - 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */, 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, ); path = AppAndExtensionAndAgentTargets; @@ -9640,7 +9634,6 @@ 3706FACD293F65D500E42796 /* PopUpButton.swift in Sources */, B6BCC54B2AFDF24B002C5499 /* TaskWithProgress.swift in Sources */, 3706FACE293F65D500E42796 /* SuggestionViewController.swift in Sources */, - 7BFE95552A9DF2990081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, B6F9BDDD2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 3706FAD1293F65D500E42796 /* VisitViewModel.swift in Sources */, 3706FAD2293F65D500E42796 /* Atb.swift in Sources */, @@ -10738,7 +10731,6 @@ B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, - 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, @@ -10787,7 +10779,6 @@ EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, - 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, F1C70D812BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -11389,7 +11380,6 @@ 4B723E1026B0006700E14D75 /* CSVImporter.swift in Sources */, 37A4CEBA282E992F00D75B89 /* StartupPreferences.swift in Sources */, 4B41EDB12B168B1E001EEDF4 /* VPNFeedbackFormView.swift in Sources */, - 7BFE95542A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, AA4BBA3B25C58FA200C4FB0F /* MainMenu.swift in Sources */, AA585D84248FD31100E9A3E2 /* BrowserTabViewController.swift in Sources */, 85707F22276A32B600DC0649 /* CallToAction.swift in Sources */, @@ -12985,7 +12975,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 151.0.0; + version = 152.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 c78b729c67a..d7d10dd71cc 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" : "2c08ce0fb8e2f7429fd57b1130dcb4a0c39e5c41", - "version" : "151.0.0" + "revision" : "e32733e0e0b03bbac2fec160a2f967a15ed1794b", + "version" : "152.0.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionWaitlist.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionWaitlist.swift deleted file mode 100644 index c557fb26053..00000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionWaitlist.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// UserDefaults+NetworkProtectionWaitlist.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 - -enum WaitlistOverride: Int { - case useRemoteValue = 0 - case on - case off - - static let `default`: WaitlistOverride = .useRemoteValue -} - -protocol WaitlistBetaOverriding { - var waitlistActive: WaitlistOverride { get } - var waitlistEnabled: WaitlistOverride { get } -} - -final class DefaultWaitlistBetaOverrides: WaitlistBetaOverriding { - private let userDefaults: UserDefaults = .netP - - var waitlistActive: WaitlistOverride { - .init(rawValue: userDefaults.networkProtectionWaitlistBetaActiveOverrideRawValue) ?? .default - } - - var waitlistEnabled: WaitlistOverride { - .init(rawValue: userDefaults.networkProtectionWaitlistEnabledOverrideRawValue) ?? .default - } -} - -extension UserDefaults { - // Convenience declaration - var networkProtectionWaitlistActiveOverrideRawValueKey: String { - UserDefaultsWrapper.Key.networkProtectionWaitlistActiveOverrideRawValue.rawValue - } - - /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` - /// extension, and the key for this property must match its name exactly. - /// - @objc - dynamic var networkProtectionWaitlistBetaActiveOverrideRawValue: Int { - get { - value(forKey: networkProtectionWaitlistActiveOverrideRawValueKey) as? Int ?? WaitlistOverride.default.rawValue - } - - set { - set(newValue, forKey: networkProtectionWaitlistActiveOverrideRawValueKey) - } - } - - // Convenience declaration - var networkProtectionWaitlistEnabledOverrideRawValueKey: String { - UserDefaultsWrapper.Key.networkProtectionWaitlistEnabledOverrideRawValue.rawValue - } - - /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` - /// extension, and the key for this property must match its name exactly. - /// - @objc - dynamic var networkProtectionWaitlistEnabledOverrideRawValue: Int { - get { - value(forKey: networkProtectionWaitlistEnabledOverrideRawValueKey) as? Int ?? WaitlistOverride.default.rawValue - } - - set { - set(newValue, forKey: networkProtectionWaitlistEnabledOverrideRawValueKey) - } - } -} diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index a9b92cdac07..e88a74b3406 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -40,7 +40,6 @@ protocol NetworkProtectionFeatureVisibility { struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private static var subscriptionAuthTokenPrefix: String { "ddg:" } private let vpnUninstaller: VPNUninstalling - private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation private let privacyConfigurationManager: PrivacyConfigurationManaging private let defaults: UserDefaults @@ -48,7 +47,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), - featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), vpnUninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP, log: OSLog = .networkProtection, @@ -57,7 +55,6 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { self.privacyConfigurationManager = privacyConfigurationManager self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.vpnUninstaller = vpnUninstaller - self.featureOverrides = featureOverrides self.defaults = defaults self.subscriptionManager = subscriptionManager } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 7262cd7b93c..37ad4bb25c6 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: "151.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 788623359cc..eb18be1f3aa 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: "151.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.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 9b5afe9f089..8fab2675395 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: "151.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From ee8a29c3cb36c1287f43c74475b78c9ad728d5ad Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 5 Jun 2024 23:59:47 +0200 Subject: [PATCH 13/35] Generalize app launcher to prepare it for UDS support (#2824) Task/Issue URL: https://app.asana.com/0/1206580121312550/1207452193049430/f ## Description We need some form of support for launching the app besides login items, now that we can support UDS and we need to be able to launch the browser directly. This PR generalizes the app launcher and separates the VPN logic from it. --- DuckDuckGo.xcodeproj/project.pbxproj | 70 ++++++-- DuckDuckGo/Application/URLEventHandler.swift | 16 +- DuckDuckGo/DBP/DBPHomeViewController.swift | 4 +- .../AppLauncher.swift | 156 ------------------ ...etworkProtectionNavBarPopoverManager.swift | 14 +- ...rkProtectionUNNotificationsPresenter.swift | 6 +- ...tectionNotificationsPresenterFactory.swift | 1 + .../DuckDuckGoNotificationsAppDelegate.swift | 2 + .../AppLauncher+DefaultInitializer.swift | 1 + DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 12 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + LocalPackages/AppLauncher/Package.swift | 26 +++ .../AppLauncher/AppLaunchCommand.swift} | 17 +- .../Sources/AppLauncher/AppLauncher.swift | 77 +++++++++ .../AppLauncherTests/AppLauncherTests.swift | 24 +++ .../NetworkProtectionMac/Package.swift | 17 ++ .../Menu/StatusBarMenu.swift | 8 +- .../NetworkProtectionPopover.swift | 4 +- .../VPNUIActionHandler.swift | 26 +++ .../NetworkProtectionStatusViewModel.swift | 12 +- .../TunnelControllerViewModel.swift | 10 +- .../AppLauncher+VPNUIActionHandler.swift | 40 +++++ .../VPNAppLauncher/VPNAppLaunchCommand.swift | 76 +++++++++ .../MockVPNUIActionHandler.swift} | 39 +++-- .../NetworkProtectionStatusBarMenuTests.swift | 4 +- .../TunnelControllerViewModelTests.swift | 14 +- 26 files changed, 435 insertions(+), 249 deletions(-) delete mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift create mode 100644 LocalPackages/AppLauncher/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 LocalPackages/AppLauncher/Package.swift rename LocalPackages/{NetworkProtectionMac/Tests/NetworkProtectionUITests/AppLaunching/MockAppLauncher.swift => AppLauncher/Sources/AppLauncher/AppLaunchCommand.swift} (62%) create mode 100644 LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift create mode 100644 LocalPackages/AppLauncher/Tests/AppLauncherTests/AppLauncherTests.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/VPNUIActionHandler/VPNUIActionHandler.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/AppLauncher+VPNUIActionHandler.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift rename LocalPackages/NetworkProtectionMac/{Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift => Tests/NetworkProtectionUITests/MockVPNUIActionHandler/MockVPNUIActionHandler.swift} (52%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bae3cb26f02..87f94d809e6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1494,7 +1494,12 @@ 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; - 7B25856C2BA2F2D000D49F79 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; + 7B2366842C09FAC2002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366832C09FAC2002D393F /* VPNAppLauncher */; }; + 7B2366862C09FACD002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366852C09FACD002D393F /* VPNAppLauncher */; }; + 7B2366882C09FADA002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366872C09FADA002D393F /* VPNAppLauncher */; }; + 7B23668A2C09FAE8002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366892C09FAE8002D393F /* VPNAppLauncher */; }; + 7B23668C2C09FAF1002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B23668B2C09FAF1002D393F /* VPNAppLauncher */; }; + 7B23668E2C09FAFA002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B23668D2C09FAFA002D393F /* VPNAppLauncher */; }; 7B25856E2BA2F2ED00D49F79 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B25856D2BA2F2ED00D49F79 /* NetworkProtectionUI */; }; 7B2DDCF82A93A8BB0039D884 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; @@ -2534,7 +2539,6 @@ EE9D81C32BC57A3700338BE3 /* StateRestorationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */; }; 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 */; }; @@ -2550,10 +2554,6 @@ EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; - EEC589D92A4F1CE300BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; - EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; - EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; - EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7BE2D2BC6C09400F86835 /* AddressBarKeyboardShortcutsTests.swift */; }; EEC8EB3E2982CA3B0065AA39 /* JSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC111E5294D06290086524F /* JSAlertViewModel.swift */; }; EEC8EB3F2982CA440065AA39 /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; @@ -3382,6 +3382,7 @@ 7B76E6852AD5D77600186A84 /* XPCHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = XPCHelper; sourceTree = ""; }; 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+vpnLegacyUser.swift"; sourceTree = ""; }; 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAppEventsHandler.swift; sourceTree = ""; }; + 7B9167A82C09E88800322310 /* AppLauncher */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AppLauncher; sourceTree = ""; }; 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+NetworkProtectionShared.swift"; sourceTree = ""; }; 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; @@ -4099,7 +4100,6 @@ EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateRestorationTests.swift; sourceTree = ""; }; EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; - 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 = ""; }; @@ -4161,6 +4161,7 @@ 85D44B882BA08D30001B4AB5 /* Suggestions in Frameworks */, 4BF97AD12B43C43F00EB4240 /* NetworkProtectionIPC in Frameworks */, 37F44A5F298C17830025E7FE /* Navigation in Frameworks */, + 7B2366862C09FACD002D393F /* VPNAppLauncher in Frameworks */, 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, @@ -4238,6 +4239,7 @@ 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */, + 7B23668C2C09FAF1002D393F /* VPNAppLauncher in Frameworks */, EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */, EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */, 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */, @@ -4249,6 +4251,7 @@ buildActionMask = 2147483647; files = ( 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */, + 7B23668E2C09FAFA002D393F /* VPNAppLauncher in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, @@ -4264,6 +4267,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7B23668A2C09FAE8002D393F /* VPNAppLauncher in Frameworks */, 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */, 37269F052B3332C2005E8E46 /* Common in Frameworks */, ); @@ -4278,6 +4282,7 @@ EE7295E72A545BBB008C0991 /* NetworkProtection in Frameworks */, F198C7162BD18A44000BF24D /* PixelKit in Frameworks */, 4B4D60AF2A0C837F00BCD287 /* Networking in Frameworks */, + 7B2366882C09FADA002D393F /* VPNAppLauncher in Frameworks */, 7B25856E2BA2F2ED00D49F79 /* NetworkProtectionUI in Frameworks */, 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */, ); @@ -4359,6 +4364,7 @@ 371D00E129D8509400EC8598 /* OpenSSL in Frameworks */, 9FF521462BAA908500B9819B /* Lottie in Frameworks */, 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */, + 7B2366842C09FAC2002D393F /* VPNAppLauncher in Frameworks */, 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */, 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, @@ -4811,6 +4817,7 @@ 378E279C2970217400FCADA2 /* LocalPackages */ = { isa = PBXGroup; children = ( + 7B9167A82C09E88800322310 /* AppLauncher */, 378E279D2970217400FCADA2 /* BuildToolPlugins */, 3192A2702A4C4E330084EA89 /* DataBrokerProtection */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, @@ -8234,7 +8241,6 @@ EEC589D62A4F1B1F00BCD60C /* AppAndExtensionAndAgentTargets */ = { isa = PBXGroup; children = ( - EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */, 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, @@ -8329,6 +8335,7 @@ 537FC71EA5115A983FAF3170 /* Crashes */, F198C7132BD18A30000BF24D /* PixelKit */, F198C71F2BD18D92000BF24D /* SwiftLintTool */, + 7B2366852C09FACD002D393F /* VPNAppLauncher */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8479,6 +8486,7 @@ EE2F9C5A2B90F2FF00D45FC9 /* Subscription */, F198C7192BD18A5B000BF24D /* PixelKit */, BDADBDC82BD2BC2200421B9B /* Lottie */, + 7B23668B2C09FAF1002D393F /* VPNAppLauncher */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8512,6 +8520,7 @@ 4BCBE45B2BA7E18500FC75A1 /* Subscription */, F198C71B2BD18A61000BF24D /* PixelKit */, BDADBDCA2BD2BC2800421B9B /* Lottie */, + 7B23668D2C09FAFA002D393F /* VPNAppLauncher */, ); productName = DuckDuckGoAgentAppStore; productReference = 4B2D06692A13318400DE1F49 /* DuckDuckGo VPN App Store.app */; @@ -8534,6 +8543,7 @@ packageProductDependencies = ( 37269F042B3332C2005E8E46 /* Common */, 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */, + 7B2366892C09FAE8002D393F /* VPNAppLauncher */, ); productName = DuckDuckGoNotifications; productReference = 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */; @@ -8560,6 +8570,7 @@ 7B25856D2BA2F2ED00D49F79 /* NetworkProtectionUI */, DC3F73D49B2D44464AFEFCD8 /* Subscription */, F198C7152BD18A44000BF24D /* PixelKit */, + 7B2366872C09FADA002D393F /* VPNAppLauncher */, ); productName = NetworkProtectionAppExtension; productReference = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; @@ -8726,6 +8737,7 @@ F1DF95E62BD188B60045E591 /* LoginItems */, F198C7112BD18A28000BF24D /* PixelKit */, F198C71D2BD18D88000BF24D /* SwiftLintTool */, + 7B2366832C09FAC2002D393F /* VPNAppLauncher */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -9626,7 +9638,6 @@ 3706FEBF293F6EFF00E42796 /* BWError.swift in Sources */, 3706FAC6293F65D500E42796 /* ConnectBitwardenViewController.swift in Sources */, 1DDC84FC2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, - EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */, 3706FAC8293F65D500E42796 /* AppTrackerDataSetProvider.swift in Sources */, 3706FAC9293F65D500E42796 /* EncryptionKeyGeneration.swift in Sources */, 3706FACA293F65D500E42796 /* TabLazyLoader.swift in Sources */, @@ -10742,7 +10753,6 @@ 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, - EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, @@ -10768,7 +10778,6 @@ F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, - EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -10809,7 +10818,6 @@ 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, - EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10818,7 +10826,6 @@ buildActionMask = 2147483647; files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - 7B25856C2BA2F2D000D49F79 /* AppLauncher.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, @@ -11558,7 +11565,6 @@ 4B8A4DFF27C83B29005F40E8 /* SaveIdentityViewController.swift in Sources */, 1DDC84FB2B8356CE00670238 /* PreferencesDefaultBrowserView.swift in Sources */, F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */, - EEC589D92A4F1CE300BCD60C /* AppLauncher.swift in Sources */, 4BA1A69B258B076900F6F690 /* FileStore.swift in Sources */, 1D01A3D02B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, @@ -12330,6 +12336,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */; buildSettings = { + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSPrincipalClass = Application; }; name = Debug; }; @@ -12337,6 +12346,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */; buildSettings = { + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSPrincipalClass = Application; }; name = CI; }; @@ -12344,6 +12356,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */; buildSettings = { + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSPrincipalClass = Application; }; name = Release; }; @@ -12351,6 +12366,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */; buildSettings = { + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSPrincipalClass = Application; }; name = Review; }; @@ -13293,6 +13311,30 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; }; + 7B2366832C09FAC2002D393F /* VPNAppLauncher */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppLauncher; + }; + 7B2366852C09FACD002D393F /* VPNAppLauncher */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppLauncher; + }; + 7B2366872C09FADA002D393F /* VPNAppLauncher */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppLauncher; + }; + 7B2366892C09FAE8002D393F /* VPNAppLauncher */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppLauncher; + }; + 7B23668B2C09FAF1002D393F /* VPNAppLauncher */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppLauncher; + }; + 7B23668D2C09FAFA002D393F /* VPNAppLauncher */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppLauncher; + }; 7B25856D2BA2F2ED00D49F79 /* NetworkProtectionUI */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 723fbc2123e..ca07ace2e65 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -21,8 +21,8 @@ import Foundation import AppKit import PixelKit import Subscription - import NetworkProtectionUI +import VPNAppLauncher #if DBP import DataBrokerProtection @@ -145,25 +145,25 @@ final class URLEventHandler { private static func handleNetworkProtectionURL(_ url: URL) { DispatchQueue.main.async { switch url { - case AppLaunchCommand.showStatus.launchURL: + case VPNAppLaunchCommand.showStatus.launchURL: Task { await WindowControllersManager.shared.showNetworkProtectionStatus() } - case AppLaunchCommand.showSettings.launchURL: + case VPNAppLaunchCommand.showSettings.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) - case AppLaunchCommand.shareFeedback.launchURL: + case VPNAppLaunchCommand.shareFeedback.launchURL: WindowControllersManager.shared.showShareFeedbackModal() - case AppLaunchCommand.justOpen.launchURL: + case VPNAppLaunchCommand.justOpen.launchURL: WindowControllersManager.shared.showMainWindow() - case AppLaunchCommand.showVPNLocations.launchURL: + case VPNAppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() - case AppLaunchCommand.showPrivacyPro.launchURL: + case VPNAppLaunchCommand.showPrivacyPro.launchURL: let url = Application.appDelegate.subscriptionManager.url(for: .purchase) WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) #if !APPSTORE && !DEBUG - case AppLaunchCommand.moveAppToApplications.launchURL: + case VPNAppLaunchCommand.moveAppToApplications.launchURL: // this should be run after NSApplication.shared is set PFMoveToApplicationsFolderIfNecessary(false) #endif diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index cbcbb3dce45..9f1848bcbed 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -230,7 +230,9 @@ extension DBPHomeViewController { // MARK: - System configuration +import AppLauncher import ServiceManagement +import VPNAppLauncher extension DBPHomeViewController { func openLoginItemSettings() { @@ -246,7 +248,7 @@ extension DBPHomeViewController { func moveToApplicationFolder() { pixelHandler.fire(.homeViewCTAMoveApplicationClicked) Task { @MainActor in - await AppLauncher(appBundleURL: Bundle.main.bundleURL).launchApp(withCommand: .moveAppToApplications) + try? await AppLauncher(appBundleURL: Bundle.main.bundleURL).launchApp(withCommand: VPNAppLaunchCommand.moveAppToApplications) } } } diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift deleted file mode 100644 index 6aecbc6727c..00000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// AppLauncher.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 Common -import NetworkProtectionUI - -extension AppLaunchCommand { - var rawValue: String { - switch self { - case .startVPN: return "startVPN" - case .stopVPN: return "stopVPN" - case .justOpen: return "justOpen" - case .shareFeedback: return "shareFeedback" - case .showFAQ: return "showFAQ" - case .showStatus: return "showStatus" - case .showSettings: return "showSettings" - case .showVPNLocations: return "showVPNLocations" - case .enableOnDemand: return "enableOnDemand" - case .moveAppToApplications: return "moveAppToApplications" - case .showPrivacyPro: return "showPrivacyPro" - } - } -} - -/// Launches the main App -/// -public final class AppLauncher: AppLaunching { - - private let mainBundleURL: URL - - public init(appBundleURL: URL) { - mainBundleURL = appBundleURL - } - - public func launchApp(withCommand command: AppLaunchCommand) async { - let configuration = NSWorkspace.OpenConfiguration() - configuration.allowsRunningApplicationSubstitution = command.allowsRunningApplicationSubstitution - configuration.arguments = [command.rawValue] - - if command.hideApp { - configuration.activates = false - configuration.addsToRecentItems = false - configuration.createsNewApplicationInstance = true - configuration.hides = true - } else { - configuration.activates = true - configuration.addsToRecentItems = true - configuration.createsNewApplicationInstance = false - configuration.hides = false - } - - do { - if let launchURL = command.launchURL { - try await NSWorkspace.shared.open([launchURL], withApplicationAt: mainBundleURL, configuration: configuration) - } else if let helperAppPath = command.helperAppPath { - let launchURL = mainBundleURL.appending(helperAppPath) - try await NSWorkspace.shared.openApplication(at: launchURL, configuration: configuration) - } - } catch { - os_log("🔵 Open Application failed: %{public}@", type: .error, error.localizedDescription) - } - } -} - -extension AppLaunchCommand { - var commandURL: String? { - switch self { - case .justOpen: - return "networkprotection://just-open" - case .shareFeedback: - return "networkprotection://share-feedback" - case .showFAQ: - return "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/" - case .showStatus: - return "networkprotection://show-status" - case .showSettings: - return "networkprotection://show-settings" - case .showVPNLocations: - return "networkprotection://show-settings/locations" - case .moveAppToApplications: - return "networkprotection://move-app-to-applications" - case .showPrivacyPro: - return "networkprotection://show-privacy-pro" - default: - return nil - } - } - - var allowsRunningApplicationSubstitution: Bool { - switch self { - case .showSettings: - return true - default: - return false - } - } - - var helperAppPath: String? { - switch self { - case .startVPN: - return "Contents/Resources/startVPN.app" - case .stopVPN: - return "Contents/Resources/stopVPN.app" - case .enableOnDemand: - return "Contents/Resources/enableOnDemand.app" - default: - return nil - } - } - - public var launchURL: URL? { - guard let commandURL else { - return nil - } - - return URL(string: commandURL)! - } - - var hideApp: Bool { - switch self { - case .startVPN, .stopVPN: - return true - default: - return false - } - } -} - -extension URL { - - func appending(_ path: String) -> URL { - if #available(macOS 13.0, *) { - return appending(path: path) - } else { - return appendingPathComponent(path) - } - } - -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 9b4946913e4..997b03d7b86 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppLauncher import AppKit import Combine import Foundation @@ -24,6 +25,7 @@ import NetworkProtection import NetworkProtectionIPC import NetworkProtectionUI import Subscription +import VPNAppLauncher protocol NetworkProtectionIPCClient { var ipcStatusObserver: ConnectionStatusObserver { get } @@ -80,34 +82,34 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { let popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, - appLauncher: appLauncher, + uiActionHandler: appLauncher, menuItems: { if UserDefaults.netP.networkProtectionOnboardingStatus == .completed { return [ NetworkProtectionStatusView.Model.MenuItem( name: UserText.networkProtectionNavBarStatusMenuVPNSettings, action: { - await appLauncher.launchApp(withCommand: .showSettings) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) }), NetworkProtectionStatusView.Model.MenuItem( name: UserText.networkProtectionNavBarStatusMenuFAQ, action: { - await appLauncher.launchApp(withCommand: .showFAQ) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), NetworkProtectionStatusView.Model.MenuItem( name: UserText.networkProtectionNavBarStatusViewShareFeedback, action: { - await appLauncher.launchApp(withCommand: .shareFeedback) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }) ] } else { return [ NetworkProtectionStatusView.Model.MenuItem( name: UserText.networkProtectionNavBarStatusMenuFAQ, action: { - await appLauncher.launchApp(withCommand: .showFAQ) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), NetworkProtectionStatusView.Model.MenuItem( name: UserText.networkProtectionNavBarStatusViewShareFeedback, action: { - await appLauncher.launchApp(withCommand: .shareFeedback) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }) ] } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index 3977fccef59..a2c16349017 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -16,10 +16,12 @@ // limitations under the License. // +import AppLauncher import Foundation import UserNotifications import NetworkProtection import NetworkProtectionUI +import VPNAppLauncher extension UNNotificationAction { @@ -182,10 +184,10 @@ extension NetworkProtectionUNNotificationsPresenter: UNUserNotificationCenterDel func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { switch UNNotificationAction.Identifier(rawValue: response.actionIdentifier) { case .reconnect: - await appLauncher.launchApp(withCommand: .startVPN) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showStatus) case .none: - await appLauncher.launchApp(withCommand: .showStatus) + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showStatus) } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift index aed8d26cf84..f25bdafa7bc 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppLauncher import Foundation import NetworkProtection diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index 41677dc8f13..a6ab84eee85 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -16,11 +16,13 @@ // limitations under the License. // +import AppLauncher import Cocoa import Combine import Common import NetworkExtension import NetworkProtection +import VPNAppLauncher @objc(Application) final class DuckDuckGoNotificationsApplication: NSApplication { diff --git a/DuckDuckGoVPN/AppLauncher+DefaultInitializer.swift b/DuckDuckGoVPN/AppLauncher+DefaultInitializer.swift index 08523cd4861..b48710a2fb3 100644 --- a/DuckDuckGoVPN/AppLauncher+DefaultInitializer.swift +++ b/DuckDuckGoVPN/AppLauncher+DefaultInitializer.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppLauncher import Foundation /// Includes a convenience default initializer for `AppLauncher` that's specific to this App target. diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 406272e7330..1b5a3610515 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppLauncher import Cocoa import Combine import Common @@ -28,6 +29,7 @@ import NetworkProtectionUI import ServiceManagement import PixelKit import Subscription +import VPNAppLauncher @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { @@ -314,20 +316,20 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { statusReporter: statusReporter, controller: tunnelController, iconProvider: iconProvider, - appLauncher: appLauncher, + uiActionHandler: appLauncher, menuItems: { [ StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuVPNSettings, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .showSettings) + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showSettings) }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .showFAQ) + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .shareFeedback) + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .justOpen) + try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.justOpen) }), ] }, diff --git a/LocalPackages/AppLauncher/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LocalPackages/AppLauncher/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000000..18d981003d6 --- /dev/null +++ b/LocalPackages/AppLauncher/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/LocalPackages/AppLauncher/Package.swift b/LocalPackages/AppLauncher/Package.swift new file mode 100644 index 00000000000..c93c3889030 --- /dev/null +++ b/LocalPackages/AppLauncher/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AppLauncher", + platforms: [ .macOS("11.4") ], + products: [ + .library( + name: "AppLauncher", + targets: ["AppLauncher"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "AppLauncher", + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ]), + .testTarget( + name: "AppLauncherTests", + dependencies: ["AppLauncher"]), + ] +) diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/AppLaunching/MockAppLauncher.swift b/LocalPackages/AppLauncher/Sources/AppLauncher/AppLaunchCommand.swift similarity index 62% rename from LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/AppLaunching/MockAppLauncher.swift rename to LocalPackages/AppLauncher/Sources/AppLauncher/AppLaunchCommand.swift index 07f0d58fd8a..83a89df004f 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/AppLaunching/MockAppLauncher.swift +++ b/LocalPackages/AppLauncher/Sources/AppLauncher/AppLaunchCommand.swift @@ -1,7 +1,7 @@ // -// MockAppLauncher.swift +// AppLaunchCommand.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -17,14 +17,9 @@ // import Foundation -import NetworkProtectionUI -public final class MockAppLauncher: AppLaunching { - public init() { - } - - public var spyLaunchAppCommand: AppLaunchCommand? - public func launchApp(withCommand command: AppLaunchCommand) async { - spyLaunchAppCommand = command - } +public protocol AppLaunchCommand { + var allowsRunningApplicationSubstitution: Bool { get } + var launchURL: URL? { get } + var hideApp: Bool { get } } diff --git a/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift b/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift new file mode 100644 index 00000000000..c09cb039409 --- /dev/null +++ b/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift @@ -0,0 +1,77 @@ +// +// AppLauncher.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 + +public protocol AppLaunching { + func launchApp(withCommand command: AppLaunchCommand) async throws +} + +/// Launches the main App +/// +public final class AppLauncher: AppLaunching { + + public enum AppLaunchError: CustomNSError { + case workspaceOpenError(_ error: Error) + + public var errorCode: Int { + switch self { + case .workspaceOpenError: return 0 + } + } + + public var errorUserInfo: [String: Any] { + switch self { + case .workspaceOpenError(let error): + return [NSUnderlyingErrorKey: error as NSError] + } + } + } + + private let mainBundleURL: URL + + public init(appBundleURL: URL) { + mainBundleURL = appBundleURL + } + + public func launchApp(withCommand command: AppLaunchCommand) async throws { + let configuration = NSWorkspace.OpenConfiguration() + configuration.allowsRunningApplicationSubstitution = command.allowsRunningApplicationSubstitution + + if command.hideApp { + configuration.activates = false + configuration.addsToRecentItems = false + configuration.createsNewApplicationInstance = true + configuration.hides = true + } else { + configuration.activates = true + configuration.addsToRecentItems = true + configuration.createsNewApplicationInstance = false + configuration.hides = false + } + + do { + if let launchURL = command.launchURL { + try await NSWorkspace.shared.open([launchURL], withApplicationAt: mainBundleURL, configuration: configuration) + } + } catch { + throw AppLaunchError.workspaceOpenError(error) + } + } +} diff --git a/LocalPackages/AppLauncher/Tests/AppLauncherTests/AppLauncherTests.swift b/LocalPackages/AppLauncher/Tests/AppLauncherTests/AppLauncherTests.swift new file mode 100644 index 00000000000..720ecd71cec --- /dev/null +++ b/LocalPackages/AppLauncher/Tests/AppLauncherTests/AppLauncherTests.swift @@ -0,0 +1,24 @@ +// +// AppLauncherTests.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 AppLauncher + +final class AppLauncherTests: XCTestCase { + // TBD +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index eb18be1f3aa..ac278804075 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -29,10 +29,12 @@ let package = Package( .library(name: "NetworkProtectionIPC", targets: ["NetworkProtectionIPC"]), .library(name: "NetworkProtectionProxy", targets: ["NetworkProtectionProxy"]), .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), + .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), + .package(path: "../AppLauncher"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), @@ -65,6 +67,21 @@ let package = Package( ] ), + // MARK: - VPNAppLauncher + + .target( + name: "VPNAppLauncher", + dependencies: [ + "NetworkProtectionUI", + .product(name: "AppLauncher", package: "AppLauncher"), + .product(name: "NetworkProtection", package: "BrowserServicesKit"), + .product(name: "PixelKit", package: "BrowserServicesKit"), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), + // MARK: - NetworkProtectionUI .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 5e02caa125e..9d541e976cb 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -37,7 +37,7 @@ public final class StatusBarMenu: NSObject { private let controller: TunnelController private let statusReporter: NetworkProtectionStatusReporter private let onboardingStatusPublisher: OnboardingStatusPublisher - private let appLauncher: AppLaunching + private let uiActionHandler: VPNUIActionHandler private let menuItems: () -> [MenuItem] private let agentLoginItem: LoginItem? private let isMenuBarStatusView: Bool @@ -64,7 +64,7 @@ public final class StatusBarMenu: NSObject { statusReporter: NetworkProtectionStatusReporter, controller: TunnelController, iconProvider: IconProvider, - appLauncher: AppLaunching, + uiActionHandler: VPNUIActionHandler, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, @@ -80,7 +80,7 @@ public final class StatusBarMenu: NSObject { self.controller = controller self.statusReporter = statusReporter self.onboardingStatusPublisher = onboardingStatusPublisher - self.appLauncher = appLauncher + self.uiActionHandler = uiActionHandler self.menuItems = menuItems self.agentLoginItem = agentLoginItem self.isMenuBarStatusView = isMenuBarStatusView @@ -134,7 +134,7 @@ public final class StatusBarMenu: NSObject { popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, - appLauncher: appLauncher, + uiActionHandler: uiActionHandler, menuItems: menuItems, agentLoginItem: agentLoginItem, isMenuBarStatusView: isMenuBarStatusView, diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index 4236e963aa9..0d1837d6e89 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -53,7 +53,7 @@ public final class NetworkProtectionPopover: NSPopover { public required init(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, - appLauncher: AppLaunching, + uiActionHandler: VPNUIActionHandler, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, @@ -66,7 +66,7 @@ public final class NetworkProtectionPopover: NSPopover { onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, debugInformationPublisher: debugInformationPublisher.eraseToAnyPublisher(), - appLauncher: appLauncher, + uiActionHandler: uiActionHandler, menuItems: menuItems, agentLoginItem: agentLoginItem, isMenuBarStatusView: isMenuBarStatusView, diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/VPNUIActionHandler/VPNUIActionHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/VPNUIActionHandler/VPNUIActionHandler.swift new file mode 100644 index 00000000000..76ff99d563c --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/VPNUIActionHandler/VPNUIActionHandler.swift @@ -0,0 +1,26 @@ +// +// VPNUIActionHandler.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 VPNUIActionHandler { + func moveAppToApplications() async + func shareFeedback() async + func showPrivacyPro() async + func showVPNLocations() async +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 5e6fc27f86b..8c126a81422 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -95,7 +95,7 @@ extension NetworkProtectionStatusView { /// private let runLoopMode: RunLoop.Mode? - private let appLauncher: AppLaunching + private let uiActionHandler: VPNUIActionHandler private let uninstallHandler: () async -> Void @@ -115,7 +115,7 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, debugInformationPublisher: AnyPublisher, - appLauncher: AppLaunching, + uiActionHandler: VPNUIActionHandler, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, @@ -132,7 +132,7 @@ extension NetworkProtectionStatusView { self.agentLoginItem = agentLoginItem self.isMenuBarStatusView = isMenuBarStatusView self.runLoopMode = runLoopMode - self.appLauncher = appLauncher + self.uiActionHandler = uiActionHandler self.uninstallHandler = uninstallHandler tunnelControllerViewModel = TunnelControllerViewModel(controller: tunnelController, @@ -140,7 +140,7 @@ extension NetworkProtectionStatusView { statusReporter: statusReporter, vpnSettings: .init(defaults: userDefaults), locationFormatter: locationFormatter, - appLauncher: appLauncher) + uiActionHandler: uiActionHandler) connectionStatus = statusReporter.statusObserver.recentValue isHavingConnectivityIssues = statusReporter.connectivityIssuesObserver.recentValue @@ -187,13 +187,13 @@ extension NetworkProtectionStatusView { func openPrivacyPro() { Task { - await appLauncher.launchApp(withCommand: .showPrivacyPro) + await uiActionHandler.showPrivacyPro() } } func openFeedbackForm() { Task { - await appLauncher.launchApp(withCommand: .shareFeedback) + await uiActionHandler.shareFeedback() } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 5c63ecea7ea..22e8d546b97 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -67,7 +67,7 @@ public final class TunnelControllerViewModel: ObservableObject { return formatter }() - private let appLauncher: AppLaunching + private let uiActionHandler: VPNUIActionHandler // MARK: - Misc @@ -91,7 +91,7 @@ public final class TunnelControllerViewModel: ObservableObject { runLoopMode: RunLoop.Mode? = nil, vpnSettings: VPNSettings, locationFormatter: VPNLocationFormatting, - appLauncher: AppLaunching) { + uiActionHandler: VPNUIActionHandler) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher @@ -99,7 +99,7 @@ public final class TunnelControllerViewModel: ObservableObject { self.runLoopMode = runLoopMode self.vpnSettings = vpnSettings self.locationFormatter = locationFormatter - self.appLauncher = appLauncher + self.uiActionHandler = uiActionHandler connectionStatus = statusReporter.statusObserver.recentValue formattedDataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) @@ -520,13 +520,13 @@ public final class TunnelControllerViewModel: ObservableObject { func showLocationSettings() { Task { @MainActor in - await appLauncher.launchApp(withCommand: .showVPNLocations) + await uiActionHandler.showVPNLocations() } } func moveToApplications() { Task { @MainActor in - await appLauncher.launchApp(withCommand: .moveAppToApplications) + await uiActionHandler.moveAppToApplications() } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/AppLauncher+VPNUIActionHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/AppLauncher+VPNUIActionHandler.swift new file mode 100644 index 00000000000..ed491f743b2 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/AppLauncher+VPNUIActionHandler.swift @@ -0,0 +1,40 @@ +// +// AppLauncher+VPNUIActionHandler.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 AppLauncher +import Foundation +import NetworkProtectionUI + +extension AppLauncher: VPNUIActionHandler { + + public func moveAppToApplications() async { + try? await launchApp(withCommand: VPNAppLaunchCommand.moveAppToApplications) + } + + public func shareFeedback() async { + try? await launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) + } + + public func showVPNLocations() async { + try? await launchApp(withCommand: VPNAppLaunchCommand.showVPNLocations) + } + + public func showPrivacyPro() async { + try? await launchApp(withCommand: VPNAppLaunchCommand.showPrivacyPro) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift new file mode 100644 index 00000000000..81e01787e64 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNAppLauncher/VPNAppLaunchCommand.swift @@ -0,0 +1,76 @@ +// +// VPNAppLaunchCommand.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 AppLauncher +import Foundation + +public enum VPNAppLaunchCommand: Codable, AppLaunchCommand { + case justOpen + case shareFeedback + case showFAQ + case showStatus + case showSettings + case showVPNLocations + case moveAppToApplications + case showPrivacyPro + + var commandURL: String? { + switch self { + case .justOpen: + return "networkprotection://just-open" + case .shareFeedback: + return "networkprotection://share-feedback" + case .showFAQ: + return "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/" + case .showStatus: + return "networkprotection://show-status" + case .showSettings: + return "networkprotection://show-settings" + case .showVPNLocations: + return "networkprotection://show-settings/locations" + case .moveAppToApplications: + return "networkprotection://move-app-to-applications" + case .showPrivacyPro: + return "networkprotection://show-privacy-pro" + } + } + + public var allowsRunningApplicationSubstitution: Bool { + switch self { + case .showSettings: + return true + default: + return false + } + } + + public var launchURL: URL? { + guard let commandURL else { + return nil + } + + return URL(string: commandURL)! + } + + public var hideApp: Bool { + switch self { + default: + return false + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNUIActionHandler/MockVPNUIActionHandler.swift similarity index 52% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift rename to LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNUIActionHandler/MockVPNUIActionHandler.swift index e64f0a605a4..5e0240ca3db 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/MockVPNUIActionHandler/MockVPNUIActionHandler.swift @@ -1,7 +1,7 @@ // -// AppLaunching.swift +// MockVPNUIActionHandler.swift // -// Copyright © 2022 DuckDuckGo. All rights reserved. +// 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. @@ -16,25 +16,24 @@ // limitations under the License. // -// SPDX-License-Identifier: MIT -// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved. - import Foundation +import NetworkProtectionUI -public enum AppLaunchCommand: Codable { - case justOpen - case shareFeedback - case showFAQ - case showStatus - case showSettings - case showVPNLocations - case startVPN - case stopVPN - case enableOnDemand - case moveAppToApplications - case showPrivacyPro -} +public final class MockVPNUIActionHandler: VPNUIActionHandler { + + public func moveAppToApplications() async { + // placeholder + } + + public func shareFeedback() async { + // placeholder + } + + public func showVPNLocations() async { + // placeholder + } -public protocol AppLaunching { - func launchApp(withCommand command: AppLaunchCommand) async + public func showPrivacyPro() async { + // placeholder + } } diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index 967f92c9957..22ea5ffbc63 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -53,7 +53,7 @@ final class StatusBarMenuTests: XCTestCase { statusReporter: MockNetworkProtectionStatusReporter(), controller: TestTunnelController(), iconProvider: MenuIconProvider(), - appLauncher: MockAppLauncher(), + uiActionHandler: MockVPNUIActionHandler(), menuItems: { [] }, agentLoginItem: nil, isMenuBarStatusView: false, @@ -79,7 +79,7 @@ final class StatusBarMenuTests: XCTestCase { statusReporter: MockNetworkProtectionStatusReporter(), controller: TestTunnelController(), iconProvider: MenuIconProvider(), - appLauncher: MockAppLauncher(), + uiActionHandler: MockVPNUIActionHandler(), menuItems: { [] }, agentLoginItem: nil, isMenuBarStatusView: false, diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index b6853541fb6..4f9407e5cc2 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -118,7 +118,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) let isToggleOn = model.isToggleOn.wrappedValue XCTAssertFalse(isToggleOn) @@ -140,7 +140,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) XCTAssertEqual(model.connectionStatusDescription, UserText.networkProtectionStatusDisconnecting) XCTAssertEqual(model.timeLapsed, UserText.networkProtectionStatusViewTimerZero) @@ -169,7 +169,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) let isToggleOn = model.isToggleOn.wrappedValue XCTAssertTrue(isToggleOn) @@ -193,7 +193,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) XCTAssertEqual(model.connectionStatusDescription, UserText.networkProtectionStatusConnecting) XCTAssertEqual(model.timeLapsed, UserText.networkProtectionStatusViewTimerZero) @@ -214,7 +214,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) XCTAssertEqual(model.formattedDataVolume, .init(dataSent: "512 KB", dataReceived: "1 MB")) } @@ -231,7 +231,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) let networkProtectionWasStarted = expectation(description: "The model started the VPN when appropriate") controller.startCallback = { @@ -263,7 +263,7 @@ final class TunnelControllerViewModelTests: XCTestCase { statusReporter: statusReporter, vpnSettings: .init(defaults: .standard), locationFormatter: MockVPNLocationFormatter(), - appLauncher: MockAppLauncher()) + uiActionHandler: MockVPNUIActionHandler()) let networkProtectionWasStopped = expectation(description: "The model stopped the VPN when appropriate") From c1b767f780ea0980042d4f786a9c1bda76cf071b Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 5 Jun 2024 17:58:22 -0700 Subject: [PATCH 14/35] VPN quick wins (#2818) Task/Issue URL: https://app.asana.com/0/1203137811378537/1207380189901328/f Tech Design URL: CC: Description: This PR implements four quick wins: The VPN pane in settings has been moved into the same section as Privacy Pro, and given an on/off indicator Added a new setting to the VPN pane to toggling pinning for the VPN icon Updated the "Connect on Login" copy Updated the "Frequently Asked Questions" copy --- .../UserText+NetworkProtection.swift | 13 ++++-- .../Model/PreferencesSection.swift | 15 ++++--- .../Model/PreferencesSidebarModel.swift | 28 ++++++++++++- .../Model/VPNPreferencesModel.swift | 33 +++++++++++++++ .../View/PreferencesRootView.swift | 2 +- .../Preferences/View/PreferencesSidebar.swift | 7 ++-- .../Preferences/View/PreferencesVPNView.swift | 41 ++++++++++++++++--- DuckDuckGoVPN/UserText.swift | 2 +- 8 files changed, 119 insertions(+), 22 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 27cb8280254..239134a2af9 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -47,7 +47,7 @@ extension UserText { // "network.protection.status.menu.vpn.settings" - The status menu 'VPN Settings' menu item static let networkProtectionNavBarStatusMenuVPNSettings = "VPN Settings…" // "network.protection.status.menu.faq" - The status menu 'FAQ' menu item - static let networkProtectionNavBarStatusMenuFAQ = "Frequently Asked Questions…" + static let networkProtectionNavBarStatusMenuFAQ = "FAQs and Support…" // MARK: - System Extension Installation Messages // "network.protection.configuration.system-settings.legacy" - Text for a label in the VPN popover, displayed after attempting to enable the VPN for the first time while using macOS 12 and below @@ -126,6 +126,8 @@ extension UserText { static let vpnLocationTitle = "Location" // "vpn.general.title" - General section title in VPN settings static let vpnGeneralTitle = "General" + // "vpn.shortcuts.settings.title" - Shortcuts section title in VPN settings + static let vpnShortcutsSettingsTitle = "Shortcuts" // "vpn.notifications.settings.title" - Notifications section title in VPN settings static let vpnNotificationsSettingsTitle = "Notifications" // "vpn.advanced.settings.title" - VPN Advanced section title in VPN settings @@ -159,9 +161,11 @@ extension UserText { // MARK: - Settings // "vpn.setting.title.connect.on.login" - Connect on Login setting title - static let vpnConnectOnLoginSettingTitle = "Connect on login" - // "vpn.setting.title.connect.on.login" - Display VPN status in the menu bar. + static let vpnConnectOnLoginSettingTitle = "Connect to VPN when logging in to your computer" + // "vpn.setting.title.show.in.menu.bar" - Display VPN status in the menu bar static let vpnShowInMenuBarSettingTitle = "Show VPN in menu bar" + // "vpn.setting.title.show.in.browser.toolbar" - Display VPN status in the browser toolbar + static let vpnShowInBrowserToolbarSettingTitle = "Show VPN in browser toolbar" // "vpn.setting.description.always.on" - Always ON setting description static let vpnAlwaysOnSettingDescription = "Automatically restores the VPN connection after interruption. For your security, this setting cannot be disabled." // "vpn.setting.title.exclude.local.networks" - Exclude Local Networks setting title @@ -171,7 +175,8 @@ extension UserText { // "vpn.setting.description.secure.dns" - Secure DNS setting description static let vpnSecureDNSSettingDescription = "Our VPN uses Secure DNS to keep your online activity private, so that your Internet provider can't see what websites you visit." // "vpn.button.title.uninstall.vpn" - Uninstall VPN button title - static let uninstallVPNButtonTitle = "Uninstall DuckDuckGo VPN..." + static let openVPNButtonTitle = "Open VPN…" + static let uninstallVPNButtonTitle = "Uninstall DuckDuckGo VPN…" // MARK: - VPN Settings Alerts // "vpn.uninstall.alert.title" - Alert title when the user selects to uninstall our VPN diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 7a69e108680..e94ba7a488a 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -27,11 +27,9 @@ struct PreferencesSection: Hashable, Identifiable { @MainActor static func defaultSections(includingDuckPlayer: Bool, includingSync: Bool, includingVPN: Bool) -> [PreferencesSection] { - var privacyPanes: [PreferencePaneIdentifier] = [.defaultBrowser, .privateSearch, .webTrackingProtection, .cookiePopupProtection, .emailProtection] - - if includingVPN { - privacyPanes.append(.vpn) - } + let privacyPanes: [PreferencePaneIdentifier] = [ + .defaultBrowser, .privateSearch, .webTrackingProtection, .cookiePopupProtection, .emailProtection + ] let regularPanes: [PreferencePaneIdentifier] = { var panes: [PreferencePaneIdentifier] = [.general, .appearance, .autofill, .accessibility, .dataClearing] @@ -70,7 +68,12 @@ struct PreferencesSection: Hashable, Identifiable { } if !shouldHidePrivacyProDueToNoProducts { - let subscriptionPanes: [PreferencePaneIdentifier] = [.subscription] + var subscriptionPanes: [PreferencePaneIdentifier] = [.subscription] + + if includingVPN { + subscriptionPanes.append(.vpn) + } + sections.insert(.init(id: .privacyPro, panes: subscriptionPanes), at: 1) } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index db5a483437f..8c8520ea2f4 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -21,6 +21,7 @@ import Combine import DDGSync import SwiftUI import Subscription +import NetworkProtectionIPC final class PreferencesSidebarModel: ObservableObject { @@ -30,6 +31,7 @@ final class PreferencesSidebarModel: ObservableObject { @Published var selectedTabIndex: Int = 0 @Published private(set) var selectedPane: PreferencePaneIdentifier = .defaultBrowser private let vpnVisibility: NetworkProtectionFeatureVisibility + let vpnTunnelIPCClient: TunnelControllerIPCClient var selectedTabContent: AnyPublisher { $selectedTabIndex.map { [tabSwitcherTabs] in tabSwitcherTabs[$0] }.eraseToAnyPublisher() @@ -42,11 +44,13 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent], privacyConfigurationManager: PrivacyConfigurationManaging, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager) + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + vpnTunnelIPCClient: TunnelControllerIPCClient = TunnelControllerIPCClient() ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs self.vpnVisibility = vpnVisibility + self.vpnTunnelIPCClient = vpnTunnelIPCClient resetTabSelectionIfNeeded() refreshSections() @@ -111,6 +115,28 @@ final class PreferencesSidebarModel: ObservableObject { .store(in: &cancellables) } + func vpnProtectionStatus() -> PrivacyProtectionStatus { + let recentConnectionStatus = vpnTunnelIPCClient.connectionStatusObserver.recentValue + let initialValue: Bool + + if case .connected = recentConnectionStatus { + initialValue = true + } else { + initialValue = false + } + + return PrivacyProtectionStatus( + statusPublisher: vpnTunnelIPCClient.connectionStatusObserver.publisher.receive(on: RunLoop.main), + initialValue: initialValue ? .on : .off + ) { newStatus in + if case .connected = newStatus { + return .on + } else { + return .off + } + } + } + // MARK: - Refreshing logic func refreshSections() { diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index b5e8273c17c..4d14f35440c 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -49,6 +49,16 @@ final class VPNPreferencesModel: ObservableObject { } } + @Published var showInBrowserToolbar: Bool { + didSet { + if showInBrowserToolbar { + pinningManager.pin(.networkProtection) + } else { + pinningManager.unpin(.networkProtection) + } + } + } + @Published var notifyStatusChanges: Bool { didSet { settings.notifyStatusChanges = notifyStatusChanges @@ -64,22 +74,27 @@ final class VPNPreferencesModel: ObservableObject { } private let settings: VPNSettings + private let pinningManager: PinningManager private var cancellables = Set() init(settings: VPNSettings = .init(defaults: .netP), + pinningManager: PinningManager = LocalPinningManager.shared, defaults: UserDefaults = .netP) { self.settings = settings + self.pinningManager = pinningManager connectOnLogin = settings.connectOnLogin excludeLocalNetworks = settings.excludeLocalNetworks notifyStatusChanges = settings.notifyStatusChanges showInMenuBar = settings.showInMenuBar + showInBrowserToolbar = pinningManager.isPinned(.networkProtection) showUninstallVPN = defaults.networkProtectionOnboardingStatus != .default onboardingStatus = defaults.networkProtectionOnboardingStatus locationItem = VPNLocationPreferenceItemModel(selectedLocation: settings.selectedLocation) subscribeToOnboardingStatusChanges(defaults: defaults) subscribeToShowInMenuBarSettingChanges() + subscribeToShowInBrowserToolbarSettingsChanges() subscribeToLocationSettingChanges() } @@ -96,6 +111,24 @@ final class VPNPreferencesModel: ObservableObject { .store(in: &cancellables) } + func subscribeToShowInBrowserToolbarSettingsChanges() { + NotificationCenter.default.publisher(for: .PinnedViewsChanged).sink { [weak self] notification in + guard let self = self else { + return + } + + if let userInfo = notification.userInfo as? [String: Any], + let viewType = userInfo[LocalPinningManager.pinnedViewChangedNotificationViewTypeKey] as? String, + let view = PinnableView(rawValue: viewType) { + switch view { + case .networkProtection: self.showInBrowserToolbar = self.pinningManager.isPinned(.networkProtection) + default: break + } + } + } + .store(in: &cancellables) + } + func subscribeToLocationSettingChanges() { settings.selectedLocationPublisher .map(VPNLocationPreferenceItemModel.init(selectedLocation:)) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 55833aea85b..4940baa7c16 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -98,7 +98,7 @@ enum Preferences { case .dataClearing: DataClearingView(model: DataClearingPreferences.shared) case .vpn: - VPNView(model: VPNPreferencesModel()) + VPNView(model: VPNPreferencesModel(), status: model.vpnProtectionStatus()) case .subscription: SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) case .autofill: diff --git a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift index 1b234a1c49f..8dfd85c016e 100644 --- a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift +++ b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift @@ -45,11 +45,11 @@ extension Preferences { let action: () -> Void @ObservedObject var protectionStatus: PrivacyProtectionStatus - init(pane: PreferencePaneIdentifier, isSelected: Bool, action: @escaping () -> Void) { + init(pane: PreferencePaneIdentifier, isSelected: Bool, status: PrivacyProtectionStatus? = nil, action: @escaping () -> Void) { self.pane = pane self.isSelected = isSelected self.action = action - self.protectionStatus = PrivacyProtectionStatus.status(for: pane) + self.protectionStatus = status ?? PrivacyProtectionStatus.status(for: pane) } var body: some View { @@ -180,7 +180,8 @@ extension Preferences { private func sidebarSection(_ section: PreferencesSection) -> some View { ForEach(section.panes) { pane in PaneSidebarItem(pane: pane, - isSelected: model.selectedPane == pane) { + isSelected: model.selectedPane == pane, + status: pane == .vpn ? model.vpnProtectionStatus() : nil) { model.selectPane(pane) } } diff --git a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift index 5cce49df312..b4f9e34170f 100644 --- a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift @@ -24,14 +24,33 @@ extension Preferences { struct VPNView: View { @ObservedObject var model: VPNPreferencesModel + @ObservedObject var status: PrivacyProtectionStatus var body: some View { - PreferencePane(UserText.vpn) { + PreferencePane(UserText.vpn, spacing: 4) { + + if let status = status.status { + PreferencePaneSection { + StatusIndicatorView(status: status, isLarge: true) + } + } + + PreferencePaneSection { + Button(UserText.openVPNButtonTitle) { + Task { @MainActor in + NotificationCenter.default.post(name: .ToggleNetworkProtectionInMainWindow, object: nil) + } + } + } + .padding(.bottom, 12) + + // SECTION: Location PreferencePaneSection { TextMenuItemHeader(UserText.vpnLocationTitle) VPNLocationPreferenceItem(model: model.locationItem) } + .padding(.bottom, 12) // SECTION: Manage VPN @@ -41,10 +60,6 @@ extension Preferences { ToggleMenuItem(UserText.vpnConnectOnLoginSettingTitle, isOn: $model.connectOnLogin) } - SpacedCheckbox { - ToggleMenuItem(UserText.vpnShowInMenuBarSettingTitle, isOn: $model.showInMenuBar) - } - SpacedCheckbox { ToggleMenuItemWithDescription( UserText.vpnExcludeLocalNetworksSettingTitle, @@ -80,13 +95,27 @@ extension Preferences { .background(Color(.blackWhite1)) .roundedBorder() } + .padding(.bottom, 12) + + // SECTION: Shortcuts + + PreferencePaneSection(UserText.vpnShortcutsSettingsTitle) { + SpacedCheckbox { + ToggleMenuItem(UserText.vpnShowInMenuBarSettingTitle, isOn: $model.showInMenuBar) + } + + SpacedCheckbox { + ToggleMenuItem(UserText.vpnShowInBrowserToolbarSettingTitle, isOn: $model.showInBrowserToolbar) + } + } + .padding(.bottom, 12) // SECTION: VPN Notifications PreferencePaneSection(UserText.vpnNotificationsSettingsTitle) { - ToggleMenuItem("VPN connection drops or status changes", isOn: $model.notifyStatusChanges) } + .padding(.bottom, 12) // SECTION: Uninstall diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 4c64c664ebe..e7fbe48d86b 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -22,7 +22,7 @@ final class UserText { // MARK: - Status Menu static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") - static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "Frequently Asked Questions…", comment: "The status menu 'FAQ' menu item") + static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "FAQs and Support…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") } From 5c329d85a8f60be3a9ccddbc5df946a08bffd449 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 6 Jun 2024 16:17:57 +0200 Subject: [PATCH 15/35] Notify Asana about failed UI tests workflows runs (#2839) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207501370511926/f Description: Create a task in macOS App Development project for every scheduled workflow run of UI tests or Sync E2E tests, that fails or times out. --- .github/workflows/sync_end_to_end.yml | 28 ++++++++++++++++----------- .github/workflows/ui_tests.yml | 25 +++++++++++++++--------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.github/workflows/sync_end_to_end.yml b/.github/workflows/sync_end_to_end.yml index 31f305bef35..28a4f5aa998 100644 --- a/.github/workflows/sync_end_to_end.yml +++ b/.github/workflows/sync_end_to_end.yml @@ -1,4 +1,4 @@ -name: Sync-End-to-End tests +name: Sync End-to-End tests on: workflow_dispatch: @@ -7,7 +7,7 @@ on: jobs: sync-end-to-end-tests: - name: Sync End to end Tests + name: Sync End-to-End Tests runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -95,15 +95,6 @@ jobs: | tee -a xcodebuild.log \ | tee ui-tests.log - # - name: Create Asana task when workflow failed - # if: ${{ failure() }} - # run: | - # curl -s "https://app.asana.com/api/1.0/tasks" \ - # --header "Accept: application/json" \ - # --header "Authorization: Bearer ${{ secrets.ASANA_ACCESS_TOKEN }}" \ - # --header "Content-Type: application/json" \ - # --data ' { "data": { "name": "GH Workflow Failure - Sync End to end tests", "projects": [ "${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }' - - name: Prepare test report if: always() run: | @@ -127,3 +118,18 @@ jobs: ~/Library/Logs/DiagnosticReports/* retention-days: 7 + notify-failure: + name: Notify on failure + if: ${{ always() && github.event_name == 'schedule' && (needs.sync-end-to-end-tests.result == 'failure' || needs.sync-end-to-end-tests.result == 'cancelled') }} + needs: [sync-end-to-end-tests] + runs-on: ubuntu-latest + + steps: + - name: Create Asana task when workflow failed + uses: duckduckgo/native-github-asana-sync@v1.1 + with: + action: create-asana-task + asana-pat: ${{ secrets.ASANA_ACCESS_TOKEN }} + asana-project: ${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }} + asana-task-name: GH Workflow Failure - Sync End-to-End Tests + asana-task-description: The Sync end-to-end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml index ab68232079a..0179aa8ef08 100644 --- a/.github/workflows/ui_tests.yml +++ b/.github/workflows/ui_tests.yml @@ -86,15 +86,6 @@ jobs: | tee -a xcodebuild.log \ | tee ui-tests.log - # - name: Create Asana task when workflow failed - # if: ${{ failure() }} && github.ref == 'refs/heads/main' - # run: | - # curl -s "https://app.asana.com/api/1.0/tasks" \ - # --header "Accept: application/json" \ - # --header "Authorization: Bearer ${{ secrets.ASANA_ACCESS_TOKEN }}" \ - # --header "Content-Type: application/json" \ - # --data ' { "data": { "name": "GH Workflow Failure - UI Tests", "projects": [ "${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }' - - name: Prepare test report if: always() run: | @@ -117,3 +108,19 @@ jobs: DerivedData/Logs/Test/*.xcresult ~/Library/Logs/DiagnosticReports/* retention-days: 7 + + notify-failure: + name: Notify on failure + if: ${{ always() && github.event_name == 'schedule' && (needs.ui-tests.result == 'failure' || needs.ui-tests.result == 'cancelled') }} + needs: [ui-tests] + runs-on: ubuntu-latest + + steps: + - name: Create Asana task when workflow failed + uses: duckduckgo/native-github-asana-sync@v1.1 + with: + action: create-asana-task + asana-pat: ${{ secrets.ASANA_ACCESS_TOKEN }} + asana-project: ${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }} + asana-task-name: GH Workflow Failure - UI Tests + asana-task-description: The UI Tests workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} From 3c7d0cc22981f21cb47668a70bcb015103e9e689 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 7 Jun 2024 16:40:59 +0200 Subject: [PATCH 16/35] Fix running Autofill-related UI tests in CI (#2843) Task/Issue URL: https://app.asana.com/0/1203301625297703/1207512391509002/f Description: This change fixes keychain problems in CI that were affecting UI tests runs, removes all the workarounds in the app code related to UI Tests run type and brings back logins checking in Sync end to end tests. --- .../install-certs-and-profiles/action.yml | 1 + DuckDuckGo/Application/AppDelegate.swift | 4 ++-- DuckDuckGo/Common/Database/Database.swift | 19 ++++++++----------- .../Extensions/NSApplicationExtension.swift | 15 +++++---------- .../DeviceAuthenticator.swift | 2 +- .../Preferences/Model/SyncPreferences.swift | 2 +- .../PasswordManagementViewController.swift | 2 +- DuckDuckGo/Sync/SyncDataProviders.swift | 13 +++++-------- SyncE2EUITests/CriticalPathsTests.swift | 16 ++++------------ 9 files changed, 28 insertions(+), 46 deletions(-) diff --git a/.github/actions/install-certs-and-profiles/action.yml b/.github/actions/install-certs-and-profiles/action.yml index 923bbcd9799..9240aae4ff3 100644 --- a/.github/actions/install-certs-and-profiles/action.yml +++ b/.github/actions/install-certs-and-profiles/action.yml @@ -91,6 +91,7 @@ runs: # import certificate to keychain security import $CERTIFICATE_PATH -P "${{ inputs.P12_PASSWORD }}" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH + security default-keychain -s $RUNNER_TEMP/app-signing.keychain-db # apply provisioning profile mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 49c64add783..4ef995e23e2 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -207,7 +207,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { stateRestorationManager = AppStateRestorationManager(fileStore: fileStore) #if SPARKLE - if !NSApp.runType.isUITests { + if NSApp.runType != .uiTests { updateController = UpdateController(internalUserDecider: internalUserDecider) stateRestorationManager.subscribeToAutomaticAppRelaunching(using: updateController.willRelaunchAppPublisher) } @@ -272,7 +272,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { subscriptionManager.loadInitialData() - if [.normal, .uiTests, .uiTestsInCI].contains(NSApp.runType) { + if [.normal, .uiTests].contains(NSApp.runType) { stateRestorationManager.applicationDidFinishLaunching() } diff --git a/DuckDuckGo/Common/Database/Database.swift b/DuckDuckGo/Common/Database/Database.swift index 1b1550891d0..ebfb19d771a 100644 --- a/DuckDuckGo/Common/Database/Database.swift +++ b/DuckDuckGo/Common/Database/Database.swift @@ -45,17 +45,14 @@ final class Database { let mainModel = NSManagedObjectModel.mergedModel(from: [.main])! - // Encryption is disabled for UI Tests in CI until we resolve the issue with Value Transformers intialization - if NSApp.runType != .uiTestsInCI { - _=mainModel.registerValueTransformers(withAllowedPropertyClasses: [ - NSImage.self, - NSString.self, - NSURL.self, - NSNumber.self, - NSError.self, - NSData.self - ], keyStore: keyStore) - } + _=mainModel.registerValueTransformers(withAllowedPropertyClasses: [ + NSImage.self, + NSString.self, + NSURL.self, + NSNumber.self, + NSError.self, + NSData.self + ], keyStore: keyStore) let httpsUpgradeModel = HTTPSUpgrade.managedObjectModel diff --git a/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift b/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift index e9faf4b0e8b..dd2e444259f 100644 --- a/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSApplicationExtension.swift @@ -30,26 +30,20 @@ extension NSApplication { case unitTests case integrationTests case uiTests - case uiTestsInCI case xcPreviews /// Defines if app run type requires loading full environment, i.e. databases, saved state, keychain etc. var requiresEnvironment: Bool { switch self { - case .normal, .integrationTests, .uiTests, .uiTestsInCI: + case .normal, .integrationTests, .uiTests: return true case .unitTests, .xcPreviews: return false } } - - var isUITests: Bool { - self == .uiTests || self == .uiTestsInCI - } } static let runType: RunType = { - let isCI = ProcessInfo.processInfo.environment["CI"] != nil #if DEBUG if let testBundlePath = ProcessInfo().environment["XCTestBundlePath"] { if testBundlePath.contains("Unit") { @@ -57,20 +51,21 @@ extension NSApplication { } else if testBundlePath.contains("Integration") { return .integrationTests } else { - return isCI ? .uiTestsInCI : .uiTests + return .uiTests } } else if ProcessInfo().environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { return .xcPreviews } else if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" { - return isCI ? .uiTestsInCI : .uiTests + return .uiTests } else { return .normal } #elseif REVIEW + let isCI = ProcessInfo.processInfo.environment["CI"] != nil // UITEST_MODE is set from UI Tests code, but this check didn't work reliably // in CI on its own, so we're defaulting all CI runs of the REVIEW app to UI Tests if ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" || isCI { - return isCI ? .uiTestsInCI : .uiTests + return .uiTests } return .normal #else diff --git a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift index 5e7ae66fa4c..45219d5e5d1 100644 --- a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift +++ b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift @@ -154,7 +154,7 @@ final class DeviceAuthenticator: UserAuthenticating { } func authenticateUser(reason: AuthenticationReason, result: @escaping (DeviceAuthenticationResult) -> Void) { - guard !NSApp.runType.isUITests else { + guard NSApp.runType != .uiTests else { result(.success) return } diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index f859088150c..f0b6a10fc54 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -398,7 +398,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { return } - guard [NSApplication.RunType.normal, .uiTests, .uiTestsInCI].contains(NSApp.runType) else { + guard [NSApplication.RunType.normal, .uiTests].contains(NSApp.runType) else { return } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index 1e2671b633d..bb8bd460466 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -269,7 +269,7 @@ final class PasswordManagementViewController: NSViewController { } private func promptForAuthenticationIfNecessary() { - guard !NSApp.runType.isUITests else { + guard NSApp.runType != .uiTests else { toggleLockScreen(hidden: true) return } diff --git a/DuckDuckGo/Sync/SyncDataProviders.swift b/DuckDuckGo/Sync/SyncDataProviders.swift index 3017100e9a6..69260f80285 100644 --- a/DuckDuckGo/Sync/SyncDataProviders.swift +++ b/DuckDuckGo/Sync/SyncDataProviders.swift @@ -44,14 +44,11 @@ final class SyncDataProviders: DataProvidersSource { metricsEventsHandler: metricsEventsHandler ) - // Credentials syncing is disabled in UI Tests in CI until we figure out Secure Vault errors - if NSApp.runType != .uiTestsInCI { - credentialsAdapter.setUpProviderIfNeeded( - secureVaultFactory: secureVaultFactory, - metadataStore: syncMetadata, - metricsEventsHandler: metricsEventsHandler - ) - } + credentialsAdapter.setUpProviderIfNeeded( + secureVaultFactory: secureVaultFactory, + metadataStore: syncMetadata, + metricsEventsHandler: metricsEventsHandler + ) settingsAdapter.setUpProviderIfNeeded( metadataDatabase: syncMetadataDatabase.db, diff --git a/SyncE2EUITests/CriticalPathsTests.swift b/SyncE2EUITests/CriticalPathsTests.swift index a830183df4c..51c2d5c557c 100644 --- a/SyncE2EUITests/CriticalPathsTests.swift +++ b/SyncE2EUITests/CriticalPathsTests.swift @@ -237,12 +237,8 @@ final class CriticalPathsTests: XCTestCase { // Add Bookmarks and Favorite addBookmarksAndFavorites() - // Temporarily skipping Logins testing in CI until we resolve the problem with encrypted value transformers - // See makeDatabase() in Database.swift - if !isCI { - // Add Login - addLogin() - } + // Add Login + addLogin() // Copy code to clipboard copyToClipboard(code: code) @@ -282,12 +278,8 @@ final class CriticalPathsTests: XCTestCase { // Check Unified favorites checkUnifiedFavorites() - // Temporarily skipping Logins testing in CI until we resolve the problem with encrypted value transformers - // See makeDatabase() in Database.swift - if !isCI { - // Check Logins - checkLogins() - } + // Check Logins + checkLogins() } private func logIn() { From 909d7c1267061840f59e6a458821187b67e86504 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:17:12 +0200 Subject: [PATCH 17/35] Update autoconsent to v10.10.0 (#2842) Task/Issue URL: https://app.asana.com/0/1207512946126808/1207512946126808 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v10.10.0 --- 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 bbdc3e35c11..200fd20d225 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_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_FIDES_DETECT_POPUP:()=>window.Fides?.initialized,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_KETCH_TEST:()=>document.cookie.includes("_ketch_consent_v1_"),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_ROBLOX_TEST:()=>document.cookie.includes("RBXcb"),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,#_evidon-background"),await this.waitForThenClick("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),await this.wait(500),await this.waitForThenClick("#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",prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{waitForThenClick:"#consent-tracking .affirm.btn"}],optOut:[{if:{exists:"#consent-tracking .decline.btn"},then:[{click:"#consent-tracking .decline.btn"}],else:[{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:[{if:{exists:".cmp-pref-link"},then:[{click:".cmp-pref-link"},{waitForThenClick:".cmp-body [id*=rejectAll]"},{waitForThenClick:".cmp-body .cmp-save-btn"}]}]}]},{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:"cookiecuttr",vendorUrl:"https://github.com/cdwharton/cookieCuttr",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:""},prehideSelectors:[".cc-cookies"],detectCmp:[{exists:".cc-cookies .cc-cookie-accept"}],detectPopup:[{visible:".cc-cookies .cc-cookie-accept"}],optIn:[{waitForThenClick:".cc-cookies .cc-cookie-accept"}],optOut:[{if:{exists:".cc-cookies .cc-cookie-decline"},then:[{click:".cc-cookies .cc-cookie-decline"}],else:[{hide:".cc-cookies"}]}]},{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"},{eval:"EVAL_FIDES_DETECT_POPUP"}],optIn:[{waitForThenClick:"#fides-banner .fides-accept-all-button"}],optOut:[{waitForThenClick:"#fides-banner .fides-reject-all-button"}]},{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], #lanyard_root button[class*=buttons-secondary]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description], #ketch-preferences",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description], #ketch-preferences"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton], #lanyard_root button[class*=rejectAllButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1), #lanyard_root button[class*=actionButton]"}]}],test:[{eval:"EVAL_KETCH_TEST"}]},{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:"roblox",vendorUrl:"https://roblox.com",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?roblox\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner-wrapper"}],detectPopup:[{visible:".cookie-banner-wrapper .cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner-wrapper button.btn-cta-lg"}],optOut:[{waitForThenClick:".cookie-banner-wrapper button.btn-secondary-lg"}],test:[{eval:"EVAL_ROBLOX_TEST"}]},{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",runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?uswitch\\.com/"},prehideSelectors:[".ucb"],detectCmp:[{exists:".ucb-banner"}],detectPopup:[{visible:".ucb-banner"}],optIn:[{waitForThenClick:".ucb-banner .ucb-btn-accept"}],optOut:[{waitForThenClick:".ucb-banner .ucb-btn-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_FIDES_DETECT_POPUP:()=>window.Fides?.initialized,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_KETCH_TEST:()=>document.cookie.includes("_ketch_consent_v1_"),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_ROBLOX_TEST:()=>document.cookie.includes("RBXcb"),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,#_evidon-background"),await this.waitForThenClick("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),await this.wait(500),await this.waitForThenClick("#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",prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{waitForThenClick:"#consent-tracking .affirm.btn"}],optOut:[{if:{exists:"#consent-tracking .decline.btn"},then:[{click:"#consent-tracking .decline.btn"}],else:[{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:[{if:{exists:".cmp-pref-link"},then:[{click:".cmp-pref-link"},{waitForThenClick:".cmp-body [id*=rejectAll]"},{waitForThenClick:".cmp-body .cmp-save-btn"}]}]}]},{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:"cookiecuttr",vendorUrl:"https://github.com/cdwharton/cookieCuttr",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:""},prehideSelectors:[".cc-cookies"],detectCmp:[{exists:".cc-cookies .cc-cookie-accept"}],detectPopup:[{visible:".cc-cookies .cc-cookie-accept"}],optIn:[{waitForThenClick:".cc-cookies .cc-cookie-accept"}],optOut:[{if:{exists:".cc-cookies .cc-cookie-decline"},then:[{click:".cc-cookies .cc-cookie-decline"}],else:[{hide:".cc-cookies"}]}]},{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:"ebay",vendorUrl:"https://ebay.com",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?ebay\\.([.a-z]+)/"},prehideSelectors:["#gdpr-banner"],detectCmp:[{exists:"#gdpr-banner"}],detectPopup:[{visible:"#gdpr-banner"}],optIn:[{waitForThenClick:"#gdpr-banner-accept"}],optOut:[{waitForThenClick:"#gdpr-banner-decline"}]},{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:"#ensModalWrapper[style*=block]"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{wait:500},{visible:"#ensModalWrapper[style*=block]"},{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner[style*=block]"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{wait:500},{visible:"#ensNotifyBanner[style*=block]"},{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner,.rejectAll"}]},{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"},{eval:"EVAL_FIDES_DETECT_POPUP"}],optIn:[{waitForThenClick:"#fides-banner .fides-accept-all-button"}],optOut:[{waitForThenClick:"#fides-banner .fides-reject-all-button"}]},{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], #lanyard_root button[class*=buttons-secondary]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description], #ketch-preferences",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description], #ketch-preferences"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton], #lanyard_root button[class*=rejectAllButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1), #lanyard_root button[class*=actionButton]"}]}],test:[{eval:"EVAL_KETCH_TEST"}]},{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:"roblox",vendorUrl:"https://roblox.com",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?roblox\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner-wrapper"}],detectPopup:[{visible:".cookie-banner-wrapper .cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner-wrapper button.btn-cta-lg"}],optOut:[{waitForThenClick:".cookie-banner-wrapper button.btn-secondary-lg"}],test:[{eval:"EVAL_ROBLOX_TEST"}]},{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",runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?uswitch\\.com/"},prehideSelectors:[".ucb"],detectCmp:[{exists:".ucb-banner"}],detectPopup:[{visible:".ucb-banner"}],optIn:[{waitForThenClick:".ucb-banner .ucb-btn-accept"}],optOut:[{waitForThenClick:".ucb-banner .ucb-btn-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 4d2e2ff7414..d6a96e48baf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.9.0" + "@duckduckgo/autoconsent": "^10.10.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.9.0.tgz", - "integrity": "sha512-eTlPFmW7QzThb9OGDWSSnUmB8Kq74YDO1N63cC/XsBHruaeN13N60GwhcL2/p4C05Gfkv8AhyOl/fTvUjya/hg==" + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.10.0.tgz", + "integrity": "sha512-ewY0Rrpp/FiFS8tf7qO4+iuAM5h/nSo8jdBQbpOQ1hI7aCP5pIdrZehJWIO7mkiNboVKAaP3p1WBhydIjfQDFA==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index 41c8b182200..68333b551a5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.9.0" + "@duckduckgo/autoconsent": "^10.10.0" } } From a4e813d62cf57e9ca4fee8ddb24d3cb1df8448ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 7 Jun 2024 18:09:09 +0200 Subject: [PATCH 18/35] Fire compilation failed pixel if needed (#1626) Task/Issue URL: https://app.asana.com/0/1203790657351911/1205404596674276/f Description: Introduce daily pixel containing information about failed content blocking rules compilations. --- DuckDuckGo/Application/AppDelegate.swift | 16 ++++++++++++++++ DuckDuckGo/Statistics/GeneralPixel.swift | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 4ef995e23e2..18eeeafa44d 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -306,6 +306,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { subscribeToEmailProtectionStatusNotifications() subscribeToDataImportCompleteNotification() + fireFailedCompilationsPixelIfNeeded() + UserDefaultsWrapper.clearRemovedKeys() networkProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() @@ -326,6 +328,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate { setUpAutofillPixelReporter() } + private func fireFailedCompilationsPixelIfNeeded() { + let store = FailedCompilationsStore() + if store.hasAnyFailures { + PixelKit.fire(DebugEvent(GeneralPixel.compilationFailed), + frequency: .daily, + withAdditionalParameters: store.summary, + includeAppVersionParameter: true) { didFire, _ in + if !didFire { + store.cleanup() + } + } + } + } + func applicationDidBecomeActive(_ notification: Notification) { guard didFinishLaunching else { return } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 42fb1feab69..7130685a7ae 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -352,6 +352,8 @@ enum GeneralPixel: PixelKitEventV2 { case secureVaultKeystoreEventL2KeyMigration case secureVaultKeystoreEventL2KeyPasswordMigration + case compilationFailed + var name: String { switch self { @@ -862,6 +864,8 @@ enum GeneralPixel: PixelKitEventV2 { 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" + + case .compilationFailed: return "compilation_failed" } } From a6fe6648dc50c8bc2fc98290c273c8f361fcf852 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 7 Jun 2024 17:21:45 -0300 Subject: [PATCH 19/35] DBP: Add support for noResultsSelector (#2840) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- .../DataBrokerProtection/Model/Actions/Extract.swift | 1 + .../Resources/JSON/peopleswhizr.com.json | 3 ++- .../Resources/JSON/peopleswiz.com.json | 3 ++- .../Resources/JSON/peopleswizard.com.json | 3 ++- .../Resources/JSON/peoplewhiz.com.json | 3 ++- .../Resources/JSON/peoplewhiz.net.json | 3 ++- .../Resources/JSON/peoplewhized.com.json | 3 ++- .../Resources/JSON/peoplewhized.net.json | 3 ++- .../Resources/JSON/peoplewhizr.com.json | 3 ++- .../Resources/JSON/peoplewhizr.net.json | 3 ++- .../Resources/JSON/peoplewiz.com.json | 3 ++- .../Resources/JSON/peoplewizard.net.json | 3 ++- .../Resources/JSON/peoplewizr.com.json | 3 ++- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 18 files changed, 33 insertions(+), 20 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 87f94d809e6..6efa56f3f9f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12993,7 +12993,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 152.0.0; + version = 152.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 d7d10dd71cc..850e4da4bc1 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" : "e32733e0e0b03bbac2fec160a2f967a15ed1794b", - "version" : "152.0.0" + "revision" : "43d6c090699ddc1b92c0c016dc31b923fb06f59f", + "version" : "152.0.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "fa861c4eccb21d235e34070b208b78bdc32ece08", - "version" : "5.17.0" + "revision" : "ba2ad0c3a14a8c07d7be8b50a20b47efea207e86", + "version" : "5.19.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 37ad4bb25c6..c6c69d600eb 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: "152.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Extract.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Extract.swift index 97dcfea3825..f1b8c195cf1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Extract.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Extract.swift @@ -20,6 +20,7 @@ struct ExtractAction: Action { let id: String let actionType: ActionType let selector: String + let noResultsSelector: String? let profile: ExtractProfileSelectors let dataSource: DataSource? } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json index 39bee21f023..2bea79b7748 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json @@ -1,7 +1,7 @@ { "name": "PeoplesWhizr", "url": "peopleswhizr.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "bc952139-373b-4461-8e24-83661038f657", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json index 18ac36c1286..3d88a0b760e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json @@ -1,7 +1,7 @@ { "name": "PeoplesWiz", "url": "peopleswiz.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "9e1e6ff5-cb99-49aa-a37c-b3dc80012f19", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json index 28e592c7cb1..96a80495503 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json @@ -1,7 +1,7 @@ { "name": "PeoplesWizard", "url": "peopleswizard.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "c538d04e-85ce-46a5-bbbd-3f9bd0fae078", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json index 9560a08e766..f51b86a541b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json @@ -1,7 +1,7 @@ { "name": "PeopleWhiz.com", "url": "peoplewhiz.com", - "version": "0.1.7", + "version": "0.2.0", "addedDatetime": 1676160000000, "steps": [ { @@ -17,6 +17,7 @@ "actionType": "extract", "id": "efa321f2-f214-411f-a87b-bc42feff7931", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json index af5e43e5530..cd7f9b1b23c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json @@ -1,7 +1,7 @@ { "name": "PeopleWhiz.net", "url": "peoplewhiz.net", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709424000000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "9e0d566b-dd31-40a7-9315-ec6fba8d6233", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json index 0e5c4026513..18f4023df77 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json @@ -1,7 +1,7 @@ { "name": "PeopleWhized.com", "url": "peoplewhized.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709424000000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "77517554-f1e7-4e73-b0d1-92f56a0f247c", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json index 66b7eec1d9a..37b4f1d0eac 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json @@ -1,7 +1,7 @@ { "name": "PeopleWhized.net", "url": "peoplewhized.net", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "440406f6-bd8a-4af7-b509-43eb24c79e4c", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json index 6efb536b083..c2bb7995761 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json @@ -1,7 +1,7 @@ { "name": "PeopleWhizr.com", "url": "peoplewhizr.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "21a783ce-7a7e-4a97-8dc2-ee4c2f3dc63a", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json index 6faef5be132..7afddb94103 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json @@ -1,7 +1,7 @@ { "name": "PeopleWhizr.net", "url": "peoplewhizr.net", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "2955fb45-374e-42f2-a72a-f7a2ffe4ebe5", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json index 5e3459100f6..25e6637ad96 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json @@ -1,7 +1,7 @@ { "name": "PeopleWiz", "url": "peoplewiz.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "36ba376d-03c1-4ecc-92ed-2fe12bd79d6f", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json index c912e7886ed..b318e39d570 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json @@ -1,7 +1,7 @@ { "name": "PeopleWizard.net", "url": "peoplewizard.net", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "f6d41239-9208-419e-b101-c58e52a71d42", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json index e6a7d1638cd..bda74021eb6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json @@ -1,7 +1,7 @@ { "name": "PeopleWizr", "url": "peoplewizr.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "53c86c5d-3960-44a9-91a3-143b71d1e07f", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index ac278804075..393ebc0a173 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.1"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../AppLauncher"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 8fab2675395..adb58da7877 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: "152.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From d5f19a2c2368cccb351398ad52a76e796e87135b Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 9 Jun 2024 17:56:10 -0300 Subject: [PATCH 20/35] DBP: Implement stats pixels (#2812) --- .../DataBrokerProtectionPixelsHandler.swift | 6 +- .../Model/DataBroker.swift | 8 + .../Model/HistoryEvent.swift | 9 + ...taBrokerProfileQueryOperationManager.swift | 2 +- ...DataBrokerProtectionEngagementPixels.swift | 28 +- .../Pixels/DataBrokerProtectionPixels.swift | 41 +- .../DataBrokerProtectionPixelsUtilities.swift | 43 ++ .../DataBrokerProtectionStatsPixels.swift | 376 ++++++++++++++++++ .../DataBrokerProtectionQueueManager.swift | 3 + ...kerProfileQueryOperationManagerTests.swift | 19 + ...DataBrokerProtectionStatsPixelsTests.swift | 371 +++++++++++++++++ .../DataBrokerProtectionTests/Mocks.swift | 37 ++ 12 files changed, 915 insertions(+), 28 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixelsUtilities.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStatsPixels.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 494dae251cf..a073fa9aa53 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -93,7 +93,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Bool { + guard let removedAt = self.removedAt else { + return false + } + + return removedAt < since + } } public enum DataBrokerHierarchy: Int { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift index 9ed693822b2..c783ea44226 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift @@ -60,4 +60,13 @@ public struct HistoryEvent: Identifiable, Sendable { return 0 } } + + func isMatchEvent() -> Bool { + switch type { + case .noMatchFound, .matchesFound: + return true + default: + return false + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 72b5772a8b1..4ec3876dabe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -139,11 +139,11 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { stageCalculator.fireScanSuccess(matchesFound: extractedProfiles.count) let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .matchesFound(count: extractedProfiles.count)) try database.add(event) + let extractedProfilesForBroker = try database.fetchExtractedProfiles(for: brokerId) for extractedProfile in extractedProfiles { // We check if the profile exists in the database. - let extractedProfilesForBroker = try database.fetchExtractedProfiles(for: brokerId) let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier } // If the profile exists we do not create a new opt-out operation diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift index e825e0eb331..918edd298f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift @@ -92,14 +92,6 @@ final class DataBrokerProtectionEngagementPixelsUserDefaults: DataBrokerProtecti - MAU Pixel Last Sent 2024-03-19 */ final class DataBrokerProtectionEngagementPixels { - - enum ActiveUserFrequency: Int { - case daily = 1 - case weekly = 7 - case monthly = 28 - } - - private let calendar = Calendar.current private let database: DataBrokerProtectionRepository private let repository: DataBrokerProtectionEngagementPixelsRepository private let handler: EventMapping @@ -139,7 +131,7 @@ final class DataBrokerProtectionEngagementPixels { return true } - return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .daily) + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .daily) } private func shouldWeFireWeeklyPixel(date: Date) -> Bool { @@ -147,7 +139,7 @@ final class DataBrokerProtectionEngagementPixels { return true } - return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .weekly) + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .weekly) } private func shouldWeFireMonthlyPixel(date: Date) -> Bool { @@ -155,20 +147,6 @@ final class DataBrokerProtectionEngagementPixels { return true } - return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .monthly) - } - - private func shouldWeFirePixel(startDate: Date, endDate: Date, daysDifference: ActiveUserFrequency) -> Bool { - if let differenceBetweenDates = differenceBetweenDates(startDate: startDate, endDate: endDate) { - return differenceBetweenDates >= daysDifference.rawValue - } - - return false - } - - private func differenceBetweenDates(startDate: Date, endDate: Date) -> Int? { - let components = calendar.dateComponents([.day], from: startDate, to: endDate) - - return components.day + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .monthly) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 5187485731e..7d2f3fcd300 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -70,6 +70,13 @@ public enum DataBrokerProtectionPixels { static let hasError = "has_error" static let brokerURL = "broker_url" static let sleepDuration = "sleep_duration" + static let numberOfRecordsFound = "num_found" + static let numberOfOptOutsInProgress = "num_inprogress" + static let numberOfSucessfulOptOuts = "num_optoutsuccess" + static let numberOfOptOutsFailure = "num_optoutfailure" + static let durationOfFirstOptOut = "duration_firstoptout" + static let numberOfNewRecordsFound = "num_new_found" + static let numberOfReappereances = "num_reappeared" } case error(error: DataBrokerProtectionError, dataBroker: String) @@ -171,6 +178,12 @@ public enum DataBrokerProtectionPixels { case entitlementCheckValid case entitlementCheckInvalid case entitlementCheckError + // Measure success/failure rate of Personal Information Removal Pixels + // https://app.asana.com/0/1204006570077678/1206889724879222/f + case globalMetricsWeeklyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) + case globalMetricsMonthlyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) + case dataBrokerMetricsWeeklyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int) + case dataBrokerMetricsMonthlyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int) } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -281,6 +294,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .entitlementCheckValid: return "m_mac_dbp_macos_entitlement_valid" case .entitlementCheckInvalid: return "m_mac_dbp_macos_entitlement_invalid" case .entitlementCheckError: return "m_mac_dbp_macos_entitlement_error" + case .globalMetricsWeeklyStats: return "m_mac_dbp_weekly_stats" + case .globalMetricsMonthlyStats: return "m_mac_dbp_monthly_stats" + case .dataBrokerMetricsWeeklyStats: return "m_mac_dbp_databroker_weekly_stats" + case .dataBrokerMetricsMonthlyStats: return "m_mac_dbp_databroker_monthly_stats" } } @@ -419,6 +436,24 @@ extension DataBrokerProtectionPixels: PixelKitEvent { 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)] + case .globalMetricsWeeklyStats(let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound), + .globalMetricsMonthlyStats(let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound): + return [Consts.numberOfRecordsFound: String(profilesFound), + Consts.numberOfOptOutsInProgress: String(optOutsInProgress), + Consts.numberOfSucessfulOptOuts: String(successfulOptOuts), + Consts.numberOfOptOutsFailure: String(failedOptOuts), + Consts.durationOfFirstOptOut: String(durationOfFirstOptOut), + Consts.numberOfNewRecordsFound: String(numberOfNewRecordsFound)] + case .dataBrokerMetricsWeeklyStats(let dataBrokerURL, let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound, let numberOfReappereances), + .dataBrokerMetricsMonthlyStats(let dataBrokerURL, let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound, let numberOfReappereances): + return [Consts.dataBrokerParamKey: dataBrokerURL, + Consts.numberOfRecordsFound: String(profilesFound), + Consts.numberOfOptOutsInProgress: String(optOutsInProgress), + Consts.numberOfSucessfulOptOuts: String(successfulOptOuts), + Consts.numberOfOptOutsFailure: String(failedOptOuts), + Consts.durationOfFirstOptOut: String(durationOfFirstOptOut), + Consts.numberOfNewRecordsFound: String(numberOfNewRecordsFound), + Consts.numberOfReappereances: String(numberOfReappereances)] } } } @@ -498,7 +533,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Bool { + if let differenceBetweenDates = numberOfDaysFrom(startDate: startDate, endDate: endDate) { + return differenceBetweenDates >= daysDifference.rawValue + } + + return false + } + + static func numberOfDaysFrom(startDate: Date, endDate: Date) -> Int? { + let components = calendar.dateComponents([.day], from: startDate, to: endDate) + + return components.day + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStatsPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStatsPixels.swift new file mode 100644 index 00000000000..e14bbddcec4 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStatsPixels.swift @@ -0,0 +1,376 @@ +// +// DataBrokerProtectionStatsPixels.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 +import BrowserServicesKit +import PixelKit + +protocol DataBrokerProtectionStatsPixelsRepository { + func markStatsWeeklyPixelDate() + func markStatsMonthlyPixelDate() + + func getLatestStatsWeeklyPixelDate() -> Date? + func getLatestStatsMonthlyPixelDate() -> Date? +} + +final class DataBrokerProtectionStatsPixelsUserDefaults: DataBrokerProtectionStatsPixelsRepository { + + enum Consts { + static let weeklyPixelKey = "macos.browser.data-broker-protection.statsWeeklyPixelKey" + static let monthlyPixelKey = "macos.browser.data-broker-protection.statsMonthlyPixelKey" + } + + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + func markStatsWeeklyPixelDate() { + userDefaults.set(Date(), forKey: Consts.weeklyPixelKey) + } + + func markStatsMonthlyPixelDate() { + userDefaults.set(Date(), forKey: Consts.monthlyPixelKey) + } + + func getLatestStatsWeeklyPixelDate() -> Date? { + userDefaults.object(forKey: Consts.weeklyPixelKey) as? Date + } + + func getLatestStatsMonthlyPixelDate() -> Date? { + userDefaults.object(forKey: Consts.monthlyPixelKey) as? Date + } +} + +struct StatsByBroker { + let dataBrokerURL: String + let numberOfProfilesFound: Int + let numberOfOptOutsInProgress: Int + let numberOfSuccessfulOptOuts: Int + let numberOfFailureOptOuts: Int + let numberOfNewMatchesFound: Int + let numberOfReAppereances: Int + let durationOfFirstOptOut: Int + + var toWeeklyPixel: DataBrokerProtectionPixels { + return .dataBrokerMetricsWeeklyStats(dataBrokerURL: dataBrokerURL, + profilesFound: numberOfProfilesFound, + optOutsInProgress: numberOfOptOutsInProgress, + successfulOptOuts: numberOfSuccessfulOptOuts, + failedOptOuts: numberOfFailureOptOuts, + durationOfFirstOptOut: durationOfFirstOptOut, + numberOfNewRecordsFound: numberOfNewMatchesFound, + numberOfReappereances: numberOfReAppereances) + } + + var toMonthlyPixel: DataBrokerProtectionPixels { + return .dataBrokerMetricsMonthlyStats(dataBrokerURL: dataBrokerURL, + profilesFound: numberOfProfilesFound, + optOutsInProgress: numberOfOptOutsInProgress, + successfulOptOuts: numberOfSuccessfulOptOuts, + failedOptOuts: numberOfFailureOptOuts, + durationOfFirstOptOut: durationOfFirstOptOut, + numberOfNewRecordsFound: numberOfNewMatchesFound, + numberOfReappereances: numberOfReAppereances) + } +} + +extension Array where Element == StatsByBroker { + + func toWeeklyPixel(durationOfFirstOptOut: Int) -> DataBrokerProtectionPixels { + let numberOfGlobalProfilesFound = map { $0.numberOfProfilesFound }.reduce(0, +) + let numberOfGlobalOptOutsInProgress = map { $0.numberOfOptOutsInProgress }.reduce(0, +) + let numberOfGlobalSuccessfulOptOuts = map { $0.numberOfSuccessfulOptOuts }.reduce(0, +) + let numberOfGlobalFailureOptOuts = map { $0.numberOfFailureOptOuts }.reduce(0, +) + let numberOfGlobalNewMatchesFound = map { $0.numberOfNewMatchesFound }.reduce(0, +) + + return .globalMetricsWeeklyStats(profilesFound: numberOfGlobalProfilesFound, + optOutsInProgress: numberOfGlobalOptOutsInProgress, + successfulOptOuts: numberOfGlobalSuccessfulOptOuts, + failedOptOuts: numberOfGlobalFailureOptOuts, + durationOfFirstOptOut: durationOfFirstOptOut, + numberOfNewRecordsFound: numberOfGlobalNewMatchesFound) + } + + func toMonthlyPixel(durationOfFirstOptOut: Int) -> DataBrokerProtectionPixels { + let numberOfGlobalProfilesFound = map { $0.numberOfProfilesFound }.reduce(0, +) + let numberOfGlobalOptOutsInProgress = map { $0.numberOfOptOutsInProgress }.reduce(0, +) + let numberOfGlobalSuccessfulOptOuts = map { $0.numberOfSuccessfulOptOuts }.reduce(0, +) + let numberOfGlobalFailureOptOuts = map { $0.numberOfFailureOptOuts }.reduce(0, +) + let numberOfGlobalNewMatchesFound = map { $0.numberOfNewMatchesFound }.reduce(0, +) + + return .globalMetricsMonthlyStats(profilesFound: numberOfGlobalProfilesFound, + optOutsInProgress: numberOfGlobalOptOutsInProgress, + successfulOptOuts: numberOfGlobalSuccessfulOptOuts, + failedOptOuts: numberOfGlobalFailureOptOuts, + durationOfFirstOptOut: durationOfFirstOptOut, + numberOfNewRecordsFound: numberOfGlobalNewMatchesFound) + } +} + +final class DataBrokerProtectionStatsPixels { + private let database: DataBrokerProtectionRepository + private let handler: EventMapping + private let repository: DataBrokerProtectionStatsPixelsRepository + private let calendar = Calendar.current + + init(database: DataBrokerProtectionRepository, + handler: EventMapping, + repository: DataBrokerProtectionStatsPixelsRepository = DataBrokerProtectionStatsPixelsUserDefaults()) { + self.database = database + self.handler = handler + self.repository = repository + } + + func tryToFireStatsPixels() { + guard let brokerProfileQueryData = try? database.fetchAllBrokerProfileQueryData() else { + return + } + + let dateOfFirstScan = dateOfFirstScan(brokerProfileQueryData) + + if shouldFireWeeklyStats(dateOfFirstScan: dateOfFirstScan) { + firePixels(for: brokerProfileQueryData, + frequency: .weekly, + dateSinceLastSubmission: repository.getLatestStatsWeeklyPixelDate()) + repository.markStatsWeeklyPixelDate() + } + + if shouldFireMonthlyStats(dateOfFirstScan: dateOfFirstScan) { + firePixels(for: brokerProfileQueryData, + frequency: .monthly, + dateSinceLastSubmission: repository.getLatestStatsMonthlyPixelDate()) + repository.markStatsMonthlyPixelDate() + } + } + + private func shouldFireWeeklyStats(dateOfFirstScan: Date?) -> Bool { + // If no initial scan was done yet, we do not want to fire the pixel. + guard let dateOfFirstScan = dateOfFirstScan else { + return false + } + + if let lastWeeklyUpdateDate = repository.getLatestStatsWeeklyPixelDate() { + // If the last weekly was set we need to compare the date with it. + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: lastWeeklyUpdateDate, endDate: Date(), daysDifference: .weekly) + } else { + // If the weekly update date was never set we need to check the first scan date. + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: dateOfFirstScan, endDate: Date(), daysDifference: .weekly) + } + } + + private func shouldFireMonthlyStats(dateOfFirstScan: Date?) -> Bool { + // If no initial scan was done yet, we do not want to fire the pixel. + guard let dateOfFirstScan = dateOfFirstScan else { + return false + } + + if let lastMonthlyUpdateDate = repository.getLatestStatsMonthlyPixelDate() { + // If the last monthly was set we need to compare the date with it. + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: lastMonthlyUpdateDate, endDate: Date(), daysDifference: .monthly) + } else { + // If the monthly update date was never set we need to check the first scan date. + return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: dateOfFirstScan, endDate: Date(), daysDifference: .monthly) + } + } + + private func firePixels(for brokerProfileQueryData: [BrokerProfileQueryData], frequency: Frequency, dateSinceLastSubmission: Date? = nil) { + let statsByBroker = calculateStatsByBroker(brokerProfileQueryData, dateSinceLastSubmission: dateSinceLastSubmission) + + fireGlobalStats(statsByBroker, brokerProfileQueryData: brokerProfileQueryData, frequency: frequency) + fireStatsByBroker(statsByBroker, frequency: frequency) + } + + private func calculateStatsByBroker(_ brokerProfileQueryData: [BrokerProfileQueryData], dateSinceLastSubmission: Date? = nil) -> [StatsByBroker] { + let profileQueriesGroupedByBroker = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker }) + let statsByBroker = profileQueriesGroupedByBroker.map { (key: DataBroker, value: [BrokerProfileQueryData]) in + calculateByBroker(key, data: value, dateSinceLastSubmission: dateSinceLastSubmission) + } + + return statsByBroker + } + + private func fireGlobalStats(_ stats: [StatsByBroker], brokerProfileQueryData: [BrokerProfileQueryData], frequency: Frequency) { + // The duration for the global stats is calculated not taking into the account the broker. That's why we do not use one from the stats. + let durationOfFirstOptOut = calculateDurationOfFirstOptOut(brokerProfileQueryData) + + switch frequency { + case .weekly: + handler.fire(stats.toWeeklyPixel(durationOfFirstOptOut: durationOfFirstOptOut)) + case .monthly: + handler.fire(stats.toMonthlyPixel(durationOfFirstOptOut: durationOfFirstOptOut)) + default: () + } + } + + private func fireStatsByBroker(_ stats: [StatsByBroker], frequency: Frequency) { + for stat in stats { + switch frequency { + case .weekly: + handler.fire(stat.toWeeklyPixel) + case .monthly: + handler.fire(stat.toMonthlyPixel) + default: () + } + } + } + + /// internal for testing purposes + func calculateByBroker(_ broker: DataBroker, data: [BrokerProfileQueryData], dateSinceLastSubmission: Date? = nil) -> StatsByBroker { + let mirrorSitesSize = broker.mirrorSites.filter { !$0.wasRemoved() }.count + var numberOfProfilesFound = 0 // Number of unique matching profiles found since the beginning. + var numberOfOptOutsInProgress = 0 // Number of opt-outs in progress since the beginning. + var numberOfSuccessfulOptOuts = 0 // Number of successfull opt-outs since the beginning + var numberOfReAppearences = 0 // Number of records that were removed and came back + + for query in data { + for optOutData in query.optOutJobData { + if broker.performsOptOutWithinParent() { + // Path when the broker is a child site. + numberOfProfilesFound += 1 + if optOutData.historyEvents.contains(where: { $0.type == .optOutConfirmed }) { + numberOfSuccessfulOptOuts += 1 + } else { + numberOfOptOutsInProgress += 1 + } + } else { + // Path when the broker is a parent site. + // If we requested the opt-out successfully but we didn't remove it yet, it means it is in progress + numberOfProfilesFound += 1 + mirrorSitesSize + + if optOutData.historyEvents.contains(where: { $0.type == .optOutRequested }) && optOutData.extractedProfile.removedDate == nil { + numberOfOptOutsInProgress += 1 + mirrorSitesSize + } else if optOutData.extractedProfile.removedDate != nil { // If it the removed date is not nil, it means we removed it. + numberOfSuccessfulOptOuts += 1 + mirrorSitesSize + } + } + } + + numberOfReAppearences += calculateNumberOfReAppereances(query.scanJobData) + mirrorSitesSize + } + + let numberOfFailureOptOuts = numberOfProfilesFound - numberOfOptOutsInProgress - numberOfSuccessfulOptOuts + let numberOfNewMatchesFound = calculateNumberOfNewMatchesFound(data) + let durationOfFirstOptOut = calculateDurationOfFirstOptOut(data, from: dateSinceLastSubmission) + + return StatsByBroker(dataBrokerURL: broker.url, + numberOfProfilesFound: numberOfProfilesFound, + numberOfOptOutsInProgress: numberOfOptOutsInProgress, + numberOfSuccessfulOptOuts: numberOfSuccessfulOptOuts, + numberOfFailureOptOuts: numberOfFailureOptOuts, + numberOfNewMatchesFound: numberOfNewMatchesFound, + numberOfReAppereances: numberOfReAppearences, + durationOfFirstOptOut: durationOfFirstOptOut) + } + + /// Calculates number of new matches found on scans that were not initial scans. + /// + /// internal for testing purposes + func calculateNumberOfNewMatchesFound(_ brokerProfileQueryData: [BrokerProfileQueryData]) -> Int { + var numberOfNewMatches = 0 + + let brokerProfileQueryDataWithAMatch = brokerProfileQueryData.filter { !$0.extractedProfiles.isEmpty } + let profileQueriesGroupedByBroker = Dictionary(grouping: brokerProfileQueryDataWithAMatch, by: { $0.dataBroker }) + + profileQueriesGroupedByBroker.forEach { (key: DataBroker, value: [BrokerProfileQueryData]) in + let mirrorSitesCount = key.mirrorSites.filter { !$0.wasRemoved() }.count + + for query in value { + let matchesFoundEvents = query.scanJobData.historyEvents + .filter { $0.isMatchEvent() } + .sorted { $0.date < $1.date } + + matchesFoundEvents.enumerated().forEach { index, element in + if index > 0 && index < matchesFoundEvents.count - 1 { + let nextElement = matchesFoundEvents[index + 1] + numberOfNewMatches += max(nextElement.matchesFound() - element.matchesFound(), 0) + } + } + + if numberOfNewMatches > 0 { + numberOfNewMatches += mirrorSitesCount + } + } + } + + return numberOfNewMatches + } + + private func calculateNumberOfReAppereances(_ scan: ScanJobData) -> Int { + return scan.historyEvents.filter { $0.type == .reAppearence }.count + } + + /// Calculate the difference in days since the first scan and the first submitted opt-out for the list of brokerProfileQueryData. + /// The scan and the opt-out do not need to be for the same record. + /// If an opt-out wasn't submitted yet, we return 0. + /// + /// internal for testing purposes + func calculateDurationOfFirstOptOut(_ brokerProfileQueryData: [BrokerProfileQueryData], from: Date? = nil) -> Int { + guard let dateOfFirstScan = dateOfFirstScan(brokerProfileQueryData), + let dateOfFirstSubmittedOptOut = dateOfFirstSubmittedOptOut(brokerProfileQueryData) else { + return 0 + } + + if dateOfFirstScan > dateOfFirstSubmittedOptOut { + return 0 + } + + guard let differenceInDays = DataBrokerProtectionPixelsUtilities.numberOfDaysFrom(startDate: dateOfFirstScan, endDate: dateOfFirstSubmittedOptOut) else { + return 0 + } + + // If the difference in days is in hours, return 1. + if differenceInDays == 0 { + return 1 + } + + return differenceInDays + } + + /// Returns the date of the first scan since the beginning if not from Date is provided + private func dateOfFirstScan(_ brokerProfileQueryData: [BrokerProfileQueryData], from: Date? = nil) -> Date? { + let allScanOperations = brokerProfileQueryData.map { $0.scanJobData } + let allScanHistoryEvents = allScanOperations.flatMap { $0.historyEvents } + let scanStartedEventsSortedByDate = allScanHistoryEvents + .filter { $0.type == .scanStarted } + .sorted { $0.date < $1.date } + + if let from = from { + return scanStartedEventsSortedByDate.filter { from < $0.date }.first?.date + } else { + return scanStartedEventsSortedByDate.first?.date + } + } + + /// Returns the date of the first sumbitted opt-out. If no from date is provided, we return it from the beginning. + private func dateOfFirstSubmittedOptOut(_ brokerProfileQueryData: [BrokerProfileQueryData], from: Date? = nil) -> Date? { + let firstOptOutSubmittedEvent = brokerProfileQueryData + .flatMap { $0.optOutJobData } + .flatMap { $0.historyEvents } + .filter { $0.type == .optOutRequested } + .sorted { $0.date < $1.date } + + if let from = from { + return firstOptOutSubmittedEvent.filter { from < $0.date }.first?.date + } else { + return firstOptOutSubmittedEvent.first?.date + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index 3567279d89b..61f85122bbe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -250,11 +250,14 @@ private extension DefaultDataBrokerProtectionQueueManager { let engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) + let statsPixels = DataBrokerProtectionStatsPixels(database: database, handler: pixelHandler) // This will fire the DAU/WAU/MAU pixels, engagementPixels.fireEngagementPixel() // This will try to fire the event weekly report pixels eventPixels.tryToFireWeeklyPixels() + // This will try to fire the stats pixels + statsPixels.tryToFireStatsPixels() } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index bd3aeafb718..b3d94d364e9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -1038,6 +1038,25 @@ extension DataBroker { ) ) } + + static func mockWith(mirroSites: [MirrorSite]) -> DataBroker { + DataBroker( + id: 1, + name: "Test broker", + url: "testbroker.com", + steps: [ + Step(type: .scan, actions: [Action]()), + Step(type: .optOut, actions: [Action]()) + ], + version: "1.0", + schedulingConfig: DataBrokerScheduleConfig( + retryError: 0, + confirmOptOutScan: 0, + maintenanceScan: 0 + ), + mirrorSites: mirroSites + ) + } } extension ExtractedProfile { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift new file mode 100644 index 00000000000..cc8298bd719 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift @@ -0,0 +1,371 @@ +// +// DataBrokerProtectionStatsPixelsTests.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 DataBrokerProtectionStatsPixelsTests: XCTestCase { + + private let handler = MockDataBrokerProtectionPixelsHandler() + + override func tearDown() { + handler.clear() + } + + func testNumberOfNewMatchesIsCalculatedCorrectly() { + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2)), + .init(brokerId: 1, profileQueryId: 1, type: .noMatchFound), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1)), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2)), + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: .mock, + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEvents), + optOutJobData: [.mock(with: .mockWithoutRemovedDate)]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateNumberOfNewMatchesFound([brokerProfileQueryData]) + + XCTAssertEqual(result, 2) + } + + func testNumberOfNewMatchesIsCalculatedCorrectlyWithMirrorSites() { + let mirrorSites: [MirrorSite] = [ + .init(name: "Mirror #1", url: "url.com", addedAt: Date()), + .init(name: "Mirror #2", url: "url.com", addedAt: Date()) + ] + let historyEvents: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2)), + .init(brokerId: 1, profileQueryId: 1, type: .noMatchFound), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1)), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2)), + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: .mockWith(mirroSites: mirrorSites), + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEvents), + optOutJobData: [.mock(with: .mockWithoutRemovedDate)]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateNumberOfNewMatchesFound([brokerProfileQueryData]) + + XCTAssertEqual(result, 4) + } + + func testWhenDurationOfFirstOptOutIsLessThan24Hours_thenWeReturn1() { + let historyEventsForScan: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: Date()), + ] + let historyEventsForOptOut: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .optOutRequested, date: Date()), + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: .mock, + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEventsForScan), + optOutJobData: [.mock(with: .mockWithoutRemovedDate, historyEvents: historyEventsForOptOut)]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateDurationOfFirstOptOut([brokerProfileQueryData]) + + XCTAssertEqual(result, 1) + } + + func testWhenDateOfOptOutIsBeforeFirstScan_thenWeReturnZero() { + let historyEventsForScan: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: Date()), + ] + let historyEventsForOptOut: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .optOutRequested, date: Date().yesterday!), + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: .mock, + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEventsForScan), + optOutJobData: [.mock(with: .mockWithoutRemovedDate, historyEvents: historyEventsForOptOut)]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateDurationOfFirstOptOut([brokerProfileQueryData]) + + XCTAssertEqual(result, 0) + } + + func testWhenOptOutWasSubmitted_thenWeReturnCorrectNumberInDays() { + var dateComponents = DateComponents() + dateComponents.day = 3 + dateComponents.hour = 2 + let threeDaysAfterToday = Calendar.current.date(byAdding: dateComponents, to: Date())! + let historyEventsForScan: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: Date()), + ] + let historyEventsForOptOut: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .optOutRequested, date: threeDaysAfterToday), + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: .mock, + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEventsForScan), + optOutJobData: [.mock(with: .mockWithoutRemovedDate, historyEvents: historyEventsForOptOut)]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateDurationOfFirstOptOut([brokerProfileQueryData]) + + XCTAssertEqual(result, 3) + } + + /// This test data has the following parameters + /// - A broker that has two mirror sites but one was removed + /// - Four matches found + /// - One match was removed + /// - Two matches are in progress of being removed (this means we submitted the opt-out) + /// - One match failed to submit an opt-out + /// - One re-appereance of an old match after it was removed + func testStatsByBroker_hasCorrectParams() { + let mirrorSites: [MirrorSite] = [ + .init(name: "Mirror #1", url: "url.com", addedAt: Date()), + .init(name: "Mirror #2", url: "url.com", addedAt: Date(), removedAt: Date().yesterday) + ] + let broker: DataBroker = .mockWith(mirroSites: mirrorSites) + let historyEventsForFirstOptOutOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .optOutStarted), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .unknown("Error"))), + .init(brokerId: 1, profileQueryId: 1, type: .optOutStarted), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .unknown("Error"))) + ] + let historyEventForOptOutWithSubmittedRequest: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .optOutStarted), + .init(brokerId: 1, profileQueryId: 1, type: .error(error: .unknown("Error"))), + .init(brokerId: 1, profileQueryId: 1, type: .optOutStarted), + .init(brokerId: 1, profileQueryId: 1, type: .optOutRequested) + ] + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 3)), + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2)), + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 3)), + .init(brokerId: 1, profileQueryId: 1, type: .reAppearence) + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: broker, + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEventsForScanOperation), + optOutJobData: [ + .mock(with: .mockWithoutRemovedDate, historyEvents: historyEventsForFirstOptOutOperation), + .mock(with: .mockWithoutRemovedDate, historyEvents: historyEventForOptOutWithSubmittedRequest), + .mock(with: .mockWithoutRemovedDate, historyEvents: historyEventForOptOutWithSubmittedRequest), + .mock(with: .mockWithRemovedDate, historyEvents: historyEventForOptOutWithSubmittedRequest), + ]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateByBroker(broker, data: [brokerProfileQueryData]) + + XCTAssertEqual(result.numberOfProfilesFound, 8) + XCTAssertEqual(result.numberOfOptOutsInProgress, 4) + XCTAssertEqual(result.numberOfSuccessfulOptOuts, 2) + XCTAssertEqual(result.numberOfFailureOptOuts, 2) + XCTAssertEqual(result.numberOfNewMatchesFound, 2) + XCTAssertEqual(result.numberOfReAppereances, 2) + } + + /// This test data has the following parameters + /// - A broker that is a children site + /// - Three matches found + /// - One match was removed + /// - Two matches are in progress of being removed + func testStatsByBrokerForChildrenSite_hasCorrectParams() { + let broker: DataBroker = .mockWithParentOptOut + let historyEventsForFirstOptOutOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed) + ] + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 3)), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2)), + ] + let brokerProfileQueryData = BrokerProfileQueryData( + dataBroker: broker, + profileQuery: .mock, + scanJobData: .mockWith(historyEvents: historyEventsForScanOperation), + optOutJobData: [ + .mock(with: .mockWithRemovedDate, historyEvents: historyEventsForFirstOptOutOperation), + .mock(with: .mockWithoutRemovedDate, historyEvents: [HistoryEvent]()), + .mock(with: .mockWithoutRemovedDate, historyEvents: [HistoryEvent]()) + ]) + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: MockDataBrokerProtectionStatsPixelsRepository()) + + let result = sut.calculateByBroker(broker, data: [brokerProfileQueryData]) + + XCTAssertEqual(result.numberOfProfilesFound, 3) + XCTAssertEqual(result.numberOfOptOutsInProgress, 2) + XCTAssertEqual(result.numberOfSuccessfulOptOuts, 1) + } + + func testWhenDateOfFirstScanIsNil_thenWeDoNotFireAnyPixel() { + let repository = MockDataBrokerProtectionStatsPixelsRepository() + let sut = DataBrokerProtectionStatsPixels(database: MockDatabase(), + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertFalse(repository.wasMarkStatsWeeklyPixelDateCalled) + XCTAssertFalse(repository.wasMarkStatsMonthlyPixelDateCalled) + } + + func testWhenLastWeeklyPixelIsNilAndAWeekDidntPassSinceInitialScan_thenWeDoNotFireWeeklyPixel() { + let database = MockDatabase() + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: Date().yesterday!), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date().yesterday!), + ] + database.brokerProfileQueryDataToReturn = [ + .init(dataBroker: .mock, profileQuery: .mock, scanJobData: .mockWith(historyEvents: historyEventsForScanOperation)) + ] + let repository = MockDataBrokerProtectionStatsPixelsRepository() + let sut = DataBrokerProtectionStatsPixels(database: database, + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertFalse(repository.wasMarkStatsWeeklyPixelDateCalled) + } + + func testWhenAWeekDidntPassSinceLastWeeklyPixelDate_thenWeDoNotFireWeeklyPixel() { + let database = MockDatabase() + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: Date().yesterday!), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date().yesterday!), + ] + database.brokerProfileQueryDataToReturn = [ + .init(dataBroker: .mock, profileQuery: .mock, scanJobData: .mockWith(historyEvents: historyEventsForScanOperation)) + ] + let repository = MockDataBrokerProtectionStatsPixelsRepository() + repository.latestStatsWeeklyPixelDate = Date().yesterday! + let sut = DataBrokerProtectionStatsPixels(database: database, + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertFalse(repository.wasMarkStatsWeeklyPixelDateCalled) + } + + func testWhenAWeekPassedSinceLastWeeklyPixelDate_thenWeFireWeeklyPixel() { + let eightDaysAgo = Calendar.current.date(byAdding: .day, value: -8, to: Date())! + let database = MockDatabase() + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: eightDaysAgo), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: eightDaysAgo), + ] + database.brokerProfileQueryDataToReturn = [ + .init(dataBroker: .mock, profileQuery: .mock, scanJobData: .mockWith(historyEvents: historyEventsForScanOperation)) + ] + let repository = MockDataBrokerProtectionStatsPixelsRepository() + repository.latestStatsWeeklyPixelDate = eightDaysAgo + let sut = DataBrokerProtectionStatsPixels(database: database, + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertTrue(repository.wasMarkStatsWeeklyPixelDateCalled) + } + + func testWhenLastMonthlyPixelIsNilAnd28DaysDidntPassSinceInitialScan_thenWeDoNotFireMonthlyPixel() { + let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: Date())! + let database = MockDatabase() + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: twentyDaysAgo), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: twentyDaysAgo), + ] + database.brokerProfileQueryDataToReturn = [ + .init(dataBroker: .mock, profileQuery: .mock, scanJobData: .mockWith(historyEvents: historyEventsForScanOperation)) + ] + let repository = MockDataBrokerProtectionStatsPixelsRepository() + let sut = DataBrokerProtectionStatsPixels(database: database, + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertFalse(repository.wasMarkStatsMonthlyPixelDateCalled) + } + + func testWhen28DaysDidntPassSinceLastMonthlyPixelDate_thenWeDoNotFireMonthlyPixel() { + let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: Date())! + let database = MockDatabase() + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: twentyDaysAgo), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: twentyDaysAgo), + ] + database.brokerProfileQueryDataToReturn = [ + .init(dataBroker: .mock, profileQuery: .mock, scanJobData: .mockWith(historyEvents: historyEventsForScanOperation)) + ] + let repository = MockDataBrokerProtectionStatsPixelsRepository() + repository.latestStatsMonthlyPixelDate = twentyDaysAgo + let sut = DataBrokerProtectionStatsPixels(database: database, + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertFalse(repository.wasMarkStatsMonthlyPixelDateCalled) + } + + func testWhen28DaysPassedSinceLastMonthlyPixelDate_thenWeFireMonthlyPixel() { + let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date())! + let database = MockDatabase() + let historyEventsForScanOperation: [HistoryEvent] = [ + .init(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: thirtyDaysAgo), + .init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: thirtyDaysAgo), + ] + database.brokerProfileQueryDataToReturn = [ + .init(dataBroker: .mock, profileQuery: .mock, scanJobData: .mockWith(historyEvents: historyEventsForScanOperation)) + ] + let repository = MockDataBrokerProtectionStatsPixelsRepository() + repository.latestStatsMonthlyPixelDate = thirtyDaysAgo + let sut = DataBrokerProtectionStatsPixels(database: database, + handler: handler, + repository: repository) + + sut.tryToFireStatsPixels() + + XCTAssertTrue(repository.wasMarkStatsMonthlyPixelDateCalled) + } + +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index fb2361e0910..dd17954cd78 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -1008,6 +1008,13 @@ extension ScanJobData { } } +extension OptOutJobData { + static func mock(with extractedProfile: ExtractedProfile, + historyEvents: [HistoryEvent] = [HistoryEvent]()) -> OptOutJobData { + .init(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents, extractedProfile: extractedProfile) + } +} + extension DataBroker { static func mock(withId id: Int64) -> DataBroker { @@ -1549,3 +1556,33 @@ extension SecureStorageError: Equatable { } } } + +final class MockDataBrokerProtectionStatsPixelsRepository: DataBrokerProtectionStatsPixelsRepository { + var wasMarkStatsWeeklyPixelDateCalled: Bool = false + var wasMarkStatsMonthlyPixelDateCalled: Bool = false + var latestStatsWeeklyPixelDate: Date? + var latestStatsMonthlyPixelDate: Date? + + func markStatsWeeklyPixelDate() { + wasMarkStatsWeeklyPixelDateCalled = true + } + + func markStatsMonthlyPixelDate() { + wasMarkStatsMonthlyPixelDateCalled = true + } + + func getLatestStatsWeeklyPixelDate() -> Date? { + return latestStatsWeeklyPixelDate + } + + func getLatestStatsMonthlyPixelDate() -> Date? { + return latestStatsMonthlyPixelDate + } + + func clear() { + wasMarkStatsWeeklyPixelDateCalled = false + wasMarkStatsMonthlyPixelDateCalled = false + latestStatsWeeklyPixelDate = nil + latestStatsMonthlyPixelDate = nil + } +} From ee4564ff5923893a51e10cad08a7c01b82bf0687 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 10 Jun 2024 10:37:42 +0000 Subject: [PATCH 21/35] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 ++-- DuckDuckGo/ContentBlocker/macos-config.json | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index bfca0463492..4c4e0cd1856 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 = "\"9d50b5b84e7af8091afd5e974da66fa5\"" - public static let embeddedDataSHA = "f2b2e252e1c9310bd5b2e14949669c8ba67ad8444b1071ef21e15ca494c5375d" + public static let embeddedDataETag = "\"c46f529760ac695b143fb664350ad760\"" + public static let embeddedDataSHA = "00383f1d5e5e8b5da067c1527ed88027093422c85f362fcfa95cda12c5870987" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 9567d19fc16..5dbf1f1058d 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": 1717171433682, + "version": 1717679238533, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -1951,6 +1951,14 @@ { "selector": "#credential_picker_container", "type": "override" + }, + { + "selector": ".shop-display-ad", + "type": "hide-empty" + }, + { + "selector": ".row.full-bleed-row", + "type": "hide-empty" } ] }, @@ -4419,7 +4427,7 @@ ] }, "state": "enabled", - "hash": "63fc18f451878e2f937e381f67d05581" + "hash": "4ba5333801a460fca5bf360d684fa994" }, "exceptionHandler": { "exceptions": [ @@ -6542,6 +6550,7 @@ { "rule": "google-analytics.com/analytics.js", "domains": [ + "docs.llamaindex.ai", "doterra.com", "easyjet.com", "edx.org", @@ -6719,6 +6728,7 @@ "algomalegalclinic.com", "bodyelectricvitality.com.au", "cosmicbook.news", + "docs.llamaindex.ai", "eatroyo.com", "thesimsresource.com", "tradersync.com", @@ -8555,7 +8565,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "c117c47ceb4270ad4dcf16bdd888b8b4" + "hash": "af06003f902448cd26fd4e538e64e70f" }, "trackingCookies1p": { "settings": { From 23e2173ddcf94aee993012d9421cbde7d3426402 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 10 Jun 2024 10:37:42 +0000 Subject: [PATCH 22/35] Set marketing version to 1.92.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 49c6763b9d7..613bd7b6b36 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.91.0 +MARKETING_VERSION = 1.92.0 From faa7efe4ef2b068e502d3ddc744d8ccf36a72057 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 10 Jun 2024 10:48:57 +0000 Subject: [PATCH 23/35] Bump version to 1.92.0 (200) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 77e525aa553..d671ec6bfe9 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 199 +CURRENT_PROJECT_VERSION = 200 From 2746b6cf84ab92d69c62cbfbf37336346b2dfaf0 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 10 Jun 2024 03:57:00 -0700 Subject: [PATCH 24/35] Prevent showing multiple VPN uninstalled popovers (#2844) Task/Issue URL: https://app.asana.com/0/1193060753475688/1207481654222172/f Tech Design URL: CC: **Description**: This PR updates the VPN uninstaller flow to avoid showing multiple VPN uninstalled popovers at once. It does this by filtering out duplicates from the publisher we listen to, and checking that the window we're presenting on doesn't already have a presented view controller. --- .../NavigationBar/View/NavigationBarViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 02368d3191c..ed260a4841d 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -384,6 +384,7 @@ final class NavigationBarViewController: NSViewController { UserDefaults.netP .publisher(for: \.networkProtectionShouldShowVPNUninstalledMessage) .receive(on: DispatchQueue.main) + .removeDuplicates() .sink { [weak self] shouldShowUninstalledMessage in if shouldShowUninstalledMessage { self?.showVPNUninstalledFeedback() @@ -394,7 +395,8 @@ final class NavigationBarViewController: NSViewController { } @objc private func showVPNUninstalledFeedback() { - guard view.window?.isKeyWindow == true else { return } + // Only show the popover if we aren't already presenting one: + guard view.window?.isKeyWindow == true, (self.presentedViewControllers ?? []).isEmpty else { return } DispatchQueue.main.async { let viewController = PopoverMessageViewController(message: "DuckDuckGo VPN was uninstalled") From cfb6fe1d844547ba6822cb454cbec545530808a0 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 10 Jun 2024 04:56:17 -0700 Subject: [PATCH 25/35] Remove VPN launch pixels (#2845) Task/Issue URL: https://app.asana.com/0/1203137811378537/1206905458729874/f Tech Design URL: CC: **Description**: This PR removes VPN launch pixels. --- DuckDuckGo/Menus/MainMenuActions.swift | 5 -- DuckDuckGo/Statistics/PrivacyProPixel.swift | 7 -- .../WaitlistThankYouPromptPresenter.swift | 7 -- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 1 - .../Pixels/VPNPrivacyProPixel.swift | 52 ------------- .../Pixels/VPNPrivacyProPixelTests.swift | 74 ------------------- 6 files changed, 146 deletions(-) delete mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Pixels/VPNPrivacyProPixel.swift delete mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/Pixels/VPNPrivacyProPixelTests.swift diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 898f51ea748..25fcc148fc3 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -794,11 +794,6 @@ extension MainViewController { Application.appDelegate.subscriptionManager.accountManager.signOut() resetThankYouModalChecks(nil) UserDefaults.netP.networkProtectionEntitlementsExpired = false - - // Clear pixel data - PixelKit.shared?.clearFrequencyHistoryFor(pixel: PrivacyProPixel.privacyProFeatureEnabled) - PixelKit.shared?.clearFrequencyHistoryFor(pixel: PrivacyProPixel.privacyProBetaUserThankYouDBP) - PixelKit.shared?.clearFrequencyHistoryFor(pixel: PrivacyProPixel.privacyProBetaUserThankYouVPN) } @objc func resetDailyPixels(_ sender: Any?) { diff --git a/DuckDuckGo/Statistics/PrivacyProPixel.swift b/DuckDuckGo/Statistics/PrivacyProPixel.swift index a4093228f6a..5c2456afbf8 100644 --- a/DuckDuckGo/Statistics/PrivacyProPixel.swift +++ b/DuckDuckGo/Statistics/PrivacyProPixel.swift @@ -29,9 +29,6 @@ fileprivate let appDistribution = "direct" enum PrivacyProPixel: PixelKitEventV2 { // Subscription - case privacyProFeatureEnabled - case privacyProBetaUserThankYouVPN - case privacyProBetaUserThankYouDBP case privacyProSubscriptionActive case privacyProOfferScreenImpression case privacyProPurchaseAttempt @@ -74,10 +71,6 @@ enum PrivacyProPixel: PixelKitEventV2 { var name: String { switch self { - case .privacyProFeatureEnabled: return - "m_mac_\(appDistribution)_privacy-pro_feature_enabled" - case .privacyProBetaUserThankYouVPN: return "m_mac_\(appDistribution)_privacy-pro_promotion-dialog_shown_vpn" - case .privacyProBetaUserThankYouDBP: return "m_mac_\(appDistribution)_privacy-pro_promotion-dialog_shown_dbp" case .privacyProSubscriptionActive: return "m_mac_\(appDistribution)_privacy-pro_app_subscription_active" case .privacyProOfferScreenImpression: return "m_mac_\(appDistribution)_privacy-pro_offer_screen_impression" case .privacyProPurchaseAttempt: return "m_mac_\(appDistribution)_privacy-pro_terms-conditions_subscribe_click" diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 11281dcaba7..75af13e0a90 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -48,19 +48,12 @@ final class WaitlistThankYouPromptPresenter { // If the user tested both, the PIR prompt will be displayed. @MainActor func presentThankYouPromptIfNecessary(in window: NSWindow) { - // Wiring this here since it's mostly useful for rolling out PrivacyPro, and should - // go away once PPro is fully rolled out. - if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - PixelKit.fire(PrivacyProPixel.privacyProFeatureEnabled, frequency: .daily) - } - guard canShowPromptCheck() else { return } if isPIRBetaTester() { saveDidShowPromptCheck() - PixelKit.fire(PrivacyProPixel.privacyProBetaUserThankYouDBP, frequency: .dailyAndCount) presentPIRThankYouPrompt(in: window) } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 1b5a3610515..b80094a9349 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -422,7 +422,6 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { UserDefaults.netP.networkProtectionEntitlementsExpired = false case .invalidEntitlement: UserDefaults.netP.networkProtectionEntitlementsExpired = true - PixelKit.fire(VPNPrivacyProPixel.vpnAccessRevokedDialogShown, frequency: .dailyAndCount) guard let self else { return } Task { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Pixels/VPNPrivacyProPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Pixels/VPNPrivacyProPixel.swift deleted file mode 100644 index 2bbbf3a464e..00000000000 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Pixels/VPNPrivacyProPixel.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// VPNPrivacyProPixel.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/1206836019887720/f -/// -public enum VPNPrivacyProPixel: PixelKitEventV2 { - - /// Fired when PrivacyPro VPN access is revoked, and the dialog is shown. - /// - case vpnAccessRevokedDialogShown - - /// Fired only once when the VPN beta becomes disabled due to the start of PrivacyPro.. - /// - case vpnBetaStoppedWhenPrivacyProEnabled - - public var name: String { - switch self { - case .vpnAccessRevokedDialogShown: - return "vpn_access_revoked_dialog_shown" - case .vpnBetaStoppedWhenPrivacyProEnabled: - return "vpn_beta_stopped_when_privacy_pro_enabled" - } - } - - public var error: Error? { - nil - } - - public var parameters: [String: String]? { - nil - } -} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/Pixels/VPNPrivacyProPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/Pixels/VPNPrivacyProPixelTests.swift deleted file mode 100644 index 21d5472f492..00000000000 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/Pixels/VPNPrivacyProPixelTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// VPNPrivacyProPixelTests.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 PixelKit -import PixelKitTestingUtilities -import XCTest -@testable import NetworkProtectionUI - -final class VPNPrivacyProPixelTests: XCTestCase { - - private enum TestError: CustomNSError { - case testError - case underlyingError - - /// The domain of the error. - static var errorDomain: String { - "testDomain" - } - - /// The error code within the given domain. - var errorCode: Int { - switch self { - case .testError: return 1 - case .underlyingError: return 2 - } - } - - /// The user-info dictionary. - var errorUserInfo: [String: Any] { - switch self { - case .testError: - return [NSUnderlyingErrorKey: TestError.underlyingError] - case .underlyingError: - return [:] - } - } - } - - // MARK: - Test Firing Pixels - - /// This test verifies validates expectations when firing `VPNPrivacyProPixel`. - /// - /// This test verifies a few different things: - /// - That the pixel name is not changed by mistake. - /// - That when the pixel is fired its name and parameters are exactly what's expected. - /// - func testVPNPixelFireExpectations() { - fire(VPNPrivacyProPixel.vpnAccessRevokedDialogShown, - frequency: .dailyAndCount, - and: .expect(pixelName: "m_mac_vpn_access_revoked_dialog_shown"), - file: #filePath, - line: #line) - fire(VPNPrivacyProPixel.vpnBetaStoppedWhenPrivacyProEnabled, - frequency: .dailyAndCount, - and: .expect(pixelName: "m_mac_vpn_beta_stopped_when_privacy_pro_enabled"), - file: #filePath, - line: #line) - } -} From f7e7230418a40ead7333d4593a99bbc669519d09 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Mon, 10 Jun 2024 15:47:38 -0300 Subject: [PATCH 26/35] DBP: Update people-wizard.com (#2849) --- .../DataBrokerProtection/Resources/JSON/people-wizard.com.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json index c3875942637..394e39b40fd 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-wizard.com.json @@ -1,7 +1,7 @@ { "name": "People-Wizard.com", "url": "people-wizard.com", - "version": "0.1.7", + "version": "0.2.0", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -18,6 +18,7 @@ "actionType": "extract", "id": "1344b079-bb6b-417b-9c4d-eec858914b13", "selector": "[class^='ResultsTable__Record-sc']", + "noResultsSelector": "div[class^=ResultNotFound__NoResults]", "profile": { "name": { "selector": "[class^='ResultsTable__Name-sc']" From 54c312c8ba78366fc8c13aa278785effca4cb6df Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 10 Jun 2024 14:17:34 -0700 Subject: [PATCH 27/35] Update BSK for RMF survey changes (#2846) Task/Issue URL: https://app.asana.com/0/414235014887631/1207500645737162/f Tech Design URL: CC: Description: This PR updates BSK for recent survey changes to RMF. --- 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 6efa56f3f9f..3a420a5442c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12993,7 +12993,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 152.0.1; + version = 153.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 850e4da4bc1..8bc4cd17154 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" : "43d6c090699ddc1b92c0c016dc31b923fb06f59f", - "version" : "152.0.1" + "revision" : "b78ae617c7fe66244741f489158a1f40e567e674", + "version" : "153.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index c6c69d600eb..fc6b5e2af22 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: "152.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "153.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 393ebc0a173..ed49843a93b 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "152.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "153.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../AppLauncher"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index adb58da7877..86a9679004d 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: "152.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "153.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From b3be6103c63cc73d75471e60e2b6b441cabaec10 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 11 Jun 2024 05:15:04 +0000 Subject: [PATCH 28/35] Bump version to 1.92.0 (201) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index d671ec6bfe9..5cfcaedf6d9 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 200 +CURRENT_PROJECT_VERSION = 201 From 98fddd523286919a57e92feba06ab2f6affe736e Mon Sep 17 00:00:00 2001 From: amddg44 Date: Tue, 11 Jun 2024 11:57:52 +0200 Subject: [PATCH 29/35] Make passwords easier to discover (#2847) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206531758082882/f Tech Design URL: CC: Description: UI enhancements to make passwords manager easier for users to discover --- DuckDuckGo.xcodeproj/project.pbxproj | 12 + .../Key-Color-24.imageset/Contents.json | 12 + .../Key-Color-24.imageset/Key-Color-24.svg | 4 + .../Passwords-Add-128.imageset/Contents.json | 12 + .../Passwords-Add-128.svg | 18 + .../Bookmarks-Favorites-Color-24.svg | 6 + .../Contents.json | 12 + .../Clear-Recolorable-16.svg | 12 + .../Contents.json | 12 + DuckDuckGo/Common/Localizables/UserText.swift | 39 +- .../Model/DataImportShortcutsViewModel.swift | 54 + .../Model/DataImportSummaryViewModel.swift | 5 + .../Model/DataImportViewModel.swift | 24 +- .../View/BrowserImportMoreInfoView.swift | 2 +- .../View/DataImportShortcutsView.swift | 92 + .../View/DataImportSummaryView.swift | 228 ++- .../View/DataImportTypePicker.swift | 3 +- .../DataImport/View/DataImportView.swift | 47 +- DuckDuckGo/Localizable.xcstrings | 1725 ++++++++++++++--- .../PopoverMessageViewController.swift | 2 + .../NavigationBar/View/MoreOptionsMenu.swift | 6 +- .../View/NavigationBarPopovers.swift | 2 + .../View/NavigationBarViewController.swift | 38 + .../Model/PreferencesSection.swift | 2 +- .../View/PreferencesAutofillView.swift | 15 +- .../View/PreferencesRootView.swift | 2 +- .../Extensions/UserText+PasswordManager.swift | 10 +- .../PasswordManagementViewController.swift | 8 +- .../View/PasswordManager.storyboard | 51 +- .../View/SaveCredentialsViewController.swift | 17 +- .../PopoverMessageView.swift | 2 +- .../DataImport/DataImportViewModelTests.swift | 37 +- UnitTests/Menus/MoreOptionsMenuTests.swift | 4 +- 33 files changed, 2075 insertions(+), 440 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json create mode 100644 DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift create mode 100644 DuckDuckGo/DataImport/View/DataImportShortcutsView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3a420a5442c..39e7d25be7a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2481,12 +2481,16 @@ C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */; }; C13909FB2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; + C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */; }; + C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */; }; C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; C17CA7AD2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */; }; C17CA7AE2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */; }; C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */; }; C17CA7B32B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */; }; + C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; + C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; @@ -4065,9 +4069,11 @@ C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; C13909FA2B861039001626ED /* AutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionPresenter.swift; sourceTree = ""; }; + C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportShortcutsView.swift; sourceTree = ""; }; C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillNeverPromptWebsitesManager.swift; sourceTree = ""; }; C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopoversTests.swift; sourceTree = ""; }; C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillPopoverPresenter.swift; sourceTree = ""; }; + C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportShortcutsViewModel.swift; sourceTree = ""; }; C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPopoverPresenter.swift; sourceTree = ""; }; C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionPresenter.swift; sourceTree = ""; }; C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionExecutor.swift; sourceTree = ""; }; @@ -5360,6 +5366,7 @@ 4B8AC93426B3B2FD00879451 /* NSAlert+DataImport.swift */, B6B5F5832B03580A008DB58A /* RequestFilePermissionView.swift */, B677FC4E2B06376B0099EB04 /* ReportFeedbackView.swift */, + C16127ED2BDFB46400966BB9 /* DataImportShortcutsView.swift */, ); path = View; sourceTree = ""; @@ -7986,6 +7993,7 @@ B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */, B6A22B612B1E29D000ECD2BA /* DataImportSummaryViewModel.swift */, B6619EF82B111CBE00CD9186 /* InstructionsFormatParser.swift */, + C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */, ); path = Model; sourceTree = ""; @@ -9828,6 +9836,7 @@ B6B5F5802B024105008DB58A /* DataImportSummaryView.swift in Sources */, 4B9DB0422A983B24000927DB /* WaitlistDialogView.swift in Sources */, 3706FB57293F65D500E42796 /* AppPrivacyConfigurationDataProvider.swift in Sources */, + C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */, 857E5AF62A790B7000FC0FB4 /* PixelExperiment.swift in Sources */, 9F33445F2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, 3706FB58293F65D500E42796 /* LinkButton.swift in Sources */, @@ -10239,6 +10248,7 @@ 3706FC62293F65D500E42796 /* ThirdPartyBrowser.swift in Sources */, 3706FC63293F65D500E42796 /* CircularProgressView.swift in Sources */, 3706FC64293F65D500E42796 /* SuggestionContainer.swift in Sources */, + C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, 3706FC67293F65D500E42796 /* OperatingSystemVersionExtension.swift in Sources */, B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */, @@ -11280,6 +11290,7 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, + C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, 4B9579212AC687170062CA31 /* HardwareModel.swift in Sources */, @@ -11412,6 +11423,7 @@ 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, + C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */, 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json new file mode 100644 index 00000000000..c30b2bb83d8 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Key-Color-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg new file mode 100644 index 00000000000..c35cfc60c19 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Key-Color-24.imageset/Key-Color-24.svg @@ -0,0 +1,4 @@ + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json new file mode 100644 index 00000000000..71a4b8cbc9c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Passwords-Add-128.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg new file mode 100644 index 00000000000..cb07aafa6a5 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-Add-128.imageset/Passwords-Add-128.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg new file mode 100644 index 00000000000..7dc9f7901a4 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Bookmarks-Favorites-Color-24.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json new file mode 100644 index 00000000000..4c42ee8e0a7 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Bookmarks-Favorites-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Bookmarks-Favorites-Color-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg new file mode 100644 index 00000000000..be459c2cd0b --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Clear-Recolorable-16.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json new file mode 100644 index 00000000000..bc04fe84d64 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Clear-Recolorable-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Clear-Recolorable-16.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 02fb18ac463..2693a490c4b 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -396,10 +396,12 @@ struct UserText { static let restartBitwarden = NSLocalizedString("restart.bitwarden", value: "Restart Bitwarden", comment: "Button to restart Bitwarden application") static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again.") - static let autofillViewContentButton = NSLocalizedString("autofill.view-autofill-content", value: "View Autofill Content…", comment: "View Autofill Content Button name in the autofill settings") - static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Save and Autofill", comment: "Autofill settings section title") + static let autofillViewContentButtonPasswords = NSLocalizedString("autofill.view-autofill-content.passwords", value: "Open Passwords…", comment: "View Password Content Button title in the autofill Settings") + static let autofillViewContentButtonPaymentMethods = NSLocalizedString("autofill.view-autofill-content.payment-methods", value: "Open Payment Methods…", comment: "View Payment Methods Content Button title in the autofill Settings") + static let autofillViewContentButtonIdentities = NSLocalizedString("autofill.view-autofill-content.identities", value: "Open Identities…", comment: "View Identities Content Button title in the autofill Settings") + static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Ask to Save and Autofill", comment: "Autofill settings section title") static let autofillAskToSaveExplanation = NSLocalizedString("autofill.ask-to-save.explanation", value: "Receive prompts to save new information and autofill online forms.", comment: "Description of Autofill autosaving feature - used in settings") - static let autofillUsernamesAndPasswords = NSLocalizedString("autofill.usernames-and-passwords", value: "Usernames and passwords", comment: "Autofill autosaved data type") + static let autofillPasswords = NSLocalizedString("autofill.passwords", value: "Passwords", comment: "Autofill autosaved data type") static let autofillAddresses = NSLocalizedString("autofill.addresses", value: "Addresses", comment: "Autofill autosaved data type") static let autofillPaymentMethods = NSLocalizedString("autofill.payment-methods", value: "Payment methods", comment: "Autofill autosaved data type") static let autofillExcludedSites = NSLocalizedString("autofill.excluded-sites", value: "Excluded Sites", comment: "Autofill settings section title") @@ -420,7 +422,6 @@ struct UserText { static let downloadsOpenPopupOnCompletion = NSLocalizedString("downloads.open.on.completion", value: "Automatically open the Downloads panel when downloads complete", comment: "Checkbox to open a Download Manager popover when downloads are completed") // MARK: Password Manager - static let passwordManagement = NSLocalizedString("passsword.management", value: "Autofill", comment: "Used as title for password management user interface") static let passwordManagementAllItems = NSLocalizedString("passsword.management.all-items", value: "All Items", comment: "Used as title for the Autofill All Items option") static let passwordManagementLogins = NSLocalizedString("passsword.management.logins", value: "Passwords", comment: "Used as title for the Autofill Logins option") static let passwordManagementIdentities = NSLocalizedString("passsword.management.identities", value: "Identities", comment: "Used as title for the Autofill Identities option") @@ -431,19 +432,19 @@ struct UserText { static let passwordManagementUnlock = NSLocalizedString("passsword.management.unlock", value: "Unlock", comment: "Unlock Logins Vault menu") static let passwordManagementSavePayment = NSLocalizedString("passsword.management.save.payment", value: "Save Payment Method?", comment: "Title of dialog that allows the user to save a payment method") static let passwordManagementSaveAddress = NSLocalizedString("passsword.management.save.address", value: "Save Address?", comment: "Title of dialog that allows the user to save an address method") - static let passwordManagementSaveCredentialsPasswordManagerTitle = NSLocalizedString("passsword.management.save.credentials.password.manager.title", value: "Save Login to Bitwarden?", comment: "Title of the passwored manager section of dialog that allows the user to save credentials") + static let passwordManagementSaveCredentialsPasswordManagerTitle = NSLocalizedString("passsword.management.save.credentials.password.manager.title", value: "Save password to Bitwarden?", comment: "Title of the passwored manager section of dialog that allows the user to save credentials") static let passwordManagementSaveCredentialsUnlockPasswordManager = NSLocalizedString("passsword.management.save.credentials.unlock.password.manager", value: "Unlock Bitwarden to Save", comment: "In the password manager dialog, alerts the user that they need to unlock Bitworden before being able to save the credential") - static let passwordManagementSaveCredentialsFireproofCheckboxTitle = NSLocalizedString("passsword.management.save.credentials.fireproof.checkbox.title", value: "Fireproof?", comment: "In the password manager dialog, title of the section that allows the user to fireproof a website via a checkbox") + static let passwordManagementSaveCredentialsFireproofCheckboxTitle = NSLocalizedString("passsword.management.save.credentials.fireproof.checkbox.title", value: "Fireproof this website", comment: "In the password manager dialog, title of the section that allows the user to fireproof a website via a checkbox") static let passwordManagementSaveCredentialsFireproofCheckboxDescription = NSLocalizedString("passsword.management.save.credentials.fireproof.checkbox.description", value: "Keeps you signed in after using the Fire Button", comment: "In the password manager dialog, description of the section that allows the user to fireproof a website via a checkbox") static func passwordManagementSaveCredentialsAccountLabel(activeVault: String) -> String { let localized = NSLocalizedString("passsword.management.save.credentials.account.label", value: "Connected to %@", comment: "In the password manager dialog, label that specifies the password manager vault we are connected with") return String(format: localized, activeVault) } + static let passwordManagementTitle = NSLocalizedString("password.management.title", value: "Passwords & Autofill", comment: "Used as the title for menu item and related Settings page") static let settingsSuspended = NSLocalizedString("Settings…", comment: "Menu item") static let passwordManagerUnlockAutofill = NSLocalizedString("passsword.manager.unlock.autofill", value: "Unlock your Autofill info", comment: "In the password manager text of button to unlock autofill info") static let passwordManagerEmptyStateTitle = NSLocalizedString("passsword.manager.empty.state.title", value: "No logins or credit card info yet", comment: "In the password manager title when there are no items") static let passwordManagerEmptyStateMessage = NSLocalizedString("passsword.manager.empty.state.message", value: "If your logins are saved in another browser, you can import them into DuckDuckGo.", comment: "In the password manager message when there are no items") - static let importData = NSLocalizedString("Import", comment: "Menu item") static let passwordManagerAlertRemovePasswordConfirmation = NSLocalizedString("passsword.manager.alert.remove-password.confirmation", value: "Are you sure you want to delete this saved password", comment: "Text of the alert that asks the user to confirm they want to delete a password") static let passwordManagerAlertSaveChanges = NSLocalizedString("passsword.manager.alert.save-changes", value: "Save the changes you made?", comment: "Text of the alert that asks the user if the want to save the changes made") static let passwordManagerAlertDuplicatePassword = NSLocalizedString("passsword.manager.alert.duplicate.password", value: "Duplicate Password", comment: "Title of the alert that the password inserted already exists") @@ -457,7 +458,11 @@ struct UserText { static let importBookmarks = NSLocalizedString("import.browser.data.bookmarks", value: "Import Bookmarks…", comment: "Opens Import Browser Data dialog") static let importPasswords = NSLocalizedString("import.browser.data.passwords", value: "Import Passwords…", comment: "Opens Import Browser Data dialog") - static let importDataTitle = NSLocalizedString("import.browser.data", value: "Import Browser Data", comment: "Import Browser Data dialog title") + static let importDataTitle = NSLocalizedString("import.browser.data", value: "Import to DuckDuckGo", comment: "Import Browser Data dialog title") + static let importDataShortcutsTitle = NSLocalizedString("import.browser.data.shortcuts", value: "Almost done!", comment: "Import Browser Data dialog title for final stage when choosing shortcuts to enable") + static let importDataShortcutsSubtitle = NSLocalizedString("import.browser.data.shortcuts.subtitle", value: "You can always right-click on the browser toolbar to find more shortcuts like these.", comment: "Subtitle explaining how users can find toolbar shortcuts.") + static let importDataSourceTitle = NSLocalizedString("import.browser.data.source.title", value: "Import From", comment: "Import Browser Data title for option to choose source browser to import from") + static let importDataSubtitle = NSLocalizedString("import.browser.data.source.subtitle", value: "Access and manage your passwords in DuckDuckGo Settings > Passwords & Autofill.", comment: "Subtitle explaining where users can find imported passwords.") static let exportLogins = NSLocalizedString("export.logins.data", value: "Export Passwords…", comment: "Opens Export Logins Data dialog") static let exportBookmarks = NSLocalizedString("export.bookmarks.menu.item", value: "Export Bookmarks…", comment: "Export bookmarks menu item") @@ -754,6 +759,11 @@ struct UserText { static let bookmarkImportBookmarks = NSLocalizedString("import.bookmarks.bookmarks", value: "Bookmarks", comment: "Title text for the Bookmarks import option") + static let importShortcutsBookmarksTitle = NSLocalizedString("import.shortcuts.bookmarks.title", value: "Show Bookmarks Bar", comment: "Title for the setting to enable the bookmarks bar") + static let importShortcutsBookmarksSubtitle = NSLocalizedString("import.shortcuts.bookmarks.subtitle", value: "Put your favorite bookmarks in easy reach", comment: "Description for the setting to enable the bookmarks bar") + static let importShortcutsPasswordsTitle = NSLocalizedString("import.shortcuts.passwords.title", value: "Show Passwords Shortcut", comment: "Title for the setting to enable the passwords shortcut") + static let importShortcutsPasswordsSubtitle = NSLocalizedString("import.shortcuts.passwords.subtitle", value: "Keep passwords nearby in the address bar", comment: "Description for the setting to enable the passwords shortcut") + static let openDeveloperTools = NSLocalizedString("main.menu.show.inspector", value: "Open Developer Tools", comment: "Show Web Inspector/Open Developer Tools") static let closeDeveloperTools = NSLocalizedString("main.menu.close.inspector", value: "Close Developer Tools", comment: "Hide Web Inspector/Close Developer Tools") @@ -951,8 +961,8 @@ struct UserText { static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device") static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history") - static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") - static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") + static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Passwords Shortcut", comment: "Menu item for showing the passwords shortcut") + static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Passwords Shortcut", comment: "Menu item for hiding the passwords shortcut") static let showBookmarksShortcut = NSLocalizedString("pinning.show-bookmarks-shortcut", value: "Show Bookmarks Shortcut", comment: "Menu item for showing the bookmarks shortcut") static let hideBookmarksShortcut = NSLocalizedString("pinning.hide-bookmarks-shortcut", value: "Hide Bookmarks Shortcut", comment: "Menu item for hiding the bookmarks shortcut") @@ -1035,6 +1045,15 @@ struct UserText { value: "View", comment: "Button to view the recently autosaved password") + static let passwordManagerAutoPinnedPopoverText = NSLocalizedString("autofill.popover.passwords.auto-pinned.text", value: "Shortcut Added!", comment: "Text confirming the password manager has been pinned to the toolbar") + + static let passwordManagerPinnedPromptPopoverText = NSLocalizedString("autofill.popover.passwords.pin-prompt.text", + value: "Add passwords shortcut?", + comment: "Text prompting user to pin the password manager shortcut to the toolbar") + static let passwordManagerPinnedPromptPopoverButtonText = NSLocalizedString("autofill.popover.passwords.pin-prompt.button.text", + value: "Add Shortcut", + comment: "Button to pin the password manager shortcut to the toolbar") + static func openPasswordManagerButton(managerName: String) -> String { let localized = NSLocalizedString("autofill.popover.open-password-manager", value: "Open %@", comment: "Open password manager button") return String(format: localized, managerName) diff --git a/DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift new file mode 100644 index 00000000000..763c82a1b55 --- /dev/null +++ b/DuckDuckGo/DataImport/Model/DataImportShortcutsViewModel.swift @@ -0,0 +1,54 @@ +// +// DataImportShortcutsViewModel.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 DataImportShortcutsViewModel: ObservableObject { + typealias DataType = DataImport.DataType + + let dataTypes: Set? + private let prefs: AppearancePreferences + private let pinningManager: LocalPinningManager + + @Published var showBookmarksBarStatus: Bool { + didSet { + prefs.showBookmarksBar = showBookmarksBarStatus + } + } + + @Published var showPasswordsPinnedStatus: Bool { + didSet { + if showPasswordsPinnedStatus { + pinningManager.pin(.autofill) + NotificationCenter.default.post(name: .passwordsAutoPinned, object: nil) + } else { + pinningManager.unpin(.autofill) + } + } + } + + init(dataTypes: Set? = nil, prefs: AppearancePreferences = AppearancePreferences.shared, pinningManager: LocalPinningManager = LocalPinningManager.shared) { + self.dataTypes = dataTypes + self.prefs = prefs + self.pinningManager = pinningManager + + showBookmarksBarStatus = prefs.showBookmarksBar + showPasswordsPinnedStatus = pinningManager.isPinned(.autofill) + } +} diff --git a/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift index cb8612860a8..681db6827f5 100644 --- a/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportSummaryViewModel.swift @@ -55,5 +55,10 @@ struct DataImportSummaryViewModel { dataTypes.contains(dataType) ? results.last(where: { $0.dataType == dataType }) : nil } } +} +extension DataImportSummaryViewModel { + func resultsFiltered(by dataType: DataType) -> [DataTypeImportResult] { + results.filter { $0.dataType == dataType } + } } diff --git a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift index 7e6211da9df..aa5aff6ac63 100644 --- a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift @@ -70,6 +70,7 @@ struct DataImportViewModel { case fileImport(dataType: DataType, summary: Set = []) case summary(Set, isFileImport: Bool = false) case feedback + case shortcuts(Set) var isFileImport: Bool { if case .fileImport = self { true } else { false } @@ -338,8 +339,23 @@ struct DataImportViewModel { // errors occurred during import: show feedback screen self.screen = .feedback } else { - // When we skip a manual import, and there are no next non-imported data types, we dismiss - self.dismiss(using: dismiss) + // When we skip a manual import, and there are no next non-imported data types, + // if some data was successfully imported we present the shortcuts screen, otherwise we dismiss + var dataTypes: Set = [] + + // Filter out only the successful results with a positive count of successful summaries + for dataTypeImportResult in summary { + guard case .success(let summary) = dataTypeImportResult.result, summary.successful > 0 else { + continue + } + dataTypes.insert(dataTypeImportResult.dataType) + } + + if !dataTypes.isEmpty { + self.screen = .shortcuts(dataTypes) + } else { + self.dismiss(using: dismiss) + } } } @@ -630,11 +646,13 @@ extension DataImportViewModel { if let screen = screenForNextDataTypeRemainingToImport(after: DataType.allCases.last(where: dataTypes.contains)) { return .next(screen) } else { - return .done + return .next(.shortcuts(dataTypes)) } case .feedback: return .submit + case .shortcuts: + return .done } } diff --git a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift index 24806677bbd..670a0291315 100644 --- a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift +++ b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoView.swift @@ -30,7 +30,7 @@ struct BrowserImportMoreInfoView: View { switch source { case .chrome, .chromium, .coccoc, .edge, .brave, .opera, .operaGX, .vivaldi: Text(""" - If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password. + After clicking import, your computer may ask you to enter a password. You may need to enter your password two times before importing starts. DuckDuckGo will not see that password. Imported passwords are stored securely using encryption. """, comment: "Warning that Chromium data import would require entering system passwords.") diff --git a/DuckDuckGo/DataImport/View/DataImportShortcutsView.swift b/DuckDuckGo/DataImport/View/DataImportShortcutsView.swift new file mode 100644 index 00000000000..a0ed0b448da --- /dev/null +++ b/DuckDuckGo/DataImport/View/DataImportShortcutsView.swift @@ -0,0 +1,92 @@ +// +// DataImportShortcutsView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DataImportShortcutsView: ModalView { + + typealias DataType = DataImport.DataType + + @ObservedObject private var model: DataImportShortcutsViewModel + + init(model: DataImportShortcutsViewModel = DataImportShortcutsViewModel(), dataTypes: Set? = nil) { + self.init(model: .init(dataTypes: dataTypes)) + } + + init(model: DataImportShortcutsViewModel) { + self.model = model + } + + var body: some View { + + VStack(alignment: .leading, spacing: 8) { + VStack(spacing: 0) { + if let dataTypes = model.dataTypes, dataTypes.contains(.bookmarks) { + importShortcutsRow(image: Image(.bookmarksFavoritesColor24), + title: UserText.importShortcutsBookmarksTitle, + subtitle: UserText.importShortcutsBookmarksSubtitle, + isOn: $model.showBookmarksBarStatus) + } + + if let dataTypes = model.dataTypes, dataTypes.count > 1 { + Divider() + .padding(.leading) + } + + importShortcutsRow(image: Image(.keyColor24), + title: UserText.importShortcutsPasswordsTitle, + subtitle: UserText.importShortcutsPasswordsSubtitle, + isOn: $model.showPasswordsPinnedStatus) + } + .roundedBorder() + } + + importShortcutsSubtitle() + } +} + +private func importShortcutsRow(image: Image, title: String, subtitle: String, isOn: Binding) -> some View { + HStack { + image + VStack(alignment: .leading) { + Text(title) + Text(subtitle) + .font(.subheadline) + .foregroundColor(.greyText) + } + .padding(.top, 0) + .padding(.bottom, 1) + Spacer() + Toggle("", isOn: isOn) + .toggleStyle(.switch) + } + .padding(.horizontal) + .padding(.vertical, 10) +} + +private func importShortcutsSubtitle() -> some View { + Text(UserText.importDataShortcutsSubtitle) + .font(.subheadline) + .foregroundColor(Color(.greyText)) + .padding(.top, 8) + .padding(.leading, 8) +} + +#Preview { + DataImportShortcutsView() +} diff --git a/DuckDuckGo/DataImport/View/DataImportSummaryView.swift b/DuckDuckGo/DataImport/View/DataImportSummaryView.swift index b385a90c668..49affa7de50 100644 --- a/DuckDuckGo/DataImport/View/DataImportSummaryView.swift +++ b/DuckDuckGo/DataImport/View/DataImportSummaryView.swift @@ -22,6 +22,7 @@ struct DataImportSummaryView: View { typealias DataType = DataImport.DataType typealias Summary = DataImport.DataTypeSummary + typealias DataTypeImportResult = DataImportViewModel.DataTypeImportResult let model: DataImportSummaryViewModel @@ -33,7 +34,7 @@ struct DataImportSummaryView: View { self.model = model } - private let zeroString = "0" + private let zero = 0 var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -43,7 +44,7 @@ struct DataImportSummaryView: View { Text("Import Results:", comment: "Data Import result summary headline") case .importComplete(.bookmarks), - .fileImportComplete(.bookmarks): + .fileImportComplete(.bookmarks): Text("Bookmarks Import Complete:", comment: "Bookmarks Data Import result summary headline") case .fileImportComplete(.passwords): @@ -51,105 +52,146 @@ struct DataImportSummaryView: View { } }().padding(.bottom, 4) - ForEach(model.results, id: \.dataType) { item in - switch (item.dataType, item.result) { - case (.bookmarks, .success(let summary)): - HStack { - successImage() - Text("Bookmarks:", - comment: "Data import summary format of how many bookmarks (%lld) were successfully imported.") - + Text(" " as String) - + Text(String(summary.successful)).bold() + VStack { + ForEach(model.resultsFiltered(by: .bookmarks), id: \.dataType) { item in + switch item.result { + case (.success(let summary)): + bookmarksSuccessSummary(summary) + case (.failure(let error)) where error.errorType == .noData: + importSummaryRow(image: .failed, + text: "Bookmarks:", + comment: "Data import summary format of how many bookmarks were successfully imported.", + count: zero) + case (.failure): + importSummaryRow(image: .failed, + text: "Bookmark import failed.", + comment: "Data import summary message of failed bookmarks import.", + count: nil) } - if summary.duplicate > 0 { - HStack { - skippedImage() - Text("Duplicate Bookmarks Skipped:", - comment: "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.") - + Text(" " as String) - + Text(String(summary.duplicate)).bold() - } - } - if summary.failed > 0 { - HStack { - failureImage() - Text("Bookmark import failed:", - comment: "Data import summary format of how many bookmarks (%lld) failed to import.") - + Text(" " as String) - + Text(String(summary.failed)).bold() + } + } + .applyConditionalModifiers(!model.resultsFiltered(by: .bookmarks).isEmpty) + + VStack { + ForEach(model.resultsFiltered(by: .passwords), id: \.dataType) { item in + switch item.result { + case (.failure(let error)): + if error.errorType == .noData { + importSummaryRow(image: .failed, + text: "Passwords:", + comment: "Data import summary format of how many passwords were successfully imported.", + count: zero) + } else { + importSummaryRow(image: .failed, + text: "Password import failed.", + comment: "Data import summary message of failed passwords import.", + count: nil) } - } - case (.bookmarks, .failure(let error)) where error.errorType == .noData: - HStack { - skippedImage() - Text("Bookmarks:", - comment: "Data import summary format of how many bookmarks were successfully imported.") - + Text(" " as String) - + Text(zeroString).bold() + case (.success(let summary)): + passwordsSuccessSummary(summary) } + } + } + .applyConditionalModifiers(!model.resultsFiltered(by: .passwords).isEmpty) - case (.bookmarks, .failure): - HStack { - failureImage() - Text("Bookmark import failed.", - comment: "Data import summary message of failed bookmarks import.") - } + if !model.resultsFiltered(by: .passwords).isEmpty { + importPasswordSubtitle() + } + } + } +} - case (.passwords, .failure(let error)): - if error.errorType == .noData { - HStack { - skippedImage() - Text("Passwords:", - comment: "Data import summary format of how many passwords were successfully imported.") - + Text(" " as String) - + Text(zeroString).bold() - } - } else { - HStack { - failureImage() - Text("Password import failed.", - comment: "Data import summary message of failed passwords import.") - } - } +func bookmarksSuccessSummary(_ summary: DataImport.DataTypeSummary) -> some View { + VStack { + importSummaryRow(image: .success, + text: "Bookmarks:", + comment: "Data import summary format of how many bookmarks (%lld) were successfully imported.", + count: summary.successful) + if summary.duplicate > 0 { + lineSeparator() + importSummaryRow(image: .failed, + text: "Duplicate Bookmarks Skipped:", + comment: "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.", + count: summary.duplicate) + } + if summary.failed > 0 { + lineSeparator() + importSummaryRow(image: .failed, + text: "Bookmark import failed:", + comment: "Data import summary format of how many bookmarks (%lld) failed to import.", + count: summary.failed) + } + } +} - case (.passwords, .success(let summary)): - HStack { - successImage() - Text("Passwords:", - comment: "Data import summary format of how many passwords (%lld) were successfully imported.") - + Text(" " as String) - + Text(String(summary.successful)).bold() - } - if summary.failed > 0 { - HStack { - failureImage() - Text("Password import failed: ", - comment: "Data import summary format of how many passwords (%lld) failed to import.") - + Text(" " as String) - + Text(String(summary.failed)).bold() - } - } - } - } +private func passwordsSuccessSummary(_ summary: DataImport.DataTypeSummary) -> some View { + VStack { + importSummaryRow(image: .success, + text: "Passwords:", + comment: "Data import summary format of how many passwords (%lld) were successfully imported.", + count: summary.successful) + if summary.failed > 0 { + lineSeparator() + importSummaryRow(image: .failed, + text: "Password import failed: ", + comment: "Data import summary format of how many passwords (%lld) failed to import.", + count: summary.failed) + } + } +} + +private func importPasswordSubtitle() -> some View { + Text(UserText.importDataSubtitle) + .font(.subheadline) + .foregroundColor(Color(.greyText)) + .padding(.top, -2) + .padding(.leading, 8) +} + +private func importSummaryRow(image: Image, text: LocalizedStringKey, comment: StaticString, count: Int?) -> some View { + HStack(spacing: 0) { + image + .frame(width: 16, height: 16) + .padding(.trailing, 14) + Text(text, comment: comment) + Text(verbatim: " ") + if let count = count { + Text(String(count)).bold() } + Spacer() } +} +private func lineSeparator() -> some View { + Divider() + .padding(EdgeInsets(top: 5, leading: 0, bottom: 8, trailing: 0)) } -private func successImage() -> some View { - Image(.successCheckmark) - .frame(width: 16, height: 16) +private extension Image { + static let success = Image(.successCheckmark) + static let failed = Image(.clearRecolorable16) } -private func failureImage() -> some View { - Image(.error) - .frame(width: 16, height: 16) +private struct ConditionalModifier: ViewModifier { + let applyModifiers: Bool + + func body(content: Content) -> some View { + if applyModifiers { + content + .padding([.leading, .vertical]) + .padding(.trailing, 0) + .roundedBorder() + } else { + content + } + } } -private func skippedImage() -> some View { - Image(.skipped) - .frame(width: 16, height: 16) +private extension View { + func applyConditionalModifiers(_ condition: Bool) -> some View { + modifier(ConditionalModifier(applyModifiers: condition)) + } } #if DEBUG @@ -157,17 +199,19 @@ private func skippedImage() -> some View { VStack { HStack { DataImportSummaryView(model: .init(source: .chrome, results: [ -// .init(.bookmarks, .success(.init(successful: 123, duplicate: 456, failed: 7890))), -// .init(.passwords, .success(.init(successful: 123, duplicate: 456, failed: 7890))), -// .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .bookmarks, errorType: .dataCorrupted))), -// .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), - .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), - .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + .init(.bookmarks, .success(.init(successful: 123, duplicate: 456, failed: 7890))), + // .init(.passwords, .success(.init(successful: 123, duplicate: 456, failed: 7890))), + // .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .bookmarks, errorType: .dataCorrupted))), + // .init(.bookmarks, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + // .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + // .init(.passwords, .failure(DataImportViewModel.TestImportError(action: .passwords, errorType: .keychainError))), + // .init(.passwords, .success(.init(successful: 100, duplicate: 0, failed: 0))) + .init(.passwords, .success(.init(successful: 100, duplicate: 30, failed: 40))) ])) .padding(EdgeInsets(top: 20, leading: 20, bottom: 16, trailing: 20)) Spacer() } } - .frame(width: 512) + .frame(width: 512, height: 400) } #endif diff --git a/DuckDuckGo/DataImport/View/DataImportTypePicker.swift b/DuckDuckGo/DataImport/View/DataImportTypePicker.swift index db6c2c9dd95..7dbb85d3c57 100644 --- a/DuckDuckGo/DataImport/View/DataImportTypePicker.swift +++ b/DuckDuckGo/DataImport/View/DataImportTypePicker.swift @@ -28,9 +28,8 @@ struct DataImportTypePicker: View { var body: some View { VStack(alignment: .leading) { - Text("Select data to import:", + Text("Select Data to Import:", comment: "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.") - .bold() ForEach(DataImport.DataType.allCases, id: \.self) { dataType in // display all types for a browser disabling unavailable options diff --git a/DuckDuckGo/DataImport/View/DataImportView.swift b/DuckDuckGo/DataImport/View/DataImportView.swift index 2302b544211..0d665affca3 100644 --- a/DuckDuckGo/DataImport/View/DataImportView.swift +++ b/DuckDuckGo/DataImport/View/DataImportView.swift @@ -47,7 +47,7 @@ struct DataImportView: ModalView { viewBody() .padding(.leading, 20) .padding(.trailing, 20) - .padding(.bottom, 32) + .padding(.bottom, 20) // if import in progress… if let importProgress = model.importProgress { @@ -77,21 +77,32 @@ struct DataImportView: ModalView { private func viewHeader() -> some View { VStack(alignment: .leading, spacing: 0) { - Text(UserText.importDataTitle) - .bold() - .padding(.bottom, 16) + if case .shortcuts = model.screen { + Text(UserText.importDataShortcutsTitle) + .font(.title2.weight(.semibold)) + .padding(.bottom, 24) - // browser to import data from picker popup - if case .feedback = model.screen {} else { - DataImportSourcePicker(importSources: model.availableImportSources, selectedSource: model.importSource) { importSource in - model.update(with: importSource) + } else { + Text(UserText.importDataTitle) + .font(.title2.weight(.semibold)) + .padding(.bottom, 24) + + Text(UserText.importDataSourceTitle) + .padding(.bottom, 16) + + // browser to import data from picker popup + if case .feedback = model.screen {} else { + DataImportSourcePicker(importSources: model.availableImportSources, selectedSource: model.importSource) { importSource in + model.update(with: importSource) + } + .disabled(model.isImportSourcePickerDisabled) + .padding(.bottom, 16) } - .disabled(model.isImportSourcePickerDisabled) - .padding(.bottom, 24) } } } + // swiftlint:disable:next cyclomatic_complexity private func viewBody() -> some View { VStack(alignment: .leading, spacing: 0) { // body @@ -109,6 +120,8 @@ struct DataImportView: ModalView { DataImportTypePicker(viewModel: $model) .disabled(model.isImportSourcePickerDisabled) + importPasswordSubtitle() + case .moreInfo: // you will be asked for your keychain password blah blah... BrowserImportMoreInfoView(source: model.importSource) @@ -146,6 +159,10 @@ struct DataImportView: ModalView { model.initiateImport(fileURL: url) } + if dataType == .passwords { + importPasswordSubtitle() + } + case .summary(let dataTypes, let isFileImport): DataImportSummaryView(model, dataTypes: dataTypes, isFileImport: isFileImport) @@ -154,6 +171,9 @@ struct DataImportView: ModalView { .padding(.bottom, 20) ReportFeedbackView(model: $model.reportModel) + + case .shortcuts(let dataTypes): + DataImportShortcutsView(dataTypes: dataTypes) } } } @@ -189,6 +209,13 @@ struct DataImportView: ModalView { } } + private func importPasswordSubtitle() -> some View { + Text(UserText.importDataSubtitle) + .font(.subheadline) + .foregroundColor(Color(.greyText)) + .padding(.top, 16) + } + private func handleImportProgress(_ progress: TaskProgress) async { // receive import progress update events // the loop is completed on the import task diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 4503c03e710..621f96ec339 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -1239,6 +1239,59 @@ } } }, + "After clicking import, your computer may ask you to enter a password. You may need to enter your password two times before importing starts. DuckDuckGo will not see that password.\n\nImported passwords are stored securely using encryption." : { + "comment" : "Warning that Chromium data import would require entering system passwords.", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachdem du auf Importieren geklickt hast, fordert dich dein Computer möglicherweise auf, ein Passwort einzugeben. Möglicherweise musst du dein Passwort zweimal eingeben, bevor der Import beginnt. DuckDuckGo wird dieses Passwort nicht sehen.\n\nImportierte Passwörter werden durch Verschlüsselung sicher gespeichert." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es posible que tu ordenador solicite que introduzcas una contraseña después de hacer clic en importar. Puede que tengas que introducir tu contraseña dos veces antes de empezar a importar. DuckDuckGo no verá esa contraseña.\n\nLas contraseñas importadas se almacenan de forma segura mediante encriptación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Après avoir cliqué sur Importer, votre ordinateur pourra vous demander de saisir un mot de passe. Vous devrez peut-être saisir votre mot de passe deux fois avant le lancement de l'importation. DuckDuckGo ne verra pas ce mot de passe.\n\nLes mots de passe importés sont stockés en toute sécurité grâce à un cryptage." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dopo aver fatto clic su \"Importa\", il tuo computer potrebbe chiederti di inserire una password e potrebbe essere necessario inserirla due volte prima che l'importazione abbia inizio. DuckDuckGo non visualizzerà mai questa password.\n\nLe password importate vengono archiviate in modo sicuro utilizzando la crittografia." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nadat je op importeren hebt geklikt, kan je computer je vragen om een wachtwoord in te voeren. Mogelijk moet je je wachtwoord twee keer invoeren voordat het importeren begint. DuckDuckGo zal dat wachtwoord niet zien.\n\nGeïmporteerde wachtwoorden worden veilig opgeslagen door middel van versleuteling." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Po kliknięciu opcji importu komputer może Cię poprosić o wprowadzenie hasła. Przed rozpoczęciem importowania może być konieczne dwukrotne wprowadzenie hasła. DuckDuckGo nie zobaczy tego hasła.\n\nZaimportowane hasła są bezpiecznie przechowywane przy użyciu szyfrowania." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Depois de clicares em Importar, o teu computador pode pedir-te para introduzires uma palavra-passe. Poderás ter de a introduzir duas vezes antes a importação começar. O DuckDuckGo não verá essa palavra-passe.\n\nAs palavras-passe importadas são armazenadas com segurança por meio de encriptação." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "После нажатия кнопки «Импорт» компьютер может попросить вас ввести пароль. Прежде чем начнется импортирование, пароль, возможно, придется ввести дважды. DuckDuckGo его не увидит.\n\nИмпортированные пароли надежно хранятся в зашифрованном виде." + } + } + } + }, "after.bitwarden.installation.info" : { "comment" : "Setup of the integration with Bitwarden app", "extractionState" : "extracted_with_value", @@ -1324,7 +1377,7 @@ "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Certains signets sont mal formatés ou trop longs et n'ont pas été synchronisés.\n" + "value" : "Certains signets sont mal formatés ou trop longs et n'ont pas été synchronisés." } }, "it" : { @@ -3399,55 +3452,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Speichern und autovervollständigen" + "value" : "Zum Speichern und automatischen Ausfüllen auffordern" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save and Autofill" + "value" : "Ask to Save and Autofill" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar y autocompletar" + "value" : "Solicitar guardar y autocompletar" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrement et saisie automatique" + "value" : "Demander l'enregistrement et la saisie automatique" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Salva e compila automaticamente" + "value" : "Chiedi di salvare e compilare automaticamente" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opslaan en automatisch invullen" + "value" : "Vragen om op te slaan en automatisch in te vullen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisywanie i autouzupełnianie" + "value" : "Poproś o zapisywanie i autouzupełnianie" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar e preencher automaticamente" + "value" : "Pedir para guardar e preencher automaticamente" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранение и автозаполнение" + "value" : "Предлагать сохранение и автозаполнение данных" } } } @@ -5252,6 +5305,66 @@ } } }, + "autofill.passwords" : { + "comment" : "Autofill autosaved data type", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли" + } + } + } + }, "autofill.payment-methods" : { "comment" : "Autofill autosaved data type", "extractionState" : "extracted_with_value", @@ -5672,6 +5785,186 @@ } } }, + "autofill.popover.passwords.auto-pinned.text" : { + "comment" : "Text confirming the password manager has been pinned to the toolbar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut hinzugefügt!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Shortcut Added!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Acceso directo añadido!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raccourci ajouté !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scorciatoia aggiunta." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Snelkoppeling toegevoegd!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodano skrót!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atalho adicionado!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык добавлен!" + } + } + } + }, + "autofill.popover.passwords.pin-prompt.button.text" : { + "comment" : "Button to pin the password manager shortcut to the toolbar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut hinzufügen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add Shortcut" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir acceso directo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un raccourci" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi scorciatoia" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sneltoets toevoegen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj skrót" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar atalho" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить ярлык" + } + } + } + }, + "autofill.popover.passwords.pin-prompt.text" : { + "comment" : "Text prompting user to pin the password manager shortcut to the toolbar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort-Shortcut hinzufügen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add passwords shortcut?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Añadir acceso directo a contraseñas?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un raccourci pour les mots de passe ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungere una scorciatoia per le password?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoordsnelkoppeling toevoegen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodać skrót do haseł?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar atalho de palavras-passe?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить ярлык для менеджера паролей?" + } + } + } + }, "autofill.popover.settings-button" : { "comment" : "Open Settings Button", "extractionState" : "extracted_with_value", @@ -5854,7 +6147,7 @@ }, "autofill.usernames-and-passwords" : { "comment" : "Autofill autosaved data type", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -5914,7 +6207,7 @@ }, "autofill.view-autofill-content" : { "comment" : "View Autofill Content Button name in the autofill settings", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -5972,6 +6265,186 @@ } } }, + "autofill.view-autofill-content.identities" : { + "comment" : "View Identities Content Button title in the autofill Settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identitäten öffnen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Identities…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir identidades…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les identités…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri identità..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identiteiten openen…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz tożsamości…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir identidades…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть учетные данные…" + } + } + } + }, + "autofill.view-autofill-content.passwords" : { + "comment" : "View Password Content Button title in the autofill Settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter öffnen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Passwords…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir contraseñas…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les mots de passe…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri password…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden openen..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz hasła…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir palavras-passe…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть пароли..." + } + } + } + }, + "autofill.view-autofill-content.payment-methods" : { + "comment" : "View Payment Methods Content Button title in the autofill Settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zahlungsmethoden öffnen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Payment Methods…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir métodos de pago…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir les modes de paiement…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri metodi di pagamento..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betaalmethoden openen..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz metody płatności…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir métodos de pagamento…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть способы оплаты…" + } + } + } + }, "automatically.clear.data" : { "comment" : "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.", "extractionState" : "extracted_with_value", @@ -22309,6 +22782,7 @@ }, "If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password.\n\nImported passwords are stored securely using encryption." : { "comment" : "Warning that Chromium data import would require entering system passwords.", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -22362,6 +22836,7 @@ }, "Import" : { "comment" : "Menu item", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -22939,55 +23414,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Browserdaten importieren" + "value" : "In DuckDuckGo importieren" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Import Browser Data" + "value" : "Import to DuckDuckGo" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Importar datos del navegador" + "value" : "Importar a DuckDuckGo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Importer les données du navigateur" + "value" : "Importer dans DuckDuckGo" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Importa dati browser" + "value" : "Importa in DuckDuckGo" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Browsergegevens importeren" + "value" : "Importeren naar DuckDuckGo" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Importuj dane przeglądarki" + "value" : "Importuj do DuckDuckGo" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Importar dados do navegador" + "value" : "Importar para o DuckDuckGo" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Импорт данных браузера" + "value" : "Импорт в DuckDuckGo" } } } @@ -23112,6 +23587,246 @@ } } }, + "import.browser.data.shortcuts" : { + "comment" : "Import Browser Data dialog title for final stage when choosing shortcuts to enable", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fast fertig!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Almost done!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Casi listo!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "C'est presque terminé !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quasi finito!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijna klaar!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prawie gotowe!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quase pronto!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Почти готово!" + } + } + } + }, + "import.browser.data.shortcuts.subtitle" : { + "comment" : "Subtitle explaining how users can find toolbar shortcuts.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst jederzeit mit der rechten Maustaste auf die Symbolleiste des Browsers klicken, um weitere Shortcuts wie diese zu finden." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You can always right-click on the browser toolbar to find more shortcuts like these." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siempre puedes hacer clic con el botón derecho en la barra de herramientas del navegador para encontrar más accesos directos como estos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous pouvez toujours faire un clic droit sur la barre d'outils du navigateur pour accéder à d'autres raccourcis de ce type." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Per trovare altre scorciatoie come queste, puoi sempre fare clic con il tasto destro del mouse sulla barra degli strumenti del browser." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je kunt altijd met de rechtermuisknop op de werkbalk van de browser klikken om meer van deze snelkoppelingen te vinden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawsze możesz kliknąć prawym przyciskiem myszy pasek narzędzi przeglądarki, aby znaleźć więcej takich skrótów." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podes sempre clicar com o botão direito do rato na barra de ferramentas do navegador para encontrares mais atalhos como estes." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Другие ярлыки можно всегда найти, щелкнув по панели инструментов браузера правой кнопкой мыши." + } + } + } + }, + "import.browser.data.source.subtitle" : { + "comment" : "Subtitle explaining where users can find imported passwords.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst deine Passwörter unter DuckDuckGo-Einstellungen > Passwörter & Autovervollständigen verwalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Access and manage your passwords in DuckDuckGo Settings > Passwords & Autofill." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consulta y gestiona tus contraseñas en Ajustes de DuckDuckGo > Contraseñas y autocompletar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accédez à vos mots de passe et gérez-les dans Paramètres de DuckDuckGo > Mots de passe et saisie automatique." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accedi e gestisci le tue password in Impostazioni di DuckDuckGo > Password e compilazione automatica." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open en beheer je wachtwoorden in DuckDuckGo Instellingen > Wachtwoorden en automatisch aanvullen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzyskaj dostęp do swoich haseł i zarządzaj nimi w obszarze Ustawienia DuckDuckGo > Hasła i autouzupełnianie." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acede e faz a gestão das tuas palavras-passe em Definições do DuckDuckGo > Palavras-passe e preenchimento automático." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Просмотреть и проконтролировать пароли в DuckDuckGo можно в разделе «Настройки > Пароли и автозаполнение»." + } + } + } + }, + "import.browser.data.source.title" : { + "comment" : "Import Browser Data title for option to choose source browser to import from", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importieren aus" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Import From" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar desde" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer depuis" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa da" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importeren vanuit" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importuj z" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar de" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импорт из" + } + } + } + }, "import.csv.instructions.bitwarden" : { "comment" : "Instructions to import Passwords as CSV from Bitwarden.\n%2$s - app name (Bitwarden)\n%7$@ - hamburger menu icon\n%9$@ - “Select Bitwarden CSV File” button\n**bold text**; _italic text_", "extractionState" : "extracted_with_value", @@ -25632,6 +26347,246 @@ } } }, + "import.shortcuts.bookmarks.subtitle" : { + "comment" : "Description for the setting to enable the bookmarks bar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Halte deine Lieblingslesezeichen griffbereit" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Put your favorite bookmarks in easy reach" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pon tus marcadores favoritos a tu alcance" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Placez vos signets préférés à portée de main" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni i tuoi segnalibri preferiti a portata di mano" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Breng je favoriete bladwijzers binnen handbereik" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umieść ulubione zakładki w łatwo dostępnym miejscu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coloca os teus marcadores favoritos num local de fácil alcance" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Любимые закладки всегда под рукой" + } + } + } + }, + "import.shortcuts.bookmarks.title" : { + "comment" : "Title for the setting to enable the bookmarks bar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lesezeichenleiste anzeigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Bookmarks Bar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar barra de marcadores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher la barre des signets" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra la barra dei segnalibri" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bladwijzerbalk tonen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż pasek zakładek" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar barra de marcadores" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать панель закладок" + } + } + } + }, + "import.shortcuts.passwords.subtitle" : { + "comment" : "Description for the setting to enable the passwords shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter in der Adressleiste aufbewahren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep passwords nearby in the address bar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantén las contraseñas a mano en la barra de direcciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gardez vos mots de passe à portée de main dans la barre d'adresse" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni le password a portata di mano nella barra degli indirizzi" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Houd wachtwoorden bij via de adresbalk" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj hasła w pobliżu, na pasku adresu" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantém as palavras-passe por perto na barra de endereços" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удобное хранение паролей в адресной строке" + } + } + } + }, + "import.shortcuts.passwords.title" : { + "comment" : "Title for the setting to enable the passwords shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort-Shortcut anzeigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Passwords Shortcut" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar acceso directo a contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher le raccourci des mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra scorciatoia per le password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Snelkoppeling voor wachtwoorden weergeven" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż skrót do haseł" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar atalho de palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать ярлык для паролей" + } + } + } + }, "invite.dialog.get.started.button" : { "comment" : "Get Started button on an invite dialog", "extractionState" : "extracted_with_value", @@ -32415,7 +33370,7 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "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}🔒" } }, @@ -32427,43 +33382,43 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "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", + "state" : "translated", "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", + "state" : "translated", "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", + "state" : "translated", "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", + "state" : "translated", "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", + "state" : "translated", "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", + "state" : "translated", "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" } } @@ -34024,7 +34979,7 @@ }, "passsword.management" : { "comment" : "Used as title for password management user interface", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -34629,55 +35584,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fireproof?" + "value" : "„Fireproof“ diese Website?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Fireproof?" + "value" : "Fireproof this website" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "¿Marcar como Fireproof?" + "value" : "Marcar el sitio web como Fireproof" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Mode coupe-feu (Fireproof) ?" + "value" : "Placer ce site Web en mode coupe-feu" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Attivare Fireproof?" + "value" : "Fireproof questo sito" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Fireproof?" + "value" : "Deze website fireproof maken" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustawić jako Fireproof?" + "value" : "Zabezpiecz tę witrynę funkcją Fireproof" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizar o Fireproof?" + "value" : "Faz o fireproof deste site" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сделать огнеупорным?" + "value" : "Fireproof-защита для сайта" } } } @@ -34689,55 +35644,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Login bei Bitwarden speichern?" + "value" : "Passwort bei Bitwarden speichern?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save Login to Bitwarden?" + "value" : "Save password to Bitwarden?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "¿Guardar inicio de sesión en Bitwarden?" + "value" : "¿Guardar contraseña en Bitwarden?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrer la connexion à Bitwarden ?" + "value" : "Enregistrer le mot de passe dans Bitwarden ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Salvare l'accesso a Bitwarden?" + "value" : "Salvare la password su Bitwarden?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Login op Bitwarden opslaan?" + "value" : "Wachtwoord opslaan in Bitwarden?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisać dane logowania w aplikacji Bitwarden?" + "value" : "Zapisać hasło w aplikacji Bitwarden?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar início de sessão no Bitwarden?" + "value" : "Guardar palavra-passe no Bitwarden?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранить логин в Bitwarden?" + "value" : "Сохранить пароль в Bitwarden?" } } } @@ -35741,6 +36696,66 @@ } } }, + "password.management.title" : { + "comment" : "Used as the title for menu item and related Settings page", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter und Autovervollständigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Passwords & Autofill" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contraseñas y autocompletar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mots de passe et saisie automatique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password e compilazione automatica" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden en automatisch invullen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasła i autouzupełnianie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Palavras-passe e preenchimento automático" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли и автозаполнение" + } + } + } + }, "password.manager" : { "comment" : "Section header", "extractionState" : "extracted_with_value", @@ -37595,61 +38610,61 @@ } }, "pinning.hide-autofill-shortcut" : { - "comment" : "Menu item for hiding the autofill shortcut", + "comment" : "Menu item for hiding the passwords shortcut", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Autovervollständigungs-Verknüpfung ausblenden" + "value" : "Passwort-Shortcut ausblenden" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Hide Autofill Shortcut" + "value" : "Hide Passwords Shortcut" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ocultar acceso directo a Autocompletar" + "value" : "Ocultar acceso directo a contraseñas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Masquer le raccourci de saisie automatique" + "value" : "Masquer le raccourci des mots de passe" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nascondi scorciatoia compilazione automatica" + "value" : "Nascondi scorciatoia per le password" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelkoppeling voor automatisch invullen verbergen" + "value" : "Snelkoppeling voor wachtwoorden verbergen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ukryj skrót do autouzupełniania" + "value" : "Ukryj skrót do haseł" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Ocultar atalho de preenchimento automático" + "value" : "Ocultar atalho de palavras-passe" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Скрыть ярлык для автозаполнения" + "value" : "Скрыть ярлык для паролей" } } } @@ -37835,61 +38850,61 @@ } }, "pinning.show-autofill-shortcut" : { - "comment" : "Menu item for showing the autofill shortcut", + "comment" : "Menu item for showing the passwords shortcut", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Autofill-Verknüpfung anzeigen" + "value" : "Passwort-Shortcut anzeigen" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Show Autofill Shortcut" + "value" : "Show Passwords Shortcut" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Mostrar acceso directo a Autocompletar" + "value" : "Mostrar acceso directo a contraseñas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Afficher le raccourci de saisie automatique" + "value" : "Afficher le raccourci des mots de passe" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Mostra scorciatoia compilazione automatica" + "value" : "Mostra scorciatoia per le password" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelkoppeling voor automatisch invullen weergeven" + "value" : "Snelkoppeling voor wachtwoorden weergeven" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pokaż skrót do autouzupełniania" + "value" : "Pokaż skrót do haseł" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Mostrar atalho de preenchimento automático" + "value" : "Mostrar atalho de palavras-passe" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Показывать ярлык для автозаполнения" + "value" : "Показывать ярлык для паролей" } } } @@ -39567,6 +40582,66 @@ } } }, + "pm.empty.default.button.title" : { + "comment" : "Import passwords button title for default empty state", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwörter importieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Import Passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer les mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoorden importeren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importuj hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импорт паролей" + } + } + } + }, "pm.empty.default.description" : { "comment" : "Label for default empty state description", "extractionState" : "extracted_with_value", @@ -39754,55 +40829,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Passwörter" + "value" : "Noch keine Passwörter gespeichert" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "No passwords" + "value" : "No passwords saved yet" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin contraseñas" + "value" : "Aún no hay contraseñas guardadas" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucun mot de passe" + "value" : "Aucun mot de passe n'a été enregistré" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna password" + "value" : "Nessuna password ancora salvata" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen wachtwoorden" + "value" : "Nog geen wachtwoorden opgeslagen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak haseł" + "value" : "Nie zapisano jeszcze żadnych haseł" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Sem palavras-passe" + "value" : "Ainda não há palavras-passe guardadas" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет паролей" + "value" : "Сохраненных паролей пока нет" } } } @@ -40342,187 +41417,187 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Разблокировать доступ к автозаполняемым данным" - } - } - } - }, - "pm.lock-screen.prompt.change-settings" : { - "comment" : "Label presented when changing Auto-Lock settings", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einstellungen für das automatische Ausfüllen von Informationen ändern" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "change your autofill info access settings" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "cambiar la configuración de acceso a la información de autocompletar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "modifier les paramètres d'accès à vos informations de saisie automatique" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "modifica le impostazioni di accesso alla compilazione automatica delle informazioni" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "toegangsinstellingen voor automatisch ingevulde gegevens wijzigen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "zmień ustawienia dostępu do informacji autouzupełniania" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "alterar as tuas definições de acesso às informações de preenchimento automático" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изменить настройки доступа к автозаполняемым данным" - } - } - } - }, - "pm.lock-screen.prompt.export-logins" : { - "comment" : "Label presented when exporting logins", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deine Benutzernamen und Passwörter exportieren" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "export your usernames and passwords" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "exporta tus nombres de usuario y contraseñas" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "exporter vos noms d'utilisateur et mots de passe" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "esporta i tuoi nomi utente e le tue password" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "je gebruikersnamen en wachtwoorden exporteren" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "eksportuj swoje nazwy użytkownika i hasła" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "exportar os nomes de utilizador e as palavras-passe" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Экспортировать имена пользователей и пароли" - } - } - } - }, - "pm.lock-screen.prompt.unlock-logins" : { - "comment" : "Label presented when unlocking Autofill", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zugang zu deinen Autovervollständigungs-Infos freischalten" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "unlock access to your autofill info" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "desbloquear el acceso a tu información de autocompletar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "déverrouiller l'accès à vos informations de saisie automatique" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "sblocca l'accesso alla compilazione automatica delle informazioni" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "toegang tot automatisch ingevulde gegevens ontgrendelen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "odblokuj dostęp do informacji autouzupełniania" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "desbloquear o acesso às tuas informações de preenchimento automático" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "разблокировать доступ к автозаполняемым данным" + "value" : "Разблокировать доступ к автозаполняемым данным" + } + } + } + }, + "pm.lock-screen.prompt.change-settings" : { + "comment" : "Label presented when changing Auto-Lock settings", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen für das automatische Ausfüllen von Informationen ändern" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "change your autofill info access settings" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cambiar la configuración de acceso a la información de autocompletar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "modifier les paramètres d'accès à vos informations de saisie automatique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "modifica le impostazioni di accesso alla compilazione automatica delle informazioni" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "toegangsinstellingen voor automatisch ingevulde gegevens wijzigen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "zmień ustawienia dostępu do informacji autouzupełniania" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "alterar as tuas definições de acesso às informações de preenchimento automático" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить настройки доступа к автозаполняемым данным" + } + } + } + }, + "pm.lock-screen.prompt.export-logins" : { + "comment" : "Label presented when exporting logins", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Benutzernamen und Passwörter exportieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "export your usernames and passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "exporta tus nombres de usuario y contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "exporter vos noms d'utilisateur et mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "esporta i tuoi nomi utente e le tue password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "je gebruikersnamen en wachtwoorden exporteren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "eksportuj swoje nazwy użytkownika i hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "exportar os nomes de utilizador e as palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Экспортировать имена пользователей и пароли" + } + } + } + }, + "pm.lock-screen.prompt.unlock-logins" : { + "comment" : "Label presented when unlocking Autofill", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "deine Passwörter freischalten und Informationen autovervollständigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "unlock your passwords and autofill info for you" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "desbloquea tus contraseñas y completa automáticamente tu información" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "débloque vos mots de passe et saisit automatiquement les informations pour vous" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "sblocca le tue password e compila automaticamente le informazioni" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ontgrendel je wachtwoorden en vul gegevens automatisch in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "odblokuj hasła i automatycznie uzupełniaj informacje" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "desbloquear as palavras-passe e preencher automaticamente informações por ti" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разблокирует ваши пароли и автоматически заполнит информацию" } } } @@ -42334,55 +43409,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Passwort speichern?" + "value" : "Passwort in DuckDuckGo speichern?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Save password?" + "value" : "Save password in DuckDuckGo?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "¿Guardar contraseña?" + "value" : "¿Guardar contraseña en DuckDuckGo?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrer le mot de passe ?" + "value" : "Enregistrer le mot de passe dans DuckDuckGo ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Salvare password?" + "value" : "Salvare la password in DuckDuckGo?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Wachtwoord opslaan?" + "value" : "Wachtwoord opslaan in DuckDuckGo?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisać hasło?" + "value" : "Zapisać hasło w DuckDuckGo?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Guardar palavra-passe?" + "value" : "Guardar palavra-passe no DuckDuckGo?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохранить пароль?" + "value" : "Сохранить пароль в DuckDuckGo?" } } } @@ -42400,7 +43475,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "New Password Saved" + "value" : "New password saved" } }, "es" : { @@ -42927,6 +44002,66 @@ } } }, + "pm.update-credentials.title" : { + "comment" : "Title for the Update Credentials popover", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort aktualisieren?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update password?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Actualizar contraseña?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifier le mot de passe ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornare password?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord bijwerken?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizować hasło?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar palavra-passe?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить пароль?" + } + } + } + }, "pm.username" : { "comment" : "Label for username edit field", "extractionState" : "extracted_with_value", @@ -48531,6 +49666,60 @@ } }, "Select data to import:" : { + "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.", + "extractionState" : "stale", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu importierende Daten auswählen:" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecciona los datos a importar:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sélectionner les données à importer :" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona i dati da importare:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecteer gegevens om te importeren:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wybierz dane do zaimportowania:" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar dados para importar:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выберите данные для импорта:" + } + } + } + }, + "Select Data to Import:" : { "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.", "localizations" : { "de" : { diff --git a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift index b0533f8712c..02c119a7037 100644 --- a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift +++ b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift @@ -77,6 +77,8 @@ final class PopoverMessageViewController: NSHostingController AnyCancellable? { @@ -987,8 +987,8 @@ final class PasswordManagementViewController: NSViewController { private func showEmptyState(category: SecureVaultSorting.Category) { switch category { - case .allItems: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateDefaultTitle, message: UserText.pmEmptyStateDefaultDescription, hideMessage: false, hideButton: false) - case .logins: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateLoginsTitle, hideMessage: false, hideButton: false) + case .allItems: showEmptyState(image: .passwordsAdd128, title: UserText.pmEmptyStateDefaultTitle, message: UserText.pmEmptyStateDefaultDescription, hideMessage: false, hideButton: false) + case .logins: showEmptyState(image: .passwordsAdd128, title: UserText.pmEmptyStateLoginsTitle, hideMessage: false, hideButton: false) case .identities: showEmptyState(image: .identitiesEmpty, title: UserText.pmEmptyStateIdentitiesTitle) case .cards: showEmptyState(image: .creditCardsEmpty, title: UserText.pmEmptyStateCardsTitle) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index 2fd6c582594..e8f73afdc7c 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -1,7 +1,7 @@ - + - + @@ -429,14 +429,35 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -742,18 +763,19 @@ DQ - + + - + @@ -782,11 +804,10 @@ DQ + - - @@ -794,6 +815,7 @@ DQ + @@ -1073,6 +1095,7 @@ DQ + diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index 0d7bc4bc6dc..cddee059280 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -39,6 +39,7 @@ final class SaveCredentialsViewController: NSViewController { return controller } + @IBOutlet var ddgPasswordManagerTitle: NSView! @IBOutlet var titleLabel: NSTextField! @IBOutlet var passwordManagerTitle: NSView! @IBOutlet var passwordManagerAccountLabel: NSTextField! @@ -81,6 +82,8 @@ final class SaveCredentialsViewController: NSViewController { private var saveButtonAction: (() -> Void)? + private var shouldFirePinPromptNotification = false + var passwordData: Data { let string = hiddenPasswordField.isHidden ? visiblePasswordField.stringValue : hiddenPasswordField.stringValue return string.data(using: .utf8)! @@ -104,6 +107,9 @@ final class SaveCredentialsViewController: NSViewController { override func viewWillDisappear() { passwordManagerStateCancellable = nil + if shouldFirePinPromptNotification { + NotificationCenter.default.post(name: .passwordsPinningPrompt, object: nil) + } } private func setUpStrings() { @@ -166,11 +172,11 @@ final class SaveCredentialsViewController: NSViewController { editButton.isHidden = true doneButton.isHidden = true - titleLabel.isHidden = passwordManagerCoordinator.isEnabled + ddgPasswordManagerTitle.isHidden = passwordManagerCoordinator.isEnabled passwordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || passwordManagerCoordinator.isLocked passwordManagerAccountLabel.stringValue = UserText.passwordManagementSaveCredentialsAccountLabel(activeVault: passwordManagerCoordinator.activeVaultEmail ?? "") unlockPasswordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || !passwordManagerCoordinator.isLocked - titleLabel.stringValue = UserText.pmSaveCredentialsEditableTitle + titleLabel.stringValue = credentials?.account.id == nil ? UserText.pmSaveCredentialsEditableTitle : UserText.pmUpdateCredentialsTitle usernameField.makeMeFirstResponder() } else { notNowSegmentedControl.isHidden = true @@ -232,9 +238,14 @@ final class SaveCredentialsViewController: NSViewController { } } } else { - _ = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeWebsiteCredentials(credentials) + let vault = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) + _ = try vault.storeWebsiteCredentials(credentials) NSApp.delegateTyped.syncService?.scheduler.notifyDataChanged() os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + + if existingCredentials?.account.id == nil, !LocalPinningManager.shared.isPinned(.autofill), let count = try? vault.accountsCount(), count == 1 { + shouldFirePinPromptNotification = true + } } } catch { os_log("%s:%s: failed to store credentials %s", type: .error, className, #function, error.localizedDescription) diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift index d08f3e7f48a..cd6cf0184ec 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift @@ -24,7 +24,7 @@ public final class PopoverMessageViewModel: ObservableObject { @Published var message: String @Published var image: NSImage? @Published var buttonText: String? - @Published var buttonAction: (() -> Void)? + @Published public var buttonAction: (() -> Void)? public init(message: String, image: NSImage? = nil, diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift index 81f5fca4a75..31ac01269be 100644 --- a/UnitTests/DataImport/DataImportViewModelTests.swift +++ b/UnitTests/DataImport/DataImportViewModelTests.swift @@ -1142,7 +1142,7 @@ import XCTest xctDescr = "\(source): " + xctDescr XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.bookmarks])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1181,7 +1181,7 @@ import XCTest // expect Final Summary let expectation = DataImportViewModel(importSource: source, screen: .summary([.bookmarks], isFileImport: true), summary: [bookmarksSummary, result.map { .init(.bookmarks, $0) }].compactMap { $0 }) XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.bookmarks])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1231,7 +1231,7 @@ import XCTest } } - func testWhenBrowsersBookmarksImportFailsNoDataAndFileImportSkippedAndNoPasswordsFileImportNeeded_dialogDismissed() throws { + func testWhenBrowsersBookmarksImportFailsNoDataAndFileImportSkippedAndNoPasswordsFileImportNeeded_shortcutsShown() throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { let bookmarksSummary = bookmarkSummaryNoData @@ -1246,12 +1246,9 @@ import XCTest screen: .fileImport(dataType: .bookmarks, summary: []), summary: [bookmarksSummary, passwordsSummary].compactMap { $0 }) - let expectation = expectation(description: "dismissed") - model.performAction(for: .skip) { - expectation.fulfill() - } + model.performAction(for: .skip) {} - waitForExpectations(timeout: 0) + XCTAssertEqual(model.screen, .shortcuts([.passwords])) } } } @@ -1410,7 +1407,7 @@ import XCTest // expect Final Summary let expectation = DataImportViewModel(importSource: source, screen: .summary([.passwords], isFileImport: true), summary: [bookmarksSummary, passwordsSummary, bookmarksFileImportSummary, result.map { .init(.passwords, $0) }].compactMap { $0 }) XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.passwords])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1447,7 +1444,7 @@ import XCTest // expect Final Summary let expectation = DataImportViewModel(importSource: source, screen: .summary([.passwords], isFileImport: true), summary: [passwordsSummary, result.map { .init(.passwords, $0) }].compactMap { $0 }) XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertEqual(model.actionButton, .next(.shortcuts([.passwords])), xctDescr) XCTAssertNil(model.secondaryButton, xctDescr) } } @@ -1503,7 +1500,7 @@ import XCTest } } - func testWhenBrowsersPasswordsImportFailNoDataAndFileImportSkipped_dialogDismissed() throws { + func testWhenBrowsersPasswordsImportFailNoDataAndFileImportSkipped_dialogDismissedOrShortcutsShown() throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { for bookmarksSummary in bookmarksSummaries { @@ -1522,12 +1519,19 @@ import XCTest screen: .fileImport(dataType: .passwords, summary: []), summary: [bookmarksSummary, passwordsSummary, bookmarksFileImportSummary].compactMap { $0 }) - let expectation = expectation(description: "dismissed") - model.performAction(for: .skip) { - expectation.fulfill() + if let result = bookmarksSummary?.result as? DataImportResult, result.isSuccess, let successful = try? result.get().successful, successful > 0 { + model.performAction(for: .skip) {} + XCTAssertEqual(model.screen, .shortcuts([.bookmarks])) + } else if let result = bookmarksFileImportSummary?.result as? DataImportResult, result.isSuccess, let successful = try? result.get().successful, successful > 0 { + model.performAction(for: .skip) {} + XCTAssertEqual(model.screen, .shortcuts([.bookmarks])) + } else { + let expectation = expectation(description: "dismissed") + model.performAction(for: .skip) { + expectation.fulfill() + } + waitForExpectations(timeout: 0) } - - waitForExpectations(timeout: 0) } } } @@ -1828,6 +1832,7 @@ extension DataImportViewModel.Screen: CustomStringConvertible { case .summary(let dataTypes, isFileImport: true): ".summary([\(dataTypes.map { "." + $0.rawValue }.sorted().joined(separator: ", "))], isFileImport: true)" case .feedback: ".feedback" + case .shortcuts: ".shortcuts" } } } diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index f6d7e42781b..0140a43a9f3 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -83,7 +83,7 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(moreOptionsMenu.items[7].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[8].title, UserText.bookmarks) XCTAssertEqual(moreOptionsMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagement) + XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagementTitle) XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) @@ -115,7 +115,7 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(moreOptionsMenu.items[7].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[8].title, UserText.bookmarks) XCTAssertEqual(moreOptionsMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagement) + XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagementTitle) XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) From d53d1745265f1e1e8775cda1e95ec1c80597c807 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 11 Jun 2024 11:25:03 +0100 Subject: [PATCH 30/35] Update Send Feedback icon (#2852) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207497751210442/f --- .../SendFeedback-Color-32x16.svg | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Assets.xcassets/Images/Send-Feedback-Color.imageset/SendFeedback-Color-32x16.svg b/DuckDuckGo/Assets.xcassets/Images/Send-Feedback-Color.imageset/SendFeedback-Color-32x16.svg index 5b838d808fb..daf91439c72 100644 --- a/DuckDuckGo/Assets.xcassets/Images/Send-Feedback-Color.imageset/SendFeedback-Color-32x16.svg +++ b/DuckDuckGo/Assets.xcassets/Images/Send-Feedback-Color.imageset/SendFeedback-Color-32x16.svg @@ -1,9 +1,9 @@ - - - - - - + + + + + + From 2c96c71b830b2716d09fdb14840e52ecdd1556db Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 12 Jun 2024 05:13:27 +0000 Subject: [PATCH 31/35] Bump version to 1.92.0 (202) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 5cfcaedf6d9..7358d3d785e 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 201 +CURRENT_PROJECT_VERSION = 202 From be3a02d592ca81464472af2ba8232f69a0f5b5fa Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 12 Jun 2024 07:16:37 -0700 Subject: [PATCH 32/35] Add VPN reddit cookie workaround (#2851) Task/Issue URL: https://app.asana.com/0/1203137811378537/1207423045670637/f Tech Design URL: CC: Description: This PR adds the reddit cookie VPN workaround. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + DuckDuckGo/Application/AppDelegate.swift | 29 +++++ .../NetworkProtectionTunnelController.swift | 2 +- .../VPNRedditSessionWorkaround.swift | 123 ++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 39e7d25be7a..ad5446c719c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1368,6 +1368,8 @@ 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; + 4BE3A6C12C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */; }; + 4BE3A6C22C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */; }; 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */; }; 4BE4005527CF3F19007D3161 /* SavePaymentMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005427CF3F19007D3161 /* SavePaymentMethodViewController.swift */; }; 4BE41A5E28446EAD00760399 /* BookmarksBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE41A5D28446EAD00760399 /* BookmarksBarViewModel.swift */; }; @@ -3300,6 +3302,7 @@ 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; + 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRedditSessionWorkaround.swift; sourceTree = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; 4BE4005427CF3F19007D3161 /* SavePaymentMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodViewController.swift; sourceTree = ""; }; 4BE41A5D28446EAD00760399 /* BookmarksBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarViewModel.swift; sourceTree = ""; }; @@ -5188,6 +5191,7 @@ EEA3EEAF2B24EB5100E8333A /* VPNLocation */, 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */, B6F1B02D2BCE6B47005E863C /* TunnelControllerProvider.swift */, + 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */, ); path = BothAppTargets; sourceTree = ""; @@ -9895,6 +9899,7 @@ 3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, B677FC552B064A9C0099EB04 /* DataImportViewModel.swift in Sources */, D64A5FF92AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, + 4BE3A6C22C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */, 3707C717294B5D0F00682A9F /* FindInPageTabExtension.swift in Sources */, 3706FB81293F65D500E42796 /* IndexPathExtension.swift in Sources */, 4BF97AD62B43C45800EB4240 /* NetworkProtectionNavBarPopoverManager.swift in Sources */, @@ -11113,6 +11118,7 @@ B6C0B23626E732000031CB7F /* DownloadListItem.swift in Sources */, 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, 4B9DB0232A983B24000927DB /* WaitlistRequest.swift in Sources */, + 4BE3A6C12C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */, B6B1E87E26D5DA0E0062C350 /* DownloadsPopover.swift in Sources */, 85774AFF2A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */, 4B9292A026670D2A00AD2C21 /* SpacerNode.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 18eeeafa44d..e0826881108 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -104,6 +104,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif + private lazy var vpnRedditSessionWorkaround: VPNRedditSessionWorkaround = { + let ipcClient = TunnelControllerIPCClient() + let statusReporter = DefaultNetworkProtectionStatusReporter( + statusObserver: ipcClient.connectionStatusObserver, + serverInfoObserver: ipcClient.serverInfoObserver, + connectionErrorObserver: ipcClient.connectionErrorObserver, + connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.dataVolumeObserver, + knownFailureObserver: KnownFailureObserverThroughDistributedNotifications() + ) + + return VPNRedditSessionWorkaround( + accountManager: accountManager, + ipcClient: ipcClient, + statusReporter: statusReporter + ) + }() + private var didFinishLaunching = false #if SPARKLE @@ -360,6 +379,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) } } + + Task { @MainActor in + await vpnRedditSessionWorkaround.installRedditSessionWorkaround() + } + } + + func applicationDidResignActive(_ notification: Notification) { + Task { @MainActor in + await vpnRedditSessionWorkaround.removeRedditSessionWorkaround() + } } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 62e32dc0a65..60dfc1841e0 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -842,7 +842,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private func fetchAuthToken() throws -> NSString? { if let accessToken = try? accessTokenStorage.getAccessToken() { - os_log(.error, log: .networkProtection, "🟢 TunnelController found token: %{public}d", accessToken) + os_log(.error, log: .networkProtection, "🟢 TunnelController found token") return Self.adaptAccessTokenForVPN(accessToken) as NSString? } os_log(.error, log: .networkProtection, "🔴 TunnelController found no token :(") diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift new file mode 100644 index 00000000000..a3f602f0b69 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift @@ -0,0 +1,123 @@ +// +// VPNRedditSessionWorkaround.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection +import NetworkProtectionIPC +import Subscription +import WebKit +import Common + +final class VPNRedditSessionWorkaround { + + private let accountManager: AccountManaging + private let ipcClient: TunnelControllerIPCClient + private let statusReporter: NetworkProtectionStatusReporter + + init(accountManager: AccountManaging, + ipcClient: TunnelControllerIPCClient, + statusReporter: NetworkProtectionStatusReporter) { + self.accountManager = accountManager + self.ipcClient = ipcClient + self.statusReporter = statusReporter + self.statusReporter.forceRefresh() + } + + @MainActor + func installRedditSessionWorkaround() async { + let configuration = WKWebViewConfiguration() + await installRedditSessionWorkaround(to: configuration.websiteDataStore.httpCookieStore) + } + + @MainActor + func removeRedditSessionWorkaround() async { + let configuration = WKWebViewConfiguration() + await removeRedditSessionWorkaround(from: configuration.websiteDataStore.httpCookieStore) + } + + @MainActor + func installRedditSessionWorkaround(to cookieStore: WKHTTPCookieStore) async { + guard accountManager.isUserAuthenticated, + statusReporter.statusObserver.recentValue.isConnected, + let redditSessionCookie = HTTPCookie.emptyRedditSession else { + return + } + + let cookies = await cookieStore.allCookies() + var requiresRedditSessionCookie = true + for cookie in cookies { + if cookie.domain == redditSessionCookie.domain, + cookie.name == redditSessionCookie.name { + // Avoid adding the cookie if one already exists + requiresRedditSessionCookie = false + break + } + } + + if requiresRedditSessionCookie { + os_log(.error, log: .networkProtection, "Installing VPN cookie workaround...") + await cookieStore.setCookie(redditSessionCookie) + os_log(.error, log: .networkProtection, "Installed VPN cookie workaround") + } + } + + func removeRedditSessionWorkaround(from cookieStore: WKHTTPCookieStore) async { + guard let redditSessionCookie = HTTPCookie.emptyRedditSession else { + return + } + + let cookies = await cookieStore.allCookies() + for cookie in cookies { + if cookie.domain == redditSessionCookie.domain, cookie.name == redditSessionCookie.name { + if cookie.value == redditSessionCookie.value { + os_log(.error, log: .networkProtection, "Removing VPN cookie workaround") + await cookieStore.deleteCookie(cookie) + os_log(.error, log: .networkProtection, "Removed VPN cookie workaround") + } + + break + } + } + } + +} + +private extension HTTPCookie { + + static var emptyRedditSession: HTTPCookie? { + return HTTPCookie(properties: [ + .domain: ".reddit.com", + .path: "/", + .name: "reddit_session", + .value: "", + .secure: "TRUE" + ]) + } + +} + +private extension ConnectionStatus { + + var isConnected: Bool { + switch self { + case .connected: return true + default: return false + } + } + +} From 39d9c419b14484374a1c8b5007441a07e32c8d37 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 12 Jun 2024 16:44:47 +0200 Subject: [PATCH 33/35] Implement VPN control through UDS (#2767) Task/Issue URL: https://app.asana.com/0/1203108348835387/1207203883170230/f ## Description Implement VPN control through UDS --- Configuration/AppStore.xcconfig | 18 ++ Configuration/DeveloperID.xcconfig | 18 ++ DuckDuckGo.xcodeproj/project.pbxproj | 37 +++ .../DuckDuckGo Privacy Browser.xcscheme | 24 ++ DuckDuckGo/Application/AppDelegate.swift | 14 +- .../Common/Extensions/BundleExtension.swift | 17 ++ DuckDuckGo/DuckDuckGo.entitlements | 1 + DuckDuckGo/DuckDuckGoAppStore.entitlements | 1 + DuckDuckGo/DuckDuckGoAppStoreCI.entitlements | 1 + DuckDuckGo/DuckDuckGoDebug.entitlements | 1 + DuckDuckGo/Info.plist | 2 + DuckDuckGo/LoginItems/LoginItemsManager.swift | 2 + .../MainWindow/MainViewController.swift | 23 +- .../View/NavigationBarPopovers.swift | 1 - .../VPNIPCResources.swift | 23 ++ .../VPNOperationErrorRecorder.swift | 4 +- ...rkProtection+ConvenienceInitializers.swift | 7 - .../NetworkProtectionDebugUtilities.swift | 6 +- ...etworkProtectionNavBarPopoverManager.swift | 4 +- .../TunnelControllerProvider.swift | 3 +- ...lerUDSClient+ConvenienceInitializers.swift | 33 +++ ...lerXPCClient+ConvenienceInitializers.swift | 29 ++ ...NetworkProtectionIPCTunnelController.swift | 2 + ...rkProtectionUNNotificationsPresenter.swift | 7 +- .../Model/PreferencesSidebarModel.swift | 4 +- .../VPNMetadataCollector.swift | 6 +- DuckDuckGo/Waitlist/IPCServiceLauncher.swift | 94 ++++++ DuckDuckGo/Waitlist/VPNUninstaller.swift | 61 ++-- DuckDuckGoVPN/DuckDuckGoVPN.entitlements | 2 +- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 7 +- .../DuckDuckGoVPNAppStore.entitlements | 1 + DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements | 1 + DuckDuckGoVPN/Info-AppStore.plist | 2 + DuckDuckGoVPN/Info.plist | 4 + .../TunnelControllerIPCService.swift | 71 ++++- .../Sources/AppLauncher/AppLauncher.swift | 12 +- .../NetworkProtectionMac/Package.swift | 2 + .../VPNControllerIPCClient.swift | 26 ++ .../VPNControllerUDSClient.swift | 38 +++ ...ent.swift => VPNControllerXPCClient.swift} | 54 +++- ...ver.swift => VPNControllerXPCServer.swift} | 23 +- .../VPNIPCClientCommand.swift | 29 ++ .../TunnelControllerView.swift | 2 +- LocalPackages/UDSHelper/.gitignore | 8 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + LocalPackages/UDSHelper/Package.swift | 26 ++ .../Sources/UDSHelper/UDSClient.swift | 236 +++++++++++++++ .../Sources/UDSHelper/UDSMessage.swift | 38 +++ .../Sources/UDSHelper/UDSReceiver.swift | 188 ++++++++++++ .../Sources/UDSHelper/UDSServer.swift | 276 ++++++++++++++++++ .../UDSHelperTests/UDSMessageTests.swift | 46 +++ 51 files changed, 1454 insertions(+), 89 deletions(-) create mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNIPCResources.swift create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift create mode 100644 DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerXPCClient+ConvenienceInitializers.swift create mode 100644 DuckDuckGo/Waitlist/IPCServiceLauncher.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerIPCClient.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerUDSClient.swift rename LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/{TunnelControllerIPCClient.swift => VPNControllerXPCClient.swift} (84%) rename LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/{TunnelControllerIPCServer.swift => VPNControllerXPCServer.swift} (93%) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNIPCClientCommand.swift create mode 100644 LocalPackages/UDSHelper/.gitignore create mode 100644 LocalPackages/UDSHelper/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 LocalPackages/UDSHelper/Package.swift create mode 100644 LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift create mode 100644 LocalPackages/UDSHelper/Sources/UDSHelper/UDSMessage.swift create mode 100644 LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift create mode 100644 LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift create mode 100644 LocalPackages/UDSHelper/Tests/UDSHelperTests/UDSMessageTests.swift diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index 30ec838615c..4bf15b00f36 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -95,3 +95,21 @@ DBP_APP_GROUP[config=CI][sdk=*] = $(DBP_BASE_APP_GROUP).debug DBP_APP_GROUP[config=Review][sdk=*] = $(DBP_BASE_APP_GROUP).review DBP_APP_GROUP[config=Debug][sdk=*] = $(DBP_BASE_APP_GROUP).debug DBP_APP_GROUP[config=Release][sdk=*] = $(DBP_BASE_APP_GROUP) + +// IPC + +// IMPORTANT: The reason this app group was created is because IPC through +// Unix Domain Sockets requires the socket file path to be no longer than +// 108 characters. Sandboxing requirements force us to place said socket +// within an app group container. +// +// Name coding: +// - ipc.a = ipc app store release +// - ipc.a.d = ipc app store debug +// - ipc.a.r = ipc app store review +// +IPC_APP_GROUP_BASE = $(DEVELOPMENT_TEAM).com.ddg.ipc.a +IPC_APP_GROUP[config=CI][sdk=*] = $(IPC_APP_GROUP_BASE).d +IPC_APP_GROUP[config=Review][sdk=*] = $(IPC_APP_GROUP_BASE).r +IPC_APP_GROUP[config=Debug][sdk=*] = $(IPC_APP_GROUP_BASE).d +IPC_APP_GROUP[config=Release][sdk=*] = $(IPC_APP_GROUP_BASE) diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 0dc8de04850..ddbe7edc51d 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -100,3 +100,21 @@ DBP_APP_GROUP[config=CI][sdk=*] = $(DBP_BASE_APP_GROUP).debug DBP_APP_GROUP[config=Review][sdk=*] = $(DBP_BASE_APP_GROUP).review DBP_APP_GROUP[config=Debug][sdk=*] = $(DBP_BASE_APP_GROUP).debug DBP_APP_GROUP[config=Release][sdk=*] = $(DBP_BASE_APP_GROUP) + +// IPC + +// IMPORTANT: The reason this app group was created is because IPC through +// Unix Domain Sockets requires the socket file path to be no longer than +// 108 characters. Sandboxing requirements force us to place said socket +// within an app group container. +// +// Name coding: +// - ipc.d = ipc developer id release +// - ipc.d.d = ipc developer id debug +// - ipc.d.r = ipc developer id review +// +IPC_APP_GROUP_BASE = $(DEVELOPMENT_TEAM).com.ddg.ipc +IPC_APP_GROUP[config=CI][sdk=*] = $(IPC_APP_GROUP_BASE).d +IPC_APP_GROUP[config=Review][sdk=*] = $(IPC_APP_GROUP_BASE).r +IPC_APP_GROUP[config=Debug][sdk=*] = $(IPC_APP_GROUP_BASE).d +IPC_APP_GROUP[config=Release][sdk=*] = $(IPC_APP_GROUP_BASE) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ad5446c719c..ba2c8460991 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1520,10 +1520,13 @@ 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 */; }; + 7B6545ED2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6545EC2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift */; }; + 7B6545EE2C0779D500115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6545EC2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift */; }; 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; 7B7FCD0F2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */; }; 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */; }; + 7B8594192B5B26230007EB3E /* UDSHelper in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8594182B5B26230007EB3E /* UDSHelper */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; @@ -1568,8 +1571,14 @@ 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 */; }; + 7BCB90C22C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */; }; + 7BCB90C32C1863BA008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.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 */; }; + 7BD7B0012C19D3830039D20A /* VPNIPCResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */; }; + 7BD7B0022C19D3830039D20A /* VPNIPCResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */; }; + 7BD7B0032C19D3830039D20A /* VPNIPCResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */; }; + 7BD7B0042C19D3830039D20A /* VPNIPCResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */; }; 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; @@ -1582,6 +1591,8 @@ 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 */; }; + 7BFF35732C10D75000F89673 /* IPCServiceLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFF35722C10D75000F89673 /* IPCServiceLauncher.swift */; }; + 7BFF35742C10D75000F89673 /* IPCServiceLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFF35722C10D75000F89673 /* IPCServiceLauncher.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 */; }; @@ -3384,10 +3395,12 @@ 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 = ""; }; + 7B6545EC2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNControllerUDSClient+ConvenienceInitializers.swift"; sourceTree = ""; }; 7B6EC5E42AE2D8AF004FE6DF /* DuckDuckGoDBPAgentAppStore.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = DuckDuckGoDBPAgentAppStore.xcconfig; sourceTree = ""; }; 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = DuckDuckGoDBPAgent.xcconfig; sourceTree = ""; }; 7B76E6852AD5D77600186A84 /* XPCHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = XPCHelper; sourceTree = ""; }; 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+vpnLegacyUser.swift"; sourceTree = ""; }; + 7B8594172B5B25FB0007EB3E /* UDSHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = UDSHelper; sourceTree = ""; }; 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAppEventsHandler.swift; sourceTree = ""; }; 7B9167A82C09E88800322310 /* AppLauncher */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AppLauncher; sourceTree = ""; }; 7B934C3D2A866CFF00FC8F9C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; @@ -3409,8 +3422,10 @@ 7BB108572A43375D000AB95F /* PFMoveApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMoveApplication.h; sourceTree = ""; }; 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; + 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNControllerXPCClient+ConvenienceInitializers.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 = ""; }; + 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIPCResources.swift; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -3421,6 +3436,7 @@ 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 = ""; }; + 7BFF35722C10D75000F89673 /* IPCServiceLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPCServiceLauncher.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 = ""; }; 8511E18325F82B34002F516B /* 01_Fire_really_small.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 01_Fire_really_small.json; sourceTree = ""; }; @@ -4247,6 +4263,7 @@ 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, + 7B8594192B5B26230007EB3E /* UDSHelper in Frameworks */, BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */, 7B23668C2C09FAF1002D393F /* VPNAppLauncher in Frameworks */, EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */, @@ -4835,6 +4852,7 @@ 37BA812B29B3CB8A0053F1A3 /* SyncUI */, 1E862A882A9FC01200F84D4B /* SubscriptionUI */, 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */, + 7B8594172B5B25FB0007EB3E /* UDSHelper */, 7B76E6852AD5D77600186A84 /* XPCHelper */, ); path = LocalPackages; @@ -5178,6 +5196,8 @@ 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, B602E81C2A1E25B0006D261F /* NEOnDemandRuleExtension.swift */, + 7B6545EC2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift */, + 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */, 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */, 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */, 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */, @@ -5494,6 +5514,7 @@ children = ( 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */, 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */, + 7BFF35722C10D75000F89673 /* IPCServiceLauncher.swift */, 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */, 31F2D1FE2AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift */, 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */, @@ -8256,6 +8277,7 @@ 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */, + 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */, ); path = AppAndExtensionAndAgentTargets; sourceTree = ""; @@ -8494,6 +8516,7 @@ 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */, 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, + 7B8594182B5B26230007EB3E /* UDSHelper */, 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, EE2F9C5A2B90F2FF00D45FC9 /* Subscription */, F198C7192BD18A5B000BF24D /* PixelKit */, @@ -9567,6 +9590,7 @@ 560C40002BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */, C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */, 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */, + 7BCB90C32C1863BA008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, 3706FA84293F65D500E42796 /* ClickToLoadModel.swift in Sources */, @@ -9580,6 +9604,7 @@ 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 3706FA8D293F65D500E42796 /* BookmarkTableCellView.swift in Sources */, 3706FA8E293F65D500E42796 /* BookmarkManagementSidebarViewController.swift in Sources */, + 7BD7B0022C19D3830039D20A /* VPNIPCResources.swift in Sources */, 3706FA8F293F65D500E42796 /* NSStackViewExtension.swift in Sources */, 3706FA90293F65D500E42796 /* OptionalExtension.swift in Sources */, 3706FA91293F65D500E42796 /* PasswordManagementLoginItemView.swift in Sources */, @@ -9603,6 +9628,7 @@ 3706FAA2293F65D500E42796 /* MainWindow.swift in Sources */, 3707C727294B5D2900682A9F /* WKWebView+SessionState.swift in Sources */, 4B44FEF42B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */, + 7BFF35742C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, @@ -10256,6 +10282,7 @@ C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, 3706FC67293F65D500E42796 /* OperatingSystemVersionExtension.swift in Sources */, + 7B6545EE2C0779D500115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift in Sources */, B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */, 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, B677FC502B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, @@ -10749,6 +10776,7 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, + 7BD7B0032C19D3830039D20A /* VPNIPCResources.swift in Sources */, F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, @@ -10790,6 +10818,7 @@ 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, + 7BD7B0042C19D3830039D20A /* VPNIPCResources.swift in Sources */, F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, @@ -11030,6 +11059,7 @@ 85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */, 31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */, 4B41EDAE2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */, + 7BFF35732C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, AA5FA697275F90C400DCE9C9 /* FaviconImageCache.swift in Sources */, 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */, B62B483E2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, @@ -11060,6 +11090,7 @@ AABEE6A524AA0A7F0043105B /* SuggestionViewController.swift in Sources */, 1D6216B229069BBF00386B2C /* BWKeyStorage.swift in Sources */, AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */, + 7BD7B0012C19D3830039D20A /* VPNIPCResources.swift in Sources */, B6676BE12AA986A700525A21 /* AddressBarTextEditor.swift in Sources */, B69B503B2726A12500758A2B /* Atb.swift in Sources */, 37A6A8F12AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */, @@ -11140,6 +11171,7 @@ B6BF5D8929470BC4006742B1 /* HTTPSUpgradeTabExtension.swift in Sources */, 1D36E65B298ACD2900AA485D /* AppIconChanger.swift in Sources */, 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */, + 7BCB90C22C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, 4B9DB0202A983B24000927DB /* ProductWaitlistRequest.swift in Sources */, 98779A0029999B64005D8EB6 /* Bookmark.xcdatamodeld in Sources */, 85589E9E27BFE4500038AD11 /* DefaultBrowserPromptView.swift in Sources */, @@ -11547,6 +11579,7 @@ 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, + 7B6545ED2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, AA5C1DD3285A217F0089850C /* RecentlyClosedCacheItem.swift in Sources */, B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */, @@ -13375,6 +13408,10 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Networking; }; + 7B8594182B5B26230007EB3E /* UDSHelper */ = { + isa = XCSwiftPackageProductDependency; + productName = UDSHelper; + }; 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index 3afb1cb16da..3adcec9f87e 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -62,6 +62,20 @@ ReferencedContainer = "container:LocalPackages/DataBrokerProtection"> + + + + + + + + com.apple.security.application-groups + $(IPC_APP_GROUP) $(DBP_APP_GROUP) $(NETP_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index 7b07bc92d81..5f57d80c3bc 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -11,6 +11,7 @@ com.apple.security.application-groups + $(IPC_APP_GROUP) $(DBP_APP_GROUP) $(NETP_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements index 8779a3f3061..8cfb90076d7 100644 --- a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements @@ -6,6 +6,7 @@ com.apple.security.application-groups + $(IPC_APP_GROUP) $(NETP_APP_GROUP) $(DBP_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGo/DuckDuckGoDebug.entitlements b/DuckDuckGo/DuckDuckGoDebug.entitlements index e5efc9872c9..2e99139e1a3 100644 --- a/DuckDuckGo/DuckDuckGoDebug.entitlements +++ b/DuckDuckGo/DuckDuckGoDebug.entitlements @@ -13,6 +13,7 @@ com.apple.security.application-groups + $(IPC_APP_GROUP) $(DBP_APP_GROUP) $(NETP_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 095900fb993..f7a87ccdded 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -550,5 +550,7 @@ 10800 ViewBridgeService + IPC_APP_GROUP + $(IPC_APP_GROUP) diff --git a/DuckDuckGo/LoginItems/LoginItemsManager.swift b/DuckDuckGo/LoginItems/LoginItemsManager.swift index 4dd2f98fb56..12601776915 100644 --- a/DuckDuckGo/LoginItems/LoginItemsManager.swift +++ b/DuckDuckGo/LoginItems/LoginItemsManager.swift @@ -26,6 +26,8 @@ protocol LoginItemsManaging { func throwingEnableLoginItems(_ items: Set, log: OSLog) throws func disableLoginItems(_ items: Set) func restartLoginItems(_ items: Set, log: OSLog) + + func isAnyEnabled(_ items: Set) -> Bool } /// Class to manage the login items for the VPN and DBP diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 4624b91d14c..ded43e5a6f8 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -55,7 +55,11 @@ final class MainViewController: NSViewController { fatalError("MainViewController: Bad initializer") } - init(tabCollectionViewModel: TabCollectionViewModel? = nil, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, autofillPopoverPresenter: AutofillPopoverPresenter) { + init(tabCollectionViewModel: TabCollectionViewModel? = nil, + bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, + autofillPopoverPresenter: AutofillPopoverPresenter, + vpnXPCClient: VPNControllerXPCClient = .shared) { + let tabCollectionViewModel = tabCollectionViewModel ?? TabCollectionViewModel() self.tabCollectionViewModel = tabCollectionViewModel self.isBurner = tabCollectionViewModel.isBurner @@ -70,14 +74,14 @@ final class MainViewController: NSViewController { } #endif - let ipcClient = TunnelControllerIPCClient() - ipcClient.register { error in + vpnXPCClient.register { error in NetworkProtectionKnownFailureStore().lastKnownFailure = KnownFailure(error) } - let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) + + let vpnUninstaller = VPNUninstaller(ipcClient: vpnXPCClient) return NetworkProtectionNavBarPopoverManager( - ipcClient: ipcClient, + ipcClient: vpnXPCClient, vpnUninstaller: vpnUninstaller) }() let networkProtectionStatusReporter: NetworkProtectionStatusReporter = { @@ -92,14 +96,13 @@ final class MainViewController: NSViewController { connectivityIssuesObserver = connectivityIssuesObserver ?? DisabledConnectivityIssueObserver() controllerErrorMessageObserver = controllerErrorMessageObserver ?? ControllerErrorMesssageObserverThroughDistributedNotifications() - let ipcClient = networkProtectionPopoverManager.ipcClient return DefaultNetworkProtectionStatusReporter( - statusObserver: ipcClient.ipcStatusObserver, - serverInfoObserver: ipcClient.ipcServerInfoObserver, - connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, + statusObserver: vpnXPCClient.ipcStatusObserver, + serverInfoObserver: vpnXPCClient.ipcServerInfoObserver, + connectionErrorObserver: vpnXPCClient.ipcConnectionErrorObserver, connectivityIssuesObserver: connectivityIssuesObserver, controllerErrorMessageObserver: controllerErrorMessageObserver, - dataVolumeObserver: ipcClient.ipcDataVolumeObserver, + dataVolumeObserver: vpnXPCClient.ipcDataVolumeObserver, knownFailureObserver: KnownFailureObserverThroughDistributedNotifications() ) }() diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift index 133c11b4b6e..29ef44add54 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift @@ -29,7 +29,6 @@ protocol PopoverPresenter { } protocol NetPPopoverManager: AnyObject { - var ipcClient: NetworkProtectionIPCClient { get } var isShown: Bool { get } func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNIPCResources.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNIPCResources.swift new file mode 100644 index 00000000000..396cc45a801 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNIPCResources.swift @@ -0,0 +1,23 @@ +// +// VPNIPCResources.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 VPNIPCResources { + public static let socketFileURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.appGroup(bundle: .ipc))!.appendingPathComponent("vpn.ipc") +} diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift index 7fa58ab46d3..f96ff470b99 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/VPNOperationErrorRecorder.swift @@ -38,10 +38,10 @@ final class ErrorInformation: NSObject, Codable { /// final class VPNOperationErrorHistory { - private let ipcClient: TunnelControllerIPCClient + private let ipcClient: VPNControllerXPCClient private let defaults: UserDefaults - init(ipcClient: TunnelControllerIPCClient, + init(ipcClient: VPNControllerXPCClient, defaults: UserDefaults = .netP) { self.ipcClient = ipcClient diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index f48a2c97fb5..2d57ecb38d4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -80,10 +80,3 @@ extension NetworkProtectionLocationListCompositeRepository { ) } } - -extension TunnelControllerIPCClient { - - convenience init() { - self.init(machServiceName: Bundle.main.vpnMenuAgentBundleId) - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index aa2c99a0043..a640eb38a82 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -29,7 +29,7 @@ import NetworkProtectionIPC /// final class NetworkProtectionDebugUtilities { - private let ipcClient: TunnelControllerIPCClient + private let ipcClient: VPNControllerXPCClient private let vpnUninstaller: VPNUninstaller // MARK: - Login Items Management @@ -46,7 +46,7 @@ final class NetworkProtectionDebugUtilities { self.loginItemsManager = loginItemsManager self.settings = settings - let ipcClient = TunnelControllerIPCClient() + let ipcClient = VPNControllerXPCClient.shared self.ipcClient = ipcClient self.vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) @@ -66,7 +66,7 @@ final class NetworkProtectionDebugUtilities { func removeSystemExtensionAndAgents() async throws { try await vpnUninstaller.removeSystemExtension() - vpnUninstaller.disableLoginItems() + vpnUninstaller.removeAgents() } func sendTestNotificationRequest() async throws { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 997b03d7b86..b2a029c8141 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -37,7 +37,7 @@ protocol NetworkProtectionIPCClient { func stop(completion: @escaping (Error?) -> Void) } -extension TunnelControllerIPCClient: NetworkProtectionIPCClient { +extension VPNControllerXPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } public var ipcConnectionErrorObserver: any NetworkProtection.ConnectionErrorObserver { connectionErrorObserver } @@ -49,7 +49,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { let ipcClient: NetworkProtectionIPCClient let vpnUninstaller: VPNUninstalling - init(ipcClient: TunnelControllerIPCClient, + init(ipcClient: VPNControllerXPCClient, vpnUninstaller: VPNUninstalling) { self.ipcClient = ipcClient self.vpnUninstaller = vpnUninstaller diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift index 1b50a1156cd..ace1bd3868a 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/TunnelControllerProvider.swift @@ -26,10 +26,11 @@ final class TunnelControllerProvider { let tunnelController: NetworkProtectionIPCTunnelController private init() { - let ipcClient = TunnelControllerIPCClient() + let ipcClient = VPNControllerXPCClient.shared ipcClient.register { error in NetworkProtectionKnownFailureStore().lastKnownFailure = KnownFailure(error) } + tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift new file mode 100644 index 00000000000..b4ea2f7d65e --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift @@ -0,0 +1,33 @@ +// +// VPNControllerUDSClient+ConvenienceInitializers.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 +import UDSHelper + +extension VPNControllerUDSClient { + convenience init() { + self.init(udsClient: .sharedVPNUDSClient) + } +} + +extension UDSClient { + static let sharedVPNUDSClient: UDSClient = { + return UDSClient(socketFileURL: VPNIPCResources.socketFileURL, log: .networkProtectionIPCLog) + }() +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerXPCClient+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerXPCClient+ConvenienceInitializers.swift new file mode 100644 index 00000000000..0aa7231ee4d --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerXPCClient+ConvenienceInitializers.swift @@ -0,0 +1,29 @@ +// +// VPNControllerXPCClient+ConvenienceInitializers.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 + +extension VPNControllerXPCClient { + + static let shared = VPNControllerXPCClient() + + convenience init() { + self.init(machServiceName: Bundle.main.vpnMenuAgentBundleId) + } +} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index 9a883ab5881..13e733d622e 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -21,6 +21,7 @@ import Foundation import NetworkProtection import NetworkProtectionIPC import PixelKit +import UDSHelper /// VPN tunnel controller through IPC. /// @@ -57,6 +58,7 @@ final class NetworkProtectionIPCTunnelController { init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, + fileManager: FileManager = .default, pixelKit: PixelFiring? = PixelKit.shared, errorRecorder: VPNOperationErrorRecorder = VPNOperationErrorRecorder(), knownFailureStore: NetworkProtectionKnownFailureStore = NetworkProtectionKnownFailureStore()) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index a2c16349017..1757d550a23 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -182,13 +182,8 @@ extension NetworkProtectionUNNotificationsPresenter: UNUserNotificationCenterDel } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - switch UNNotificationAction.Identifier(rawValue: response.actionIdentifier) { - case .reconnect: - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showStatus) - case .none: - try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showStatus) - } + try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showStatus) } } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 8c8520ea2f4..e998a35be89 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -31,7 +31,7 @@ final class PreferencesSidebarModel: ObservableObject { @Published var selectedTabIndex: Int = 0 @Published private(set) var selectedPane: PreferencePaneIdentifier = .defaultBrowser private let vpnVisibility: NetworkProtectionFeatureVisibility - let vpnTunnelIPCClient: TunnelControllerIPCClient + let vpnTunnelIPCClient: VPNControllerXPCClient var selectedTabContent: AnyPublisher { $selectedTabIndex.map { [tabSwitcherTabs] in tabSwitcherTabs[$0] }.eraseToAnyPublisher() @@ -45,7 +45,7 @@ final class PreferencesSidebarModel: ObservableObject { privacyConfigurationManager: PrivacyConfigurationManaging, syncService: DDGSyncing, vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), - vpnTunnelIPCClient: TunnelControllerIPCClient = TunnelControllerIPCClient() + vpnTunnelIPCClient: VPNControllerXPCClient = .shared ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 51f64b8912a..e4e435edda2 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -120,15 +120,17 @@ protocol VPNMetadataCollector { final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter - private let ipcClient: TunnelControllerIPCClient + private let ipcClient: VPNControllerXPCClient private let defaults: UserDefaults private let accountManager: AccountManaging private let settings: VPNSettings init(defaults: UserDefaults = .netP, accountManager: AccountManaging) { - let ipcClient = TunnelControllerIPCClient() + + let ipcClient = VPNControllerXPCClient.shared ipcClient.register { _ in } + self.accountManager = accountManager self.ipcClient = ipcClient self.defaults = defaults diff --git a/DuckDuckGo/Waitlist/IPCServiceLauncher.swift b/DuckDuckGo/Waitlist/IPCServiceLauncher.swift new file mode 100644 index 00000000000..579793a5d3b --- /dev/null +++ b/DuckDuckGo/Waitlist/IPCServiceLauncher.swift @@ -0,0 +1,94 @@ +// +// IPCServiceLauncher.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 AppLauncher +import Common +import Foundation +import LoginItems +import NetworkProtectionIPC + +final class IPCServiceLauncher { + + enum DisableError: Error { + case failedToStopService + case serviceNotRunning + } + + enum LaunchMethod { + case direct(bundleID: String, appLauncher: AppLauncher) + case loginItem(loginItem: LoginItem, loginItemsManager: LoginItemsManager) + } + + private let launchMethod: LaunchMethod + private var runningApplication: NSRunningApplication? + + init(launchMethod: LaunchMethod) { + self.launchMethod = launchMethod + } + + /// Enables the IPC service + /// + func enable() async throws { + switch launchMethod { + case .direct(let bundleID, let appLauncher): + runningApplication = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).first + + guard runningApplication == nil else { + return + } + + struct UDSLaunchAppCommand: AppLaunchCommand { + var allowsRunningApplicationSubstitution = true + var launchURL: URL? + var hideApp = true + } + + runningApplication = try await appLauncher.runApp(withCommand: UDSLaunchAppCommand()) + case .loginItem(let loginItem, let loginItemsManager): + try loginItemsManager.throwingEnableLoginItems([loginItem], log: .disabled) + } + } + + /// Disables the IPC service. + /// + /// - Throws: ``DisableError`` + /// + func disable() async throws { + switch launchMethod { + case .direct: + guard let runningApplication else { + throw DisableError.serviceNotRunning + } + + runningApplication.terminate() + + try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) + + if !runningApplication.isTerminated { + runningApplication.forceTerminate() + } + + if !runningApplication.isTerminated { + throw DisableError.failedToStopService + } + + case .loginItem(let loginItem, let loginItemsManager): + loginItemsManager.disableLoginItems([loginItem]) + } + } +} diff --git a/DuckDuckGo/Waitlist/VPNUninstaller.swift b/DuckDuckGo/Waitlist/VPNUninstaller.swift index 0de31fcf6a2..20d3b621a4c 100644 --- a/DuckDuckGo/Waitlist/VPNUninstaller.swift +++ b/DuckDuckGo/Waitlist/VPNUninstaller.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppLauncher import BrowserServicesKit import Common import LoginItems @@ -81,16 +82,16 @@ final class VPNUninstaller: VPNUninstalling { var name: String { switch self { case .begin: - return "vpn_browser_uninstall_attempt_ipc" + return "vpn_browser_uninstall_attempt_uds" case .cancelled: - return "vpn_browser_uninstall_cancelled_ipc" + return "vpn_browser_uninstall_cancelled_uds" case .success: - return "vpn_browser_uninstall_success_ipc" + return "vpn_browser_uninstall_success_uds" case .failure: - return "vpn_browser_uninstall_failure_ipc" + return "vpn_browser_uninstall_failure_uds" } } @@ -118,27 +119,36 @@ final class VPNUninstaller: VPNUninstalling { } private let log: OSLog + private let ipcServiceLauncher: IPCServiceLauncher 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 ipcClient: VPNControllerIPCClient private let pixelKit: PixelFiring? @MainActor private var isDisabling = false - init(loginItemsManager: LoginItemsManaging = LoginItemsManager(), + init(ipcServiceLauncher: IPCServiceLauncher? = nil, + loginItemsManager: LoginItemsManaging = LoginItemsManager(), pinningManager: LocalPinningManager = .shared, userDefaults: UserDefaults = .netP, settings: VPNSettings = .init(defaults: .netP), - ipcClient: TunnelControllerIPCClient = TunnelControllerIPCClient(), + ipcClient: VPNControllerIPCClient = VPNControllerUDSClient(), vpnMenuLoginItem: LoginItem = .vpnMenu, pixelKit: PixelFiring? = PixelKit.shared, log: OSLog = .networkProtection) { + let vpnAgentBundleID = Bundle.main.vpnMenuAgentBundleId + let appLauncher = AppLauncher(appBundleURL: Bundle.main.vpnMenuAgentURL) + let ipcServiceLaunchMethod = IPCServiceLauncher.LaunchMethod.direct( + bundleID: vpnAgentBundleID, + appLauncher: appLauncher) + self.log = log + self.ipcServiceLauncher = ipcServiceLauncher ?? IPCServiceLauncher(launchMethod: ipcServiceLaunchMethod) self.loginItemsManager = loginItemsManager self.pinningManager = pinningManager self.settings = settings @@ -176,15 +186,20 @@ final class VPNUninstaller: VPNUninstalling { } do { - try enableLoginItems() + try await ipcServiceLauncher.enable() } catch { throw UninstallError.runAgentError(error) } // Allow some time for the login items to fully launch + try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) do { - try await ipcClient.command(.uninstallVPN) + if removeSystemExtension { + try await ipcClient.uninstall(.all) + } else { + try await ipcClient.uninstall(.configuration) + } } catch { print("Failed to uninstall VPN, with error: \(error.localizedDescription)") @@ -200,7 +215,23 @@ final class VPNUninstaller: VPNUninstalling { // We want to give some time for the login item to reset state before disabling it try? await Task.sleep(interval: 0.5) - disableLoginItems() + + // Workaround: since status updates are provided through XPC we want to make sure the + // VPN is marked as disconnected. We may be able to more properly resolve this by using + // UDS for all VPN status updates. + // + // Ref: https://app.asana.com/0/0/1207499177312396/1207538373572594/f + // + VPNControllerXPCClient.shared.forceStatusToDisconnected() + + // While it may seem like a duplication of code, it's one thing to disable the IPC service + // and it's nother one to "uninstall" our login items. The uninstaller wants both things + // to happen. + // + // As an example of why this is important, we want all agents to be disabled even if the IPC + // service is not based on login items. + try await ipcServiceLauncher.disable() + removeAgents() notifyVPNUninstalled() isDisabling = false @@ -213,18 +244,14 @@ final class VPNUninstaller: VPNUninstalling { } } - private func enableLoginItems() throws { - try loginItemsManager.throwingEnableLoginItems(LoginItemsManager.networkProtectionLoginItems, log: log) - } - - func disableLoginItems() { + func removeAgents() { loginItemsManager.disableLoginItems(LoginItemsManager.networkProtectionLoginItems) } func removeSystemExtension() async throws { #if NETP_SYSTEM_EXTENSION do { - try await ipcClient.command(.removeSystemExtension) + try await ipcClient.uninstall(.all) } catch { throw UninstallError.systemExtensionError(error) } @@ -238,7 +265,7 @@ final class VPNUninstaller: VPNUninstalling { private func removeVPNConfiguration() async throws { // Remove the agent VPN configuration do { - try await ipcClient.command(.removeVPNConfiguration) + try await ipcClient.uninstall(.configuration) } catch { throw UninstallError.vpnConfigurationError(error) } diff --git a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements index 3701cb8c931..cdb83c97401 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements @@ -12,11 +12,11 @@ com.apple.security.application-groups $(NETP_APP_GROUP) + $(IPC_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) keychain-access-groups - $(AppIdentifierPrefix)$(NETP_APP_GROUP) $(NETP_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index b80094a9349..3f496db8cb3 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -38,7 +38,12 @@ final class DuckDuckGoVPNApplication: NSApplication { private let _delegate: DuckDuckGoVPNAppDelegate override init() { - os_log(.error, log: .networkProtection, "🟢 Status Bar Agent starting: %{public}d", NSRunningApplication.current.processIdentifier) + os_log(.default, + log: .networkProtection, + "🟢 Status Bar Agent starting\nPath: (%{public}@)\nVersion: %{public}@\nPID: %{public}d", + Bundle.main.bundlePath, + "\(Bundle.main.versionNumber!).\(Bundle.main.buildNumber)", + NSRunningApplication.current.processIdentifier) // prevent agent from running twice if let anotherInstance = NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier!).first(where: { $0 != .current }) { diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements index fd8ab618877..3c7103631f7 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppStore.entitlements @@ -20,6 +20,7 @@ com.apple.security.application-groups + $(IPC_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) $(NETP_APP_GROUP) diff --git a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements index 6fa17c3b425..2bb2f334b01 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements @@ -12,6 +12,7 @@ com.apple.security.application-groups $(NETP_APP_GROUP) + $(IPC_APP_GROUP) $(SUBSCRIPTION_APP_GROUP) keychain-access-groups diff --git a/DuckDuckGoVPN/Info-AppStore.plist b/DuckDuckGoVPN/Info-AppStore.plist index 26b434800b1..aa02b530271 100644 --- a/DuckDuckGoVPN/Info-AppStore.plist +++ b/DuckDuckGoVPN/Info-AppStore.plist @@ -16,5 +16,7 @@ public.app-category.productivity CFBundleShortVersionString $(MARKETING_VERSION) + IPC_APP_GROUP + $(IPC_APP_GROUP) diff --git a/DuckDuckGoVPN/Info.plist b/DuckDuckGoVPN/Info.plist index 26b434800b1..8e16c481689 100644 --- a/DuckDuckGoVPN/Info.plist +++ b/DuckDuckGoVPN/Info.plist @@ -4,8 +4,12 @@ DISTRIBUTED_NOTIFICATIONS_PREFIX $(DISTRIBUTED_NOTIFICATIONS_PREFIX) + IPC_APP_GROUP + $(IPC_APP_GROUP) NETP_APP_GROUP $(NETP_APP_GROUP) + SYSEX_BUNDLE_ID + $(SYSEX_BUNDLE_ID) PROXY_EXTENSION_BUNDLE_ID $(PROXY_EXTENSION_BUNDLE_ID) SUBSCRIPTION_APP_GROUP diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index c79aedc9861..e8eeb5e68ab 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -21,6 +21,8 @@ import Foundation import NetworkProtection import NetworkProtectionIPC import NetworkProtectionUI +import PixelKit +import UDSHelper /// Takes care of handling incoming IPC requests from clients that need to be relayed to the tunnel, and handling state /// changes that need to be relayed back to IPC clients. @@ -32,10 +34,12 @@ final class TunnelControllerIPCService { private let tunnelController: NetworkProtectionTunnelController private let networkExtensionController: NetworkExtensionController private let uninstaller: VPNUninstalling - private let server: NetworkProtectionIPC.TunnelControllerIPCServer + private let server: NetworkProtectionIPC.VPNControllerXPCServer private let statusReporter: NetworkProtectionStatusReporter private var cancellables = Set() private let defaults: UserDefaults + private let pixelKit: PixelKit? + private let udsServer: UDSServer enum IPCError: SilentErrorConvertible { case versionMismatched @@ -47,11 +51,35 @@ final class TunnelControllerIPCService { } } + enum UDSError: PixelKitEventV2 { + case udsServerStartFailure(_ error: Error) + + var name: String { + switch self { + case .udsServerStartFailure: + return "vpn_agent_uds_server_start_failure" + } + } + + var error: Error? { + switch self { + case .udsServerStartFailure(let error): + return error + } + } + + var parameters: [String: String]? { + return nil + } + } + init(tunnelController: NetworkProtectionTunnelController, uninstaller: VPNUninstalling, networkExtensionController: NetworkExtensionController, statusReporter: NetworkProtectionStatusReporter, - defaults: UserDefaults = .netP) { + fileManager: FileManager = .default, + defaults: UserDefaults = .netP, + pixelKit: PixelKit? = .shared) { self.tunnelController = tunnelController self.uninstaller = uninstaller @@ -59,6 +87,9 @@ final class TunnelControllerIPCService { server = .init(machServiceName: Bundle.main.bundleIdentifier!) self.statusReporter = statusReporter self.defaults = defaults + self.pixelKit = pixelKit + + udsServer = UDSServer(socketFileURL: VPNIPCResources.socketFileURL, log: .networkProtectionIPCLog) subscribeToErrorChanges() subscribeToStatusUpdates() @@ -71,6 +102,23 @@ final class TunnelControllerIPCService { public func activate() { server.activate() + + do { + try udsServer.start { [weak self] message in + guard let self else { return nil } + + let command = try JSONDecoder().decode(VPNIPCClientCommand.self, from: message) + + switch command { + case .uninstall(let component): + try await uninstall(component) + return nil + } + } + } catch { + pixelKit?.fire(UDSError.udsServerStartFailure(error)) + assertionFailure(error.localizedDescription) + } } private func subscribeToErrorChanges() { @@ -121,7 +169,7 @@ final class TunnelControllerIPCService { // MARK: - Requests from the client -extension TunnelControllerIPCService: IPCServerInterface { +extension TunnelControllerIPCService: XPCServerInterface { func register(completion: @escaping (Error?) -> Void) { register(version: version, bundlePath: bundlePath, completion: completion) @@ -183,7 +231,7 @@ extension TunnelControllerIPCService: IPCServerInterface { switch command { case .removeSystemExtension: - try await uninstaller.removeSystemExtension() + try await uninstall(.systemExtension) case .expireRegistrationKey: // Intentional no-op: handled by the extension break @@ -191,14 +239,25 @@ extension TunnelControllerIPCService: IPCServerInterface { // Intentional no-op: handled by the extension break case .removeVPNConfiguration: - try await uninstaller.removeVPNConfiguration() + try await uninstall(.configuration) case .uninstallVPN: - try await uninstaller.uninstall(includingSystemExtension: true) + try await uninstall(.all) case .disableConnectOnDemandAndShutDown: // Not implemented on macOS yet break } } + + private func uninstall(_ component: VPNUninstallComponent) async throws { + switch component { + case .all: + try await uninstaller.uninstall(includingSystemExtension: true) + case .configuration: + try await uninstaller.removeVPNConfiguration() + case .systemExtension: + try await uninstaller.removeSystemExtension() + } + } } // MARK: - Error Handling diff --git a/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift b/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift index c09cb039409..8dee99d87d7 100644 --- a/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift +++ b/LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift @@ -21,6 +21,7 @@ import Foundation public protocol AppLaunching { func launchApp(withCommand command: AppLaunchCommand) async throws + func runApp(withCommand command: AppLaunchCommand) async throws -> NSRunningApplication } /// Launches the main App @@ -51,6 +52,13 @@ public final class AppLauncher: AppLaunching { } public func launchApp(withCommand command: AppLaunchCommand) async throws { + _ = try await runApp(withCommand: command) + } + + /// The only difference with launchApp is this method returns the `NSRunningApplication` + /// + public func runApp(withCommand command: AppLaunchCommand) async throws -> NSRunningApplication { + let configuration = NSWorkspace.OpenConfiguration() configuration.allowsRunningApplicationSubstitution = command.allowsRunningApplicationSubstitution @@ -68,7 +76,9 @@ public final class AppLauncher: AppLaunching { do { if let launchURL = command.launchURL { - try await NSWorkspace.shared.open([launchURL], withApplicationAt: mainBundleURL, configuration: configuration) + return try await NSWorkspace.shared.open([launchURL], withApplicationAt: mainBundleURL, configuration: configuration) + } else { + return try await NSWorkspace.shared.openApplication(at: mainBundleURL, configuration: configuration) } } catch { throw AppLaunchError.workspaceOpenError(error) diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index ed49843a93b..a87366d64af 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -35,6 +35,7 @@ let package = Package( .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "153.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../AppLauncher"), + .package(path: "../UDSHelper"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), @@ -47,6 +48,7 @@ let package = Package( dependencies: [ .product(name: "NetworkProtection", package: "BrowserServicesKit"), .product(name: "XPCHelper", package: "XPCHelper"), + .product(name: "UDSHelper", package: "UDSHelper"), .product(name: "PixelKit", package: "BrowserServicesKit"), ], swiftSettings: [ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerIPCClient.swift new file mode 100644 index 00000000000..657a9206661 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerIPCClient.swift @@ -0,0 +1,26 @@ +// +// VPNControllerIPCClient.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 NetworkProtection + +// Base protocol for any IPC server we implement. +// +public protocol VPNControllerIPCClient { + + func uninstall(_ component: VPNUninstallComponent) async throws +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerUDSClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerUDSClient.swift new file mode 100644 index 00000000000..4a04409fffc --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerUDSClient.swift @@ -0,0 +1,38 @@ +// +// VPNControllerUDSClient.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 UDSHelper + +public final class VPNControllerUDSClient { + + private let udsClient: UDSClient + private let encoder = JSONEncoder() + + public init(udsClient: UDSClient) { + self.udsClient = udsClient + } +} + +extension VPNControllerUDSClient: VPNControllerIPCClient { + + public func uninstall(_ component: VPNUninstallComponent) async throws { + let payload = try encoder.encode(VPNIPCClientCommand.uninstall(component)) + try await udsClient.send(payload) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerXPCClient.swift similarity index 84% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerXPCClient.swift index 84a68750d0e..214f08f9e57 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerXPCClient.swift @@ -1,5 +1,5 @@ // -// TunnelControllerIPCClient.swift +// VPNControllerXPCClient.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +22,7 @@ import XPCHelper /// This protocol describes the client-side IPC interface for controlling the tunnel /// -public protocol IPCClientInterface: AnyObject { +public protocol XPCClientInterface: AnyObject { func errorChanged(_ error: String?) func serverInfoChanged(_ serverInfo: NetworkProtectionStatusServerInfo) func statusChanged(_ status: ConnectionStatus) @@ -32,7 +32,7 @@ public protocol IPCClientInterface: AnyObject { /// This is the XPC interface with parameters that can be packed properly @objc -protocol XPCClientInterface { +protocol XPCClientInterfaceObjC { func errorChanged(error: String?) func serverInfoChanged(payload: Data) func statusChanged(payload: Data) @@ -40,11 +40,11 @@ protocol XPCClientInterface { func knownFailureUpdated(failure: KnownFailure?) } -public final class TunnelControllerIPCClient { +public final class VPNControllerXPCClient { // MARK: - XPC Communication - let xpc: XPCClient + let xpc: XPCClient // MARK: - Observers offered @@ -56,7 +56,7 @@ public final class TunnelControllerIPCClient { /// The delegate. /// - public weak var clientDelegate: IPCClientInterface? { + public weak var clientDelegate: XPCClientInterface? { didSet { xpcDelegate.clientDelegate = self.clientDelegate } @@ -65,8 +65,8 @@ public final class TunnelControllerIPCClient { private let xpcDelegate: TunnelControllerXPCClientDelegate public init(machServiceName: String) { - let clientInterface = NSXPCInterface(with: XPCClientInterface.self) - let serverInterface = NSXPCInterface(with: XPCServerInterface.self) + let clientInterface = NSXPCInterface(with: XPCClientInterfaceObjC.self) + let serverInterface = NSXPCInterface(with: XPCServerInterfaceObjC.self) self.xpcDelegate = TunnelControllerXPCClientDelegate( clientDelegate: self.clientDelegate, serverInfoObserver: self.serverInfoObserver, @@ -97,18 +97,28 @@ public final class TunnelControllerIPCClient { self.register { _ in } } + + /// Forces the XPC client status to be updated to disconnected. + /// + /// This is just used as a temporary mechanism to allow the main app to tell that the VPN has been disconnected + /// when it's uninstalled. You should not call this method directly or rely on this for other logic. This should be + /// replaced by status updates through XPC. + /// + public func forceStatusToDisconnected() { + xpcDelegate.statusChanged(status: .disconnected) + } } -private final class TunnelControllerXPCClientDelegate: XPCClientInterface { +private final class TunnelControllerXPCClientDelegate: XPCClientInterfaceObjC { - weak var clientDelegate: IPCClientInterface? + weak var clientDelegate: XPCClientInterface? let serverInfoObserver: ConnectionServerInfoObserverThroughIPC let connectionErrorObserver: ConnectionErrorObserverThroughIPC let connectionStatusObserver: ConnectionStatusObserverThroughIPC let dataVolumeObserver: DataVolumeObserverThroughIPC let knownFailureObserver: KnownFailureObserverThroughIPC - init(clientDelegate: IPCClientInterface?, + init(clientDelegate: XPCClientInterface?, serverInfoObserver: ConnectionServerInfoObserverThroughIPC, connectionErrorObserver: ConnectionErrorObserverThroughIPC, connectionStatusObserver: ConnectionStatusObserverThroughIPC, @@ -141,6 +151,10 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { return } + statusChanged(status: status) + } + + func statusChanged(status: ConnectionStatus) { connectionStatusObserver.publish(status) clientDelegate?.statusChanged(status) } @@ -162,7 +176,8 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { // MARK: - Outgoing communication to the server -extension TunnelControllerIPCClient: IPCServerInterface { +extension VPNControllerXPCClient: XPCServerInterface { + public func register(completion: @escaping (Error?) -> Void) { register(version: version, bundlePath: bundlePath, completion: self.onComplete(completion)) } @@ -220,3 +235,18 @@ extension TunnelControllerIPCClient: IPCServerInterface { } } } + +extension VPNControllerXPCClient: VPNControllerIPCClient { + + public func uninstall(_ component: VPNUninstallComponent) async throws { + switch component { + case .all: + try await self.command(.uninstallVPN) + case .configuration: + try await self.command(.removeVPNConfiguration) + case .systemExtension: + try await self.command(.removeSystemExtension) + } + + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerXPCServer.swift similarity index 93% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerXPCServer.swift index 576e270415d..58d3a5f4fac 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNControllerXPCServer.swift @@ -1,5 +1,5 @@ // -// TunnelControllerIPCServer.swift +// VPNControllerXPCServer.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +22,7 @@ import XPCHelper /// This protocol describes the server-side IPC interface for controlling the tunnel /// -public protocol IPCServerInterface: AnyObject { +public protocol XPCServerInterface: AnyObject { var version: String { get } var bundlePath: String { get } @@ -59,7 +59,7 @@ public protocol IPCServerInterface: AnyObject { func command(_ command: VPNCommand) async throws } -public extension IPCServerInterface { +public extension XPCServerInterface { var version: String { DefaultIPCMetadataCollector.version } var bundlePath: String { DefaultIPCMetadataCollector.bundlePath } } @@ -70,7 +70,7 @@ public extension IPCServerInterface { /// calls to the IPC interface when appropriate. /// @objc -protocol XPCServerInterface { +protocol XPCServerInterfaceObjC { /// Registers a connection with the server. /// /// This is the point where the server will start sending status updates to the client. @@ -96,8 +96,8 @@ protocol XPCServerInterface { func command(_ payload: Data, completion: @escaping (Error?) -> Void) } -public final class TunnelControllerIPCServer { - let xpc: XPCServer +public final class VPNControllerXPCServer { + let xpc: XPCServer enum IPCError: Error { case cannotDecodeDebugCommand @@ -105,11 +105,11 @@ public final class TunnelControllerIPCServer { /// The delegate. /// - public weak var serverDelegate: IPCServerInterface? + public weak var serverDelegate: XPCServerInterface? public init(machServiceName: String) { - let clientInterface = NSXPCInterface(with: XPCClientInterface.self) - let serverInterface = NSXPCInterface(with: XPCServerInterface.self) + let clientInterface = NSXPCInterface(with: XPCClientInterfaceObjC.self) + let serverInterface = NSXPCInterface(with: XPCServerInterfaceObjC.self) xpc = XPCServer( machServiceName: machServiceName, @@ -126,7 +126,7 @@ public final class TunnelControllerIPCServer { // MARK: - Outgoing communication to the clients -extension TunnelControllerIPCServer: IPCClientInterface { +extension VPNControllerXPCServer: XPCClientInterface { public func errorChanged(_ error: String?) { xpc.forEachClient { client in @@ -187,7 +187,8 @@ extension TunnelControllerIPCServer: IPCClientInterface { // MARK: - Incoming communication from a client -extension TunnelControllerIPCServer: XPCServerInterface { +extension VPNControllerXPCServer: XPCServerInterfaceObjC { + func register(completion: @escaping (Error?) -> Void) { serverDelegate?.register(completion: completion) } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNIPCClientCommand.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNIPCClientCommand.swift new file mode 100644 index 00000000000..ec541efbc10 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/VPNIPCClientCommand.swift @@ -0,0 +1,29 @@ +// +// VPNIPCClientCommand.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 enum VPNUninstallComponent: Codable { + case all + case configuration + case systemExtension +} + +public enum VPNIPCClientCommand: Codable { + case uninstall(_ component: VPNUninstallComponent) +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index c4ede6adeb4..84db127c57c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -223,7 +223,7 @@ public struct TunnelControllerView: View { loopStartFrame: 130, loopEndFrame: 370 ), isAnimating: $model.isVPNEnabled) -} + } @ViewBuilder private func statusBadge(isConnected: Bool) -> some View { diff --git a/LocalPackages/UDSHelper/.gitignore b/LocalPackages/UDSHelper/.gitignore new file mode 100644 index 00000000000..0023a534063 --- /dev/null +++ b/LocalPackages/UDSHelper/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LocalPackages/UDSHelper/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LocalPackages/UDSHelper/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000000..18d981003d6 --- /dev/null +++ b/LocalPackages/UDSHelper/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/LocalPackages/UDSHelper/Package.swift b/LocalPackages/UDSHelper/Package.swift new file mode 100644 index 00000000000..954c358c885 --- /dev/null +++ b/LocalPackages/UDSHelper/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "UDSHelper", + platforms: [ + .macOS("11.4") + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "UDSHelper", + targets: ["UDSHelper"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "UDSHelper"), + .testTarget( + name: "UDSHelperTests", + dependencies: ["UDSHelper"]), + ] +) diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift new file mode 100644 index 00000000000..023f350761a --- /dev/null +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift @@ -0,0 +1,236 @@ +// +// UDSClient.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 Network +// swiftlint:disable:next enforce_os_log_wrapper +import os.log + +public actor UDSClient { + + public typealias PayloadHandler = (Data) async throws -> Void + + enum ConnectionError: Error { + case cancelled + case failure(_ error: Error) + } + + private var internalConnection: NWConnection? + private let socketFileURL: URL + private let receiver: UDSReceiver + private let queue = DispatchQueue(label: "com.duckduckgo.UDSConnection.queue.\(UUID().uuidString)") + private let log: OSLog + private let payloadHandler: PayloadHandler? + + // MARK: - Message completion callbacks + + typealias Callback = (Data?) async -> Void + + private var responseCallbacks = [UUID: Callback]() + + // MARK: - Initializers + + /// This should not be called directly because the socketFileURL needs to comply with some requirements in terms of + /// maximum length of the path. Use any public factory method provided below instead. + /// + public init(socketFileURL: URL, + log: OSLog, + payloadHandler: PayloadHandler? = nil) { + + os_log("UDSClient - Initialized with path: %{public}@", log: log, type: .info, socketFileURL.path) + + self.receiver = UDSReceiver(log: log) + self.socketFileURL = socketFileURL + self.log = log + self.payloadHandler = payloadHandler + } + + // MARK: - Connection Management + + private func connection() async throws -> NWConnection { + guard let internalConnection, + internalConnection.state == .ready else { + + return try await connect() + } + + return internalConnection + } + + /// Establishes a new connection + /// + private func connect() async throws -> NWConnection { + let endpoint = NWEndpoint.unix(path: socketFileURL.path) + let parameters = NWParameters.tcp + let connection = NWConnection(to: endpoint, using: parameters) + + connection.stateUpdateHandler = { state in + Task { + await self.statusUpdateHandler(state) + } + } + + startReceivingMessages(on: connection) + + internalConnection = connection + connection.start(queue: queue) + + while connection.state != .ready { + switch connection.state { + case .cancelled: + throw ConnectionError.cancelled + case .failed(let error): + throw ConnectionError.failure(error) + default: + try await Task.sleep(nanoseconds: 200 * MSEC_PER_SEC) + } + } + + return connection + } + + private func statusUpdateHandler(_ state: NWConnection.State) { + switch state { + case .cancelled: + os_log("UDSClient - Connection cancelled", log: self.log, type: .info) + + self.releaseConnection() + case .failed(let error): + os_log("UDSClient - Connection failed with error: %{public}@", log: self.log, type: .error, String(describing: error)) + + self.releaseConnection() + case .ready: + os_log("UDSClient - Connection ready", log: self.log, type: .info) + case .waiting(let error): + os_log("UDSClient - Waiting to connect... %{public}@", log: self.log, type: .info, String(describing: error)) + default: + os_log("UDSClient - Unexpected state", log: self.log, type: .info) + } + } + + private func releaseConnection() { + internalConnection?.stateUpdateHandler = nil + internalConnection = nil + } + + // MARK: - Sending commands + + @discardableResult + public func send(_ payload: Data) async throws -> Data? { + let uuid = UUID() + let message = UDSMessage(uuid: uuid, body: .request(payload)) + + return try await send(message) + } + + private func send(_ message: UDSMessage) async throws -> Data? { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + + Task { + await send(message) { result in + switch result { + case .success(let data): + continuation.resume(returning: data) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + } + + private func send(_ message: UDSMessage, completion: @escaping (Result) async -> Void) async { + + do { + let data = try JSONEncoder().encode(message) + let lengthData = withUnsafeBytes(of: UDSMessageLength(data.count)) { + Data($0) + } + let payload = lengthData + data + let connection = try await connection() + + assert(responseCallbacks[message.uuid] == nil) + responseCallbacks[message.uuid] = { (data: Data?) in + await completion(.success(data)) + } + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: payload, completion: .contentProcessed { error in + if let error { + os_log("UDSClient - Send Error %{public}@", log: self.log, String(describing: error)) + continuation.resume(throwing: error) + return + } + + os_log("UDSClient - Send Success", log: self.log) + continuation.resume() + }) + } + } catch { + responseCallbacks.removeValue(forKey: message.uuid) + await completion(.failure(error)) + } + } + + /// Starts receiveing messages for a specific connection + /// + /// - Parameters: + /// - connection: the connection to receive messages for. + /// + private func startReceivingMessages(on connection: NWConnection) { + + receiver.startReceivingMessages(on: connection) { [weak self] message in + guard let self else { return false } + + switch message.body { + case .request(let payload): + try await payloadHandler?(payload) + case .response(let response): + await handleResponse(uuid: message.uuid, response: response, on: connection) + } + + return true + } onError: { [weak self] _ in + guard let self else { return false } + await self.closeConnection(connection) + return false + } + } + + private func handleResponse(uuid: UUID, response: UDSMessageResponse, on connection: NWConnection) async { + guard let callback = responseCallbacks[uuid] else { + return + } + + responseCallbacks.removeValue(forKey: uuid) + + switch response { + case .success(let data): + await callback(data) + case .failure: + await callback(nil) + } + + return + } + + private func closeConnection(_ connection: NWConnection) { + internalConnection?.cancel() + internalConnection = nil + } +} diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSMessage.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSMessage.swift new file mode 100644 index 00000000000..2eb4117390e --- /dev/null +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSMessage.swift @@ -0,0 +1,38 @@ +// +// UDSMessage.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 enum UDSMessageResponse: Codable { + case success(_ data: Data?) + case failure +} + +public enum UDSMessageBody: Codable { + case request(_ data: Data) + case response(_ response: UDSMessageResponse) +} + +public struct UDSMessage: Codable { + public let uuid: UUID + public let body: UDSMessageBody + + public func successResponse(withPayload payload: Data?) -> UDSMessage { + UDSMessage(uuid: uuid, body: .response(.success(payload))) + } +} diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift new file mode 100644 index 00000000000..8d82a4ecdb4 --- /dev/null +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift @@ -0,0 +1,188 @@ +// +// UDSReceiver.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 Network +// swiftlint:disable:next enforce_os_log_wrapper +import os.log + +typealias UDSMessageLength = UInt16 + +struct UDSReceiver { + + /// The return value allows the callback handler to continue receiving messages (if it returns `true`) + /// or stop receiving messages (when it returns `false`). + /// + typealias MessageHandler = (UDSMessage) async throws -> Bool + + enum ReadError: Error { + case notEnoughData(expected: Int, received: Int) + case connectionError(_ error: Error) + case connectionClosed + } + + private let log: OSLog + + init(log: OSLog) { + self.log = log + } + + /// Starts receiveing messages for a specific connection + /// + /// - Parameters: + /// - connection: the connection to receive messages for. + /// - messageHandler: the callback for important events. + /// + func startReceivingMessages(on connection: NWConnection, messageHandler: @escaping MessageHandler, onError errorHandler: @escaping (Error) async -> Bool) { + Task { + await runReceiveMessageLoop(on: connection, messageHandler: messageHandler, onError: errorHandler) + } + } + + // swiftlint:disable:next cyclomatic_complexity + private func runReceiveMessageLoop(on connection: NWConnection, messageHandler: @escaping MessageHandler, onError errorHandler: @escaping (Error) async -> Bool) async { + + while true { + do { + let length = try await receiveMessageLength(on: connection) + let message = try await receiveEncodedObjectData(ofLength: length, on: connection) + + guard try await messageHandler(message) else { + return + } + } catch { + switch error { + case ReadError.notEnoughData(let expected, let received): + os_log("UDSServer - Connection closing due to error: Not enough data (expected: %{public}@, received: %{public}@", + log: log, + type: .error, + String(describing: expected), + String(describing: received)) + + guard await errorHandler(error) else { + return + } + case ReadError.connectionError(let error): + os_log("UDSServer - Connection closing due to a connection error: %{public}@", + log: log, + type: .error, + String(describing: error)) + + guard await errorHandler(error) else { + return + } + case ReadError.connectionClosed: + os_log("UDSServer - Connection closing: End of file reached", + log: log, + type: .info) + + guard await errorHandler(error) else { + return + } + default: + os_log("UDSServer - Connection closing due to error: %{public}@", + log: log, + type: .error, + String(describing: error)) + + guard await errorHandler(error) else { + return + } + } + } + } + } + + /// Receives the length value for the next message in the data stream. + /// + /// - Parameters: + /// - connection: the connection through which we're receveing messages + /// + /// - Returns: the length of the next message + /// + private func receiveMessageLength(on connection: NWConnection) async throws -> UDSMessageLength { + try await withCheckedThrowingContinuation { continuation in + let messageLengthMemorySize = MemoryLayout.size + + connection.receive(minimumIncompleteLength: messageLengthMemorySize, maximumLength: messageLengthMemorySize) { (data, _, isComplete, error) in + + if let data = data { + guard data.count == messageLengthMemorySize else { + continuation.resume(throwing: ReadError.notEnoughData(expected: messageLengthMemorySize, received: data.count)) + return + } + + let length = data.withUnsafeBytes { $0.load(as: UDSMessageLength.self) } + continuation.resume(returning: length) + } + + if let error { + continuation.resume(throwing: ReadError.connectionError(error)) + return + } + + guard !isComplete else { + continuation.resume(throwing: ReadError.connectionClosed) + return + } + } + } + } + + /// Decodes an incoming message. + /// + /// - Parameters: + /// - length: the length of the data that represents the next message. + /// - connection: the connection through which we're receiving the message. + /// + /// - Returns: a message on success. + /// + private func receiveEncodedObjectData(ofLength length: UDSMessageLength, on connection: NWConnection) async throws -> UDSMessage { + try await withCheckedThrowingContinuation { continuation in + connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { (data, _, isComplete, error) in + if let data = data { + guard data.count == length else { + continuation.resume(throwing: ReadError.notEnoughData(expected: Int(length), received: data.count)) + return + } + + do { + let message = try self.decodeMessage(from: data) + continuation.resume(returning: message) + } catch { + continuation.resume(throwing: error) + } + } + + if let error { + continuation.resume(throwing: ReadError.connectionError(error)) + return + } + + guard !isComplete else { + continuation.resume(throwing: ReadError.connectionClosed) + return + } + } + } + } + + private func decodeMessage(from data: Data) throws -> UDSMessage { + try JSONDecoder().decode(UDSMessage.self, from: data) + } +} diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift new file mode 100644 index 00000000000..41812e58196 --- /dev/null +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift @@ -0,0 +1,276 @@ +// +// UDSServer.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 Network +// swiftlint:disable:next enforce_os_log_wrapper +import os.log + +/// Convenience Hashable support for `NWConnection`, so we can use `Set` +/// +extension NWConnection: Hashable { + public static func == (lhs: NWConnection, rhs: NWConnection) -> Bool { + return lhs === rhs + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +/// An actor to manage client connections in a thread-safe manner. +/// +private actor ClientConnections { + var connections = Set() + + func forEach(perform closure: (NWConnection) -> Void) { + // Copy the set for looping so that operations that modify the set + // won't cause trouble. + let connections = connections + + for connection in connections { + closure(connection) + } + } + + func insert(_ connection: NWConnection) { + connections.insert(connection) + } + + func remove(_ connection: NWConnection) { + connections.remove(connection) + } + + func removeAll() { + connections = Set() + } +} + +/// Unix Domain Socket server +/// +public final class UDSServer { + private let listenerQueue = DispatchQueue(label: "com.duckduckgo.UDSServer.listenerQueue") + private let connectionQueue = DispatchQueue(label: "com.duckduckgo.UDSServer.connectionQueue") + + private var listener: NWListener? + private var connections = ClientConnections() + + private let receiver: UDSReceiver + + private let fileManager: FileManager + private let socketFileURL: URL + private let log: OSLog + + /// Default initializer + /// + /// - Parameters: + /// - socketFileDirectory: the directory where we want the socket file to be created. If you're planning + /// to share this socket with other apps in the same app group, this path should be in an app group + /// that both apps have access to. + /// - socketFileName: the name of the socket file + /// - log: the log to use + /// + public init(socketFileURL: URL, fileManager: FileManager = .default, log: OSLog) { + self.fileManager = fileManager + self.socketFileURL = socketFileURL + self.log = log + self.receiver = UDSReceiver(log: log) + + do { + try fileManager.removeItem(at: socketFileURL) + } catch { + print(error) + } + + os_log("UDSServer - Initialized with path: %{public}@", log: log, type: .info, socketFileURL.path) + } + + public func start(messageHandler: @escaping (Data) async throws -> Data?) throws { + let listener: NWListener + + do { + let params = NWParameters() + params.defaultProtocolStack.transportProtocol = NWProtocolTCP.Options() + params.requiredLocalEndpoint = NWEndpoint.unix(path: socketFileURL.path) + params.allowLocalEndpointReuse = true + // IMPORTANT: I'm leaving the following line commented because I want to document + // that enabling it seems to break the UDS listener completely. + // params.acceptLocalOnly = true + + listener = try NWListener(using: params) + self.listener = listener + } catch { + os_log("UDSServer - Error creating listener: %{public}@", + log: log, + type: .error, + String(describing: error)) + throw error + } + + listener.newConnectionHandler = { [weak self] connection in + self?.handleNewConnection(connection, messageHandler: messageHandler) + } + + listener.stateUpdateHandler = { [weak self] state in + guard let self else { return } + + switch state { + case .ready: + os_log("UDSServer - Listener is ready", log: log, type: .info) + case .failed(let error): + os_log("UDSServer - Listener failed with error: %{public}@", log: log, type: .error, String(describing: error)) + stop() + case .cancelled: + os_log("UDSServer - Listener cancelled", log: log, type: .info) + default: + break + } + } + + listener.start(queue: listenerQueue) + } + + func stop() { + guard let listener else { + return + } + + listener.cancel() + listener.newConnectionHandler = nil + listener.stateUpdateHandler = nil + + Task { + await stopConnections() + } + } + + private func stopConnections() async { + await connections.forEach { connection in + connection.cancel() + } + + await connections.removeAll() + } + + private func handleNewConnection(_ connection: NWConnection, messageHandler: @escaping (Data) async throws -> Data?) { + Task { + os_log("UDSServer - New connection: %{public}@", + log: log, + type: .info, + String(describing: connection.hashValue)) + + connection.stateUpdateHandler = { [weak self] state in + guard let self else { return } + + switch state { + case .ready: + os_log("UDSServer - Client connection is ready", log: log, type: .info) + self.startReceivingMessages(on: connection, messageHandler: messageHandler) + case .failed(let error): + os_log("UDSServer - Client connection failed with error: %{public}@", log: log, type: .error, String(describing: error)) + self.closeConnection(connection) + case .cancelled: + os_log("UDSServer - Client connection cancelled", log: log, type: .info) + default: + break + } + } + + await connections.insert(connection) + connection.start(queue: connectionQueue) + } + } + + private func closeAllConnections() { + Task { + await connections.forEach { connection in + connection.cancel() + } + + await connections.removeAll() + } + } + + private func closeConnection(_ connection: NWConnection) { + Task { + await self.connections.remove(connection) + connection.cancel() + } + } + + // - MARK: Data reception logic + + private enum ReadError: Error { + case notEnoughData(expected: Int, received: Int) + case connectionError(_ error: Error) + case connectionClosed + } + + /// Starts receiveing messages for a specific connection + /// + /// - Parameters: + /// - connection: the connection to receive messages for. + /// + private func startReceivingMessages(on connection: NWConnection, messageHandler: @escaping (Data) async throws -> Data?) { + + receiver.startReceivingMessages(on: connection) { [weak self] message in + guard let self else { return false } + + switch message.body { + case .request(let data): + let responsePayload = try await messageHandler(data) + let responseMessage = message.successResponse(withPayload: responsePayload) + try await self.send(responseMessage, connection: connection) + case .response: + // We still don't fully support server to client messages. This is the location where we'd + // add the handling for that. + // + // This will be useful if we, for example, want to move VPN status observation to UDS. + // + break + } + + return true + } onError: { [weak self] _ in + guard let self else { return false } + self.closeConnection(connection) + return false + } + } + + private func send(_ message: UDSMessage, connection: NWConnection) async throws { + + let data = try JSONEncoder().encode(message) + let lengthData = withUnsafeBytes(of: UDSMessageLength(data.count)) { + Data($0) + } + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: lengthData + data, completion: .contentProcessed { error in + if let error { + os_log("UDSServer - Send Error %{public}@", log: self.log, String(describing: error)) + continuation.resume(throwing: error) + return + } + + os_log("UDSServer - Send Success", log: self.log) + continuation.resume() + }) + } + } +} diff --git a/LocalPackages/UDSHelper/Tests/UDSHelperTests/UDSMessageTests.swift b/LocalPackages/UDSHelper/Tests/UDSHelperTests/UDSMessageTests.swift new file mode 100644 index 00000000000..8f59f38fe55 --- /dev/null +++ b/LocalPackages/UDSHelper/Tests/UDSHelperTests/UDSMessageTests.swift @@ -0,0 +1,46 @@ +// +// UDSMessageTests.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 UDSHelper + +final class UDSMessageTests: XCTestCase { + func testExample() throws { + let uuid = UUID() + let requestData = "request".data(using: .utf8)! + let responseData = "response".data(using: .utf8)! + + let message = UDSMessage(uuid: uuid, body: .request(requestData)) + XCTAssertEqual(message.uuid, uuid) + + let response = message.successResponse(withPayload: responseData) + XCTAssertEqual(response.uuid, uuid) + + switch response.body { + case .request: + XCTFail("Expected a response body") + case .response(let result): + switch result { + case .success(let data): + XCTAssertEqual(data, responseData) + case .failure: + XCTFail("Expected a success response") + } + } + } +} From b360f96aa12f7ecdc9e1a8a565ad9e3b1196c7c3 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 12 Jun 2024 17:21:41 +0200 Subject: [PATCH 34/35] Fixes an issue with my last merge --- DuckDuckGo/Application/AppDelegate.swift | 2 +- .../BothAppTargets/VPNRedditSessionWorkaround.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 0413ae76484..fd69a8fc1a6 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -114,7 +114,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif private lazy var vpnRedditSessionWorkaround: VPNRedditSessionWorkaround = { - let ipcClient = TunnelControllerIPCClient() + let ipcClient = VPNControllerXPCClient.shared let statusReporter = DefaultNetworkProtectionStatusReporter( statusObserver: ipcClient.connectionStatusObserver, serverInfoObserver: ipcClient.serverInfoObserver, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift index a3f602f0b69..848f27c564c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift @@ -26,11 +26,11 @@ import Common final class VPNRedditSessionWorkaround { private let accountManager: AccountManaging - private let ipcClient: TunnelControllerIPCClient + private let ipcClient: VPNControllerXPCClient private let statusReporter: NetworkProtectionStatusReporter init(accountManager: AccountManaging, - ipcClient: TunnelControllerIPCClient, + ipcClient: VPNControllerXPCClient = .shared, statusReporter: NetworkProtectionStatusReporter) { self.accountManager = accountManager self.ipcClient = ipcClient From 75a5c9a2034e92b01e4ecadf083fe0cd506bafac Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Wed, 12 Jun 2024 17:33:45 +0200 Subject: [PATCH 35/35] Check DBP Prerequisites in App and Disable Login Item If Necessary (#2850) Task/Issue URL: https://app.asana.com/0/1206488453854252/1207501562611619/f **Description**: This PR introduces changes to check DBP prerequisites (user is authenticated and has valid entitlements) from the App when the app becomes active. When prerequisites are not satisfied, the app disables the login item. This is to prevent a scenario where the background agent checks prerequisites, exits due to failed prerequisites, and then is automatically re-started by the login item. --- DuckDuckGo.xcodeproj/project.pbxproj | 60 +++-- DuckDuckGo/Application/AppDelegate.swift | 19 +- .../Surveys/SurveyRemoteMessaging.swift | 1 - .../DBP/DataBrokerProtectionAppEvents.swift | 63 ++--- .../DBP/DataBrokerProtectionDebugMenu.swift | 37 --- .../DataBrokerProtectionFeatureDisabler.swift | 2 +- ...taBrokerProtectionFeatureGatekeeper.swift} | 104 +++++--- .../DataBrokerProtectionPixelsHandler.swift | 5 +- .../Model/HomePageContinueSetUpModel.swift | 40 +--- .../MainWindow/MainViewController.swift | 1 - DuckDuckGo/Menus/MainMenu.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 18 +- .../View/NavigationBarViewController.swift | 4 +- .../NetworkProtectionAppEvents.swift | 12 +- .../NetworkProtectionNavBarButtonModel.swift | 6 +- ...NetworkProtectionIPCTunnelController.swift | 8 +- .../Model/PreferencesSidebarModel.swift | 14 +- .../Model/VPNPreferencesModel.swift | 2 +- .../View/PreferencesViewController.swift | 2 +- ...ility.swift => VPNFeatureGatekeeper.swift} | 6 +- .../WaitlistThankYouPromptPresenter.swift | 2 +- DuckDuckGo/Waitlist/Waitlist.swift | 36 --- .../Pixels/DataBrokerProtectionPixels.swift | 18 +- .../DataBrokerProtectionVisibilityTests.swift | 149 ------------ .../DBP/Mocks/DataBrokerProtectionMocks.swift | 82 +++++++ ...okerPrerequisitesStatusVerifierTests.swift | 0 ...okerProtectionFeatureGatekeeperTests.swift | 223 ++++++++++++++++++ UnitTests/Menus/MoreOptionsMenuTests.swift | 8 +- 28 files changed, 519 insertions(+), 405 deletions(-) rename DuckDuckGo/DBP/{DataBrokerProtectionFeatureVisibility.swift => DataBrokerProtectionFeatureGatekeeper.swift} (72%) rename DuckDuckGo/Waitlist/{NetworkProtectionFeatureVisibility.swift => VPNFeatureGatekeeper.swift} (96%) delete mode 100644 UnitTests/DBP/DataBrokerProtectionVisibilityTests.swift create mode 100644 UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift rename UnitTests/DBP/{ => Tests}/DataBrokerPrerequisitesStatusVerifierTests.swift (100%) create mode 100644 UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ba2c8460991..61cbca3faf6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -161,7 +161,7 @@ 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */; }; 311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B262628E73E0A00FD181A /* TabShadowConfig.swift */; }; - 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */; }; + 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */; }; 31267C6A2B640C4B00FEF811 /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; 31267C6B2B640C5200FEF811 /* DataBrokerProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */; }; 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 312978892B64131200B67619 /* DataBrokerProtection */; }; @@ -170,7 +170,7 @@ 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 3158B14D2B0BF74D00AF130C /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; 3158B1532B0BF75700AF130C /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; }; - 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */; }; + 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */; }; 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */; }; 315A023F2B6421AE00BFA577 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 315A023E2B6421AE00BFA577 /* Networking */; }; @@ -192,8 +192,8 @@ 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317295D12AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift */; }; 3184AC6D288F29D800C35E4B /* BadgeNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6C288F29D800C35E4B /* BadgeNotificationAnimationModel.swift */; }; 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6E288F2A1100C35E4B /* CookieNotificationAnimationModel.swift */; }; - 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; - 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; + 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; + 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.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 */; }; @@ -1199,8 +1199,8 @@ 4B677442255DBEEA00025BD8 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.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 */; }; + 4B67854A2AA8DE75008A5004 /* VPNFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */; }; + 4B67854B2AA8DE76008A5004 /* VPNFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */; }; 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; 4B6B64852BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */ = {isa = PBXBuildFile; fileRef = 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */; }; @@ -2504,6 +2504,8 @@ C17CA7B32B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */; }; C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; + C1D8BE452C1739E70057E426 /* DataBrokerProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */; }; + C1D8BE462C1739EC0057E426 /* DataBrokerProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */; }; C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; @@ -2951,12 +2953,12 @@ 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPHomeViewController.swift; sourceTree = ""; }; 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 = ""; }; + 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeperTests.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 = ""; }; - 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureVisibility.swift; sourceTree = ""; }; + 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeper.swift; sourceTree = ""; }; 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 = ""; }; @@ -3425,8 +3427,8 @@ 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNControllerXPCClient+ConvenienceInitializers.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 = ""; }; + 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNFeatureGatekeeper.swift; sourceTree = ""; }; 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIPCResources.swift; sourceTree = ""; }; - 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; @@ -4093,6 +4095,7 @@ C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopoversTests.swift; sourceTree = ""; }; C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillPopoverPresenter.swift; sourceTree = ""; }; C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportShortcutsViewModel.swift; sourceTree = ""; }; + C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionMocks.swift; sourceTree = ""; }; C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPopoverPresenter.swift; sourceTree = ""; }; C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionPresenter.swift; sourceTree = ""; }; C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionExecutor.swift; sourceTree = ""; }; @@ -4643,7 +4646,7 @@ 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, - 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, + 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */, F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */, 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, @@ -4659,8 +4662,8 @@ 31A2FD152BAB419400D0E741 /* DBP */ = { isa = PBXGroup; children = ( - 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */, - 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, + C1D8BE432C1739BF0057E426 /* Tests */, + C1D8BE422C1739BA0057E426 /* Mocks */, ); path = DBP; sourceTree = ""; @@ -5512,7 +5515,7 @@ 4B9DB0062A983B23000927DB /* Waitlist */ = { isa = PBXGroup; children = ( - 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */, + 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */, 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */, 7BFF35722C10D75000F89673 /* IPCServiceLauncher.swift */, 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */, @@ -8202,6 +8205,23 @@ path = Mocks; sourceTree = ""; }; + C1D8BE422C1739BA0057E426 /* Mocks */ = { + isa = PBXGroup; + children = ( + C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + C1D8BE432C1739BF0057E426 /* Tests */ = { + isa = PBXGroup; + children = ( + 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */, + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; C1E961E62B879E2A001760E1 /* Mocks */ = { isa = PBXGroup; children = ( @@ -9855,7 +9875,7 @@ 3706FB4C293F65D500E42796 /* SharingMenu.swift in Sources */, 3706FB4D293F65D500E42796 /* GrammarFeaturesManager.swift in Sources */, 3706FB50293F65D500E42796 /* SafariFaviconsReader.swift in Sources */, - 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureVisibility.swift in Sources */, + 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */, 3706FB51293F65D500E42796 /* NSScreenExtension.swift in Sources */, EEC4A66A2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, 3706FB52293F65D500E42796 /* NSBezierPathExtension.swift in Sources */, @@ -9907,7 +9927,7 @@ 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, B69A14F32B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, - 4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, + 4B67854B2AA8DE76008A5004 /* VPNFeatureGatekeeper.swift in Sources */, C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, @@ -10513,6 +10533,7 @@ 3706FE33293F661700E42796 /* FireTests.swift in Sources */, B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */, + C1D8BE462C1739EC0057E426 /* DataBrokerProtectionMocks.swift in Sources */, 317295D32AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */, 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 3706FE34293F661700E42796 /* PermissionStoreTests.swift in Sources */, @@ -10540,7 +10561,7 @@ 3706FE44293F661700E42796 /* GeolocationServiceTests.swift in Sources */, 1DA6D1032A1FFA3B00540406 /* HTTPCookieTests.swift in Sources */, 3706FE45293F661700E42796 /* ProgressEstimationTests.swift in Sources */, - 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */, + 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */, B6619F072B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 3706FE46293F661700E42796 /* EncryptedValueTransformerTests.swift in Sources */, 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, @@ -11272,7 +11293,7 @@ 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, - 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, + 4B67854A2AA8DE75008A5004 /* VPNFeatureGatekeeper.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, 85D438B6256E7C9E00F3BAF8 /* ContextMenuUserScript.swift in Sources */, B693955526F04BEC0015B914 /* NSSavePanelExtension.swift in Sources */, @@ -11349,7 +11370,7 @@ 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, B6BCC53B2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, - 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, + 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */, 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, B6B5F5842B03580A008DB58A /* RequestFilePermissionView.swift in Sources */, 4B1E6EEE27AB5E5100F51793 /* PasswordManagementListSection.swift in Sources */, @@ -11791,7 +11812,7 @@ 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */, B6619EF62B10DFF700CD9186 /* InstructionsFormatParserTests.swift in Sources */, 569277C429DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */, - 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */, + 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */, 85F1B0C925EF9759004792B6 /* URLEventHandlerTests.swift in Sources */, 4B9292BD2667103100AD2C21 /* BookmarkOutlineViewDataSourceTests.swift in Sources */, C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */, @@ -11965,6 +11986,7 @@ BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */, B626A7642992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + C1D8BE452C1739E70057E426 /* DataBrokerProtectionMocks.swift in Sources */, AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, 859E7D6D274548F2009C2B69 /* BookmarksExporterTests.swift in Sources */, B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index fd69a8fc1a6..fe67b31e4c4 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -339,7 +339,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { networkProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() - NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidFinishLaunching() + NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidFinishLaunching() UNUserNotificationCenter.current().delegate = self #if DBP @@ -347,7 +347,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif #if DBP - DataBrokerProtectionAppEvents().applicationDidFinishLaunching() + DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: accountManager)).applicationDidFinishLaunching() #endif setUpAutoClearHandler() @@ -375,9 +375,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() - NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() + NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() #if DBP - DataBrokerProtectionAppEvents().applicationDidBecomeActive() + DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: accountManager)).applicationDidBecomeActive() #endif AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.toggleProtectionsCounter.sendEventsIfNeeded() @@ -677,17 +677,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - -#if DBP - if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { - DispatchQueue.main.async { - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) - } - } -#endif - } - completionHandler() } diff --git a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift index 51c14968849..93b0e2f5d83 100644 --- a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift +++ b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift @@ -73,7 +73,6 @@ final class DefaultSurveyRemoteMessaging: SurveyRemoteMessaging { subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching, vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), - networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), minimumRefreshInterval: TimeInterval, userDefaults: UserDefaults = .standard ) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index a86c213dc4f..4c98934cc4a 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -24,30 +24,41 @@ import Common import DataBrokerProtection struct DataBrokerProtectionAppEvents { - let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + + private let featureGatekeeper: DataBrokerProtectionFeatureGatekeeper + private let pixelHandler: EventMapping + private let loginItemsManager: LoginItemsManaging + private let loginItemInterface: DataBrokerProtectionLoginItemInterface enum WaitlistNotificationSource { case localPush case cardUI } - func applicationDidFinishLaunching() { - let loginItemsManager = LoginItemsManager() - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - let loginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface + init(featureGatekeeper: DataBrokerProtectionFeatureGatekeeper, + pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), + loginItemsManager: LoginItemsManaging = LoginItemsManager(), + loginItemInterface: DataBrokerProtectionLoginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface) { + self.featureGatekeeper = featureGatekeeper + self.pixelHandler = pixelHandler + self.loginItemsManager = loginItemsManager + self.loginItemInterface = loginItemInterface + } - guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + func applicationDidFinishLaunching() { + guard !featureGatekeeper.cleanUpDBPForPrivacyProIfNecessary() else { return } /// If the user is not in the waitlist and Privacy Pro flag is false, we want to remove the data for waitlist users /// since the waitlist flag might have been turned off - if !featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() { - featureVisibility.disableAndDeleteForWaitlistUsers() + if !featureGatekeeper.isFeatureVisible() && !featureGatekeeper.isPrivacyProEnabled() { + featureGatekeeper.disableAndDeleteForWaitlistUsers() return } - Task { - try? await DataBrokerProtectionWaitlist().redeemDataBrokerProtectionInviteCodeIfAvailable() + let loginItemsManager = LoginItemsManager() + let loginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface + Task { // 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 profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), @@ -58,27 +69,31 @@ struct DataBrokerProtectionAppEvents { try await Task.sleep(nanoseconds: 1_000_000_000) loginItemInterface.appLaunched() } else { - featureVisibility.disableAndDeleteForWaitlistUsers() + featureGatekeeper.disableAndDeleteForWaitlistUsers() } } } func applicationDidBecomeActive() { - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + // Check feature prerequisites and disable the login item if they are not satisfied + Task { @MainActor in + let prerequisitesMet = await featureGatekeeper.arePrerequisitesSatisfied() + guard prerequisitesMet else { + loginItemsManager.disableLoginItems([LoginItem.dbpBackgroundAgent]) + return + } + } + + guard !featureGatekeeper.cleanUpDBPForPrivacyProIfNecessary() else { return } /// If the user is not in the waitlist and Privacy Pro flag is false, we want to remove the data for waitlist users /// since the waitlist flag might have been turned off - if !featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() { - featureVisibility.disableAndDeleteForWaitlistUsers() + if !featureGatekeeper.isFeatureVisible() && !featureGatekeeper.isPrivacyProEnabled() { + featureGatekeeper.disableAndDeleteForWaitlistUsers() return } - - Task { - try? await DataBrokerProtectionWaitlist().redeemDataBrokerProtectionInviteCodeIfAvailable() - } } @MainActor @@ -95,16 +110,6 @@ struct DataBrokerProtectionAppEvents { } } - func windowDidBecomeMain() { - sendActiveDataBrokerProtectionWaitlistUserPixel() - } - - private func sendActiveDataBrokerProtectionWaitlistUserPixel() { - if DefaultDataBrokerProtectionFeatureVisibility().waitlistIsOngoing { - DataBrokerProtectionExternalWaitlistPixels.fire(pixel: GeneralPixel.dataBrokerProtectionWaitlistUserActive, frequency: .daily) - } - } - private func restartBackgroundAgent(loginItemsManager: LoginItemsManager) { DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerResetLoginItemDaily, frequency: .daily) loginItemsManager.disableLoginItems([LoginItem.dbpBackgroundAgent]) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 39eb3062a33..8abf7cf4436 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -37,7 +37,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { private let waitlistTimestampItem = NSMenuItem(title: "Waitlist Timestamp:") private let waitlistInviteCodeItem = NSMenuItem(title: "Waitlist Invite Code:") private let waitlistTermsAndConditionsAcceptedItem = NSMenuItem(title: "T&C Accepted:") - private let waitlistBypassItem = NSMenuItem(title: "Bypass Waitlist", action: #selector(DataBrokerProtectionDebugMenu.toggleBypassWaitlist)) private let productionURLMenuItem = NSMenuItem(title: "Use Production URL", action: #selector(DataBrokerProtectionDebugMenu.useWebUIProductionURL)) @@ -67,14 +66,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Send Notification", action: #selector(DataBrokerProtectionDebugMenu.sendWaitlistAvailableNotification)) .targetting(self) - NSMenuItem(title: "Fetch Invite Code", action: #selector(DataBrokerProtectionDebugMenu.fetchInviteCode)) - .targetting(self) - NSMenuItem.separator() - waitlistBypassItem - .targetting(self) - NSMenuItem.separator() waitlistTokenItem @@ -172,7 +165,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { // MARK: - Menu State Update override func update() { - updateWaitlistItems() updateWebUIMenuItemsState() updateEnvironmentMenu() updateShowStatusMenuIconMenu() @@ -311,10 +303,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("DBP waitlist state cleaned", log: .dataBrokerProtection) } - @objc private func toggleBypassWaitlist() { - DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist.toggle() - } - @objc private func resetTermsAndConditionsAcceptance() { UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.dataBrokerProtectionTermsAndConditionsAccepted.rawValue) NotificationCenter.default.post(name: .dataBrokerProtectionWaitlistAccessChanged, object: nil) @@ -327,14 +315,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("DBP waitlist notification sent", log: .dataBrokerProtection) } - @objc private func fetchInviteCode() { - os_log("Fetching invite code...", log: .dataBrokerProtection) - - Task { - try? await DataBrokerProtectionWaitlist().redeemDataBrokerProtectionInviteCodeIfAvailable() - } - } - @objc private func toggleShowStatusMenuItem() { settings.showInMenuBar.toggle() } @@ -386,23 +366,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { return menuItem } - private func updateWaitlistItems() { - let waitlistStorage = WaitlistKeychainStore(waitlistIdentifier: DataBrokerProtectionWaitlist.identifier, keychainAppGroup: Bundle.main.appGroup(bundle: .dbp)) - 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.dataBrokerProtectionTermsAndConditionsAccepted.rawValue) - waitlistTermsAndConditionsAcceptedItem.title = "T&C Accepted: \(accepted ? "Yes" : "No")" - - waitlistBypassItem.state = DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist ? .on : .off - } - private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment guard environmentMenu.items.count == 3 else { return } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index 18a1c89b863..1e81a281e8e 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -41,7 +41,7 @@ struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling } func disableAndDelete() { - if !DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist { + if !DefaultDataBrokerProtectionFeatureGatekeeper.bypassWaitlist { do { try dataManager.removeAllData() diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift similarity index 72% rename from DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift rename to DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift index 051b2e4e717..c2c4d65768f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionFeatureVisibility.swift +// DataBrokerProtectionFeatureGatekeeper.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -24,31 +24,27 @@ import Common import DataBrokerProtection import Subscription -protocol DataBrokerProtectionFeatureVisibility { +protocol DataBrokerProtectionFeatureGatekeeper { + var waitlistIsOngoing: Bool { get } func isFeatureVisible() -> Bool func disableAndDeleteForAllUsers() func disableAndDeleteForWaitlistUsers() func isPrivacyProEnabled() -> Bool func isEligibleForThankYouMessage() -> Bool + func cleanUpDBPForPrivacyProIfNecessary() -> Bool + func arePrerequisitesSatisfied() async -> Bool } -struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeatureVisibility { +struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeatureGatekeeper { private let privacyConfigurationManager: PrivacyConfigurationManaging private let featureDisabler: DataBrokerProtectionFeatureDisabling private let pixelHandler: EventMapping private let userDefaults: UserDefaults private let waitlistStorage: WaitlistStorage private let subscriptionAvailability: SubscriptionFeatureAvailability + private let accountManager: AccountManaging private let dataBrokerProtectionKey = "data-broker-protection.cleaned-up-from-waitlist-to-privacy-pro" - private var dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro: Bool { - get { - return userDefaults.bool(forKey: dataBrokerProtectionKey) - } - nonmutating set { - userDefaults.set(newValue, forKey: dataBrokerProtectionKey) - } - } /// Temporary code to use while we have both redeem flow for diary study users. Should be removed later static var bypassWaitlist = false @@ -58,13 +54,15 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), userDefaults: UserDefaults = .standard, waitlistStorage: WaitlistStorage = DataBrokerProtectionWaitlist().waitlistStorage, - subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability()) { + subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(), + accountManager: AccountManaging) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler self.userDefaults = userDefaults self.waitlistStorage = waitlistStorage self.subscriptionAvailability = subscriptionAvailability + self.accountManager = accountManager } var waitlistIsOngoing: Bool { @@ -89,26 +87,6 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature return (regionCode ?? "US") == "US" } - private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - - private var isWaitlistBetaActive: Bool { - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlistBetaActive) - } - - private var isWaitlistEnabled: Bool { - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlist) - } - - private var isWaitlistUser: Bool { - waitlistStorage.isWaitlistUser - } - - private var wasWaitlistUser: Bool { - waitlistStorage.getWaitlistInviteCode() != nil - } - func isPrivacyProEnabled() -> Bool { return subscriptionAvailability.isFeatureAvailable } @@ -159,5 +137,67 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature return isWaitlistEnabled && isWaitlistBetaActive } } + + func arePrerequisitesSatisfied() async -> Bool { + let entitlements = await accountManager.hasEntitlement(for: .dataBrokerProtection, + cachePolicy: .reloadIgnoringLocalCacheData) + var hasEntitlements: Bool + switch entitlements { + case .success(let value): + hasEntitlements = value + case .failure: + hasEntitlements = false + } + + let isAuthenticated = accountManager.accessToken != nil + + firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: hasEntitlements, isAuthenticatedResult: isAuthenticated) + + return hasEntitlements && isAuthenticated + } +} + +private extension DefaultDataBrokerProtectionFeatureGatekeeper { + + var dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro: Bool { + get { + return userDefaults.bool(forKey: dataBrokerProtectionKey) + } + nonmutating set { + userDefaults.set(newValue, forKey: dataBrokerProtectionKey) + } + } + + var isInternalUser: Bool { + NSApp.delegateTyped.internalUserDecider.isInternalUser + } + + var isWaitlistBetaActive: Bool { + return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlistBetaActive) + } + + var isWaitlistEnabled: Bool { + return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlist) + } + + var isWaitlistUser: Bool { + waitlistStorage.isWaitlistUser + } + + var wasWaitlistUser: Bool { + waitlistStorage.getWaitlistInviteCode() != nil + } + + func firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: Bool, isAuthenticatedResult: Bool) { + if !hasEntitlements { + pixelHandler.fire(.gatekeeperEntitlementsInvalid) + os_log("🔴 DBP feature Gatekeeper: Entitlement check failed", log: .dataBrokerProtection) + } + + if !isAuthenticatedResult { + pixelHandler.fire(.gatekeeperNotAuthenticated) + os_log("🔴 DBP feature Gatekeeper: Authentication check failed", log: .dataBrokerProtection) + } + } } #endif diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index a073fa9aa53..5c257d71d4f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -97,7 +97,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping { @@ -44,12 +44,12 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent], privacyConfigurationManager: PrivacyConfigurationManaging, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + vpnGatekeeper: VPNFeatureGatekeeper = DefaultVPNFeatureGatekeeper(subscriptionManager: Application.appDelegate.subscriptionManager), vpnTunnelIPCClient: VPNControllerXPCClient = .shared ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs - self.vpnVisibility = vpnVisibility + self.vpnGatekeeper = vpnGatekeeper self.vpnTunnelIPCClient = vpnTunnelIPCClient resetTabSelectionIfNeeded() @@ -81,12 +81,12 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility, + vpnGatekeeper: VPNFeatureGatekeeper, includeDuckPlayer: Bool, userDefaults: UserDefaults = .netP ) { let loadSections = { - let includingVPN = vpnVisibility.isInstalled + let includingVPN = vpnGatekeeper.isInstalled return PreferencesSection.defaultSections( includingDuckPlayer: includeDuckPlayer, @@ -99,13 +99,13 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: privacyConfigurationManager, syncService: syncService, - vpnVisibility: vpnVisibility) + vpnGatekeeper: vpnGatekeeper) } // MARK: - Setup private func setupVPNPaneVisibility() { - vpnVisibility.onboardStatusPublisher + vpnGatekeeper.onboardStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 4d14f35440c..22d95b80513 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -69,7 +69,7 @@ final class VPNPreferencesModel: ObservableObject { private var onboardingStatus: OnboardingStatus { didSet { - showUninstallVPN = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager).isInstalled + showUninstallVPN = DefaultVPNFeatureGatekeeper(subscriptionManager: Application.appDelegate.subscriptionManager).isInstalled } } diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 967bc49768e..7520ff043ae 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -35,7 +35,7 @@ final class PreferencesViewController: NSViewController { init(syncService: DDGSyncing, duckPlayer: DuckPlayer = DuckPlayer.shared) { model = PreferencesSidebarModel(syncService: syncService, - vpnVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + vpnGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: Application.appDelegate.subscriptionManager), includeDuckPlayer: duckPlayer.isAvailable) super.init(nibName: nil, bundle: nil) } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift similarity index 96% rename from DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift rename to DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift index e88a74b3406..1771c828ae1 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionFeatureVisibility.swift +// VPNFeatureGatekeeper.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -26,7 +26,7 @@ import LoginItems import PixelKit import Subscription -protocol NetworkProtectionFeatureVisibility { +protocol VPNFeatureGatekeeper { var isInstalled: Bool { get } func canStartVPN() async throws -> Bool @@ -37,7 +37,7 @@ protocol NetworkProtectionFeatureVisibility { var onboardStatusPublisher: AnyPublisher { get } } -struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { +struct DefaultVPNFeatureGatekeeper: VPNFeatureGatekeeper { private static var subscriptionAuthTokenPrefix: String { "ddg:" } private let vpnUninstaller: VPNUninstalling private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 75af13e0a90..fe79b7cc96a 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -33,7 +33,7 @@ final class WaitlistThankYouPromptPresenter { convenience init() { self.init(isPIRBetaTester: { - return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() + false }) } diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index b8bc8e1ba71..4ed31b5b082 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -199,42 +199,6 @@ struct DataBrokerProtectionWaitlist: Waitlist { self.redeemAuthenticationRepository = redeemAuthenticationRepository } - func redeemDataBrokerProtectionInviteCodeIfAvailable() async throws { - if DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist || DefaultDataBrokerProtectionFeatureVisibility().isPrivacyProEnabled() { - return - } - - do { - guard waitlistStorage.getWaitlistToken() != nil else { - os_log("User not in DBP waitlist, returning...", log: .default) - return - } - - guard redeemAuthenticationRepository.getAccessToken() == nil else { - os_log("Invite code already redeemed, returning...", log: .default) - return - } - - var inviteCode = waitlistStorage.getWaitlistInviteCode() - - if inviteCode == nil { - os_log("No DBP invite code found, fetching...", log: .default) - inviteCode = try await fetchInviteCode() - } - - if let code = inviteCode { - try await redeemInviteCode(code) - } else { - os_log("No DBP invite code available") - throw WaitlistInviteCodeFetchError.noCodeAvailable - } - - } catch { - os_log("DBP Invite code error: %{public}@", log: .error, error.localizedDescription) - throw error - } - } - private func fetchInviteCode() async throws -> String { // First check if we have it stored locally diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 7d2f3fcd300..27693288cd3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -178,12 +178,17 @@ public enum DataBrokerProtectionPixels { case entitlementCheckValid case entitlementCheckInvalid case entitlementCheckError + // Measure success/failure rate of Personal Information Removal Pixels // https://app.asana.com/0/1204006570077678/1206889724879222/f case globalMetricsWeeklyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) case globalMetricsMonthlyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) case dataBrokerMetricsWeeklyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int) case dataBrokerMetricsMonthlyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int) + + // Feature Gatekeeper + case gatekeeperNotAuthenticated + case gatekeeperEntitlementsInvalid } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -294,10 +299,15 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .entitlementCheckValid: return "m_mac_dbp_macos_entitlement_valid" case .entitlementCheckInvalid: return "m_mac_dbp_macos_entitlement_invalid" case .entitlementCheckError: return "m_mac_dbp_macos_entitlement_error" + case .globalMetricsWeeklyStats: return "m_mac_dbp_weekly_stats" case .globalMetricsMonthlyStats: return "m_mac_dbp_monthly_stats" case .dataBrokerMetricsWeeklyStats: return "m_mac_dbp_databroker_weekly_stats" case .dataBrokerMetricsMonthlyStats: return "m_mac_dbp_databroker_monthly_stats" + + // Feature Gatekeeper + case .gatekeeperNotAuthenticated: return "m_mac_dbp_gatekeeper_not_authenticated" + case .gatekeeperEntitlementsInvalid: return "m_mac_dbp_gatekeeper_entitlements_invalid" } } @@ -398,7 +408,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .secureVaultInitError, .secureVaultKeyStoreReadError, .secureVaultKeyStoreUpdateError, - .secureVaultError: + .secureVaultError, + .gatekeeperNotAuthenticated, + .gatekeeperEntitlementsInvalid: return [:] case .ipcServerProfileSavedCalledByApp, .ipcServerProfileSavedReceivedByAgent, @@ -537,7 +549,9 @@ public class DataBrokerProtectionPixelsHandler: EventMapping UserDefaults { - UserDefaults(suiteName: "testing_\(UUID().uuidString)")! - } - - override func setUpWithError() throws { - mockFeatureDisabler = MockFeatureDisabler() - mockFeatureAvailability = MockFeatureAvailability() - waitlistStorage = MockWaitlistStorage() - } - - override func tearDownWithError() throws { - mockFeatureDisabler.reset() - mockFeatureAvailability.reset() - waitlistStorage.deleteWaitlistState() - } - - /// Waitlist is OFF, Not redeemed - /// PP flag is OF - func testWhenWaitlistHasNoInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is OFF, Not redeemed - /// PP flag is ON - func testWhenWaitlistHasNoInviteCodeAndFeatureEnabled_thenCleanUpIsNotCalled() throws { - mockFeatureAvailability.mockFeatureAvailable = true - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is ON, redeemed - /// PP flag is OFF - func testWhenWaitlistHasInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { - waitlistStorage.store(waitlistToken: "potato") - waitlistStorage.store(inviteCode: "banana") - waitlistStorage.store(waitlistTimestamp: 123) - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is ON, redeemed - /// PP flag is ON - func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalled() throws { - waitlistStorage.store(waitlistToken: "potato") - waitlistStorage.store(inviteCode: "banana") - waitlistStorage.store(waitlistTimestamp: 123) - mockFeatureAvailability.mockFeatureAvailable = true - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertTrue(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is ON, redeemed - /// PP flag is ON - func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalledTwice() throws { - waitlistStorage.store(waitlistToken: "potato") - waitlistStorage.store(inviteCode: "banana") - waitlistStorage.store(waitlistTimestamp: 123) - mockFeatureAvailability.mockFeatureAvailable = true - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertTrue(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - } -} - -private class MockFeatureDisabler: DataBrokerProtectionFeatureDisabling { - var disableAndDeleteWasCalled = false - - func disableAndDelete() { - disableAndDeleteWasCalled = true - } - - func reset() { - disableAndDeleteWasCalled = false - } -} - -private class MockFeatureAvailability: SubscriptionFeatureAvailability { - var mockFeatureAvailable: Bool = false - var mockSubscriptionPurchaseAllowed: Bool = false - - var isFeatureAvailable: Bool { mockFeatureAvailable } - var isSubscriptionPurchaseAllowed: Bool { mockSubscriptionPurchaseAllowed } - - func reset() { - mockFeatureAvailable = false - mockSubscriptionPurchaseAllowed = false - } -} diff --git a/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift b/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift new file mode 100644 index 00000000000..43ef8327f4d --- /dev/null +++ b/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift @@ -0,0 +1,82 @@ +// +// DataBrokerProtectionMocks.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 +@testable import DuckDuckGo_Privacy_Browser + +final class MockAccountManager: AccountManaging { + var hasEntitlementResult: Result = .success(true) + + var delegate: AccountManagerKeychainAccessDelegate? + + var isUserAuthenticated = false + + var accessToken: String? = "" + + var authToken: String? + + var email: String? + + var externalID: String? + + func storeAuthToken(token: String) { + } + + func storeAccount(token: String, email: String?, externalID: String?) { + } + + func signOut(skipNotification: Bool) { + } + + func signOut() { + } + + func migrateAccessTokenToNewStore() throws { + } + + func hasEntitlement(for entitlement: Entitlement.ProductName, cachePolicy: CachePolicy) async -> Result { + hasEntitlementResult + } + + func hasEntitlement(for entitlement: Entitlement.ProductName) async -> Result { + hasEntitlementResult + } + + func updateCache(with entitlements: [Entitlement]) { + } + + func fetchEntitlements(cachePolicy: CachePolicy) async -> Result<[Entitlement], any Error> { + .success([]) + } + + func exchangeAuthTokenToAccessToken(_ authToken: String) async -> Result { + .success("") + } + + func fetchAccountDetails(with accessToken: String) async -> Result { + .success(AccountDetails(email: "", externalID: "")) + } + + func refreshSubscriptionAndEntitlements() async { + } + + func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { + true + } +} diff --git a/UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift b/UnitTests/DBP/Tests/DataBrokerPrerequisitesStatusVerifierTests.swift similarity index 100% rename from UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift rename to UnitTests/DBP/Tests/DataBrokerPrerequisitesStatusVerifierTests.swift diff --git a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift new file mode 100644 index 00000000000..7f70a0e8055 --- /dev/null +++ b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift @@ -0,0 +1,223 @@ +// +// DataBrokerProtectionFeatureGatekeeperTests.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 Subscription + +@testable import DuckDuckGo_Privacy_Browser + +final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { + + private var sut: DefaultDataBrokerProtectionFeatureGatekeeper! + private var mockFeatureDisabler: MockFeatureDisabler! + private var mockFeatureAvailability: MockFeatureAvailability! + private var waitlistStorage: MockWaitlistStorage! + private var mockAccountManager: MockAccountManager! + + private func userDefaults() -> UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + override func setUpWithError() throws { + mockFeatureDisabler = MockFeatureDisabler() + mockFeatureAvailability = MockFeatureAvailability() + waitlistStorage = MockWaitlistStorage() + mockAccountManager = MockAccountManager() + } + + /// Waitlist is OFF, Not redeemed + /// PP flag is OF + func testWhenWaitlistHasNoInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is OFF, Not redeemed + /// PP flag is ON + func testWhenWaitlistHasNoInviteCodeAndFeatureEnabled_thenCleanUpIsNotCalled() throws { + mockFeatureAvailability.mockFeatureAvailable = true + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is ON, redeemed + /// PP flag is OFF + func testWhenWaitlistHasInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { + waitlistStorage.store(waitlistToken: "potato") + waitlistStorage.store(inviteCode: "banana") + waitlistStorage.store(waitlistTimestamp: 123) + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is ON, redeemed + /// PP flag is ON + func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalled() throws { + waitlistStorage.store(waitlistToken: "potato") + waitlistStorage.store(inviteCode: "banana") + waitlistStorage.store(waitlistTimestamp: 123) + mockFeatureAvailability.mockFeatureAvailable = true + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertTrue(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is ON, redeemed + /// PP flag is ON + func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalledTwice() throws { + waitlistStorage.store(waitlistToken: "potato") + waitlistStorage.store(inviteCode: "banana") + waitlistStorage.store(waitlistTimestamp: 123) + mockFeatureAvailability.mockFeatureAvailable = true + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertTrue(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + } + + func testWhenNoAccessTokenIsFound_butEntitlementIs_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = nil + mockAccountManager.hasEntitlementResult = .success(true) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertFalse(result) + } + + func testWhenAccessTokenIsFound_butNoEntitlementIs_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = "token" + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertFalse(result) + } + + func testWhenAccessTokenAndEntitlementAreNotFound_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = nil + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertFalse(result) + } + + func testWhenAccessTokenAndEntitlementAreFound_thenFeatureIsEnabled() async { + // Given + mockAccountManager.accessToken = "token" + mockAccountManager.hasEntitlementResult = .success(true) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertTrue(result) + } +} + +private enum MockError: Error { + case someError +} + +private class MockFeatureDisabler: DataBrokerProtectionFeatureDisabling { + var disableAndDeleteWasCalled = false + + func disableAndDelete() { + disableAndDeleteWasCalled = true + } + + func reset() { + disableAndDeleteWasCalled = false + } +} + +private class MockFeatureAvailability: SubscriptionFeatureAvailability { + var mockFeatureAvailable: Bool = false + var mockSubscriptionPurchaseAllowed: Bool = false + + var isFeatureAvailable: Bool { mockFeatureAvailable } + var isSubscriptionPurchaseAllowed: Bool { mockSubscriptionPurchaseAllowed } + + func reset() { + mockFeatureAvailable = false + mockSubscriptionPurchaseAllowed = false + } +} diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 0140a43a9f3..a0b8b38f640 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -47,7 +47,7 @@ final class MoreOptionsMenuTests: XCTestCase { networkProtectionVisibilityMock = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: networkProtectionVisibilityMock, + vpnFeatureGatekeeper: networkProtectionVisibilityMock, sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, accountManager: accountManager) @@ -68,7 +68,7 @@ final class MoreOptionsMenuTests: XCTestCase { func testThatMoreOptionMenuHasTheExpectedItemsAuthenticated() { moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), + vpnFeatureGatekeeper: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, accountManager: accountManager) @@ -100,7 +100,7 @@ final class MoreOptionsMenuTests: XCTestCase { accountManager = AccountManagerMock(isUserAuthenticated: false) moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), + vpnFeatureGatekeeper: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, accountManager: accountManager) @@ -164,7 +164,7 @@ final class MoreOptionsMenuTests: XCTestCase { } -final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { +final class NetworkProtectionVisibilityMock: VPNFeatureGatekeeper { var onboardStatusPublisher: AnyPublisher { Just(.default).eraseToAnyPublisher()