From 86144596b016ccd4c1c13506525fc4e9ccde6692 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Wed, 8 May 2024 16:59:57 +0100 Subject: [PATCH 01/42] PIR Stabilization: Extract Operation Building from Processor (#2743) Task/Issue URL:https://app.asana.com/0/1199230911884351/1207175016813446/f **Description**:This PR includes all changes relating to [this tech design sub-task](https://app.asana.com/0/0/1207199754528648/f). Specifically: --- .../DataBrokerProtectionDatabase.swift | 20 +- .../DataBrokerDatabaseBrowserViewModel.swift | 8 +- .../DataBrokerRunCustomJSONViewModel.swift | 22 +- ...ScanOperation.swift => DebugScanJob.swift} | 4 +- .../DataBrokerForceOptOutViewModel.swift | 14 +- ...perationData.swift => BrokerJobData.swift} | 8 +- .../Model/BrokerProfileQueryData.swift | 20 +- .../Operations/DataBrokerJob.swift | 311 ++++++++++++++ ...Runner.swift => DataBrokerJobRunner.swift} | 12 +- ...wift => DataBrokerJobRunnerProvider.swift} | 12 +- .../Operations/DataBrokerOperation.swift | 403 +++++++----------- .../DataBrokerOperationsCollection.swift | 220 ---------- ...taBrokerProfileQueryOperationManager.swift | 26 +- .../OperationPreferredDateUpdater.swift | 50 +-- ...{OptOutOperation.swift => OptOutJob.swift} | 4 +- .../MismatchCalculatorUseCase.swift | 4 +- .../{ScanOperation.swift => ScanJob.swift} | 4 +- .../DataBrokerProtectionEventPixels.swift | 4 +- .../DataBrokerOperationsCreator.swift | 56 +++ .../DataBrokerProtectionProcessor.swift | 89 +--- .../DataBrokerProtectionScheduler.swift | 4 +- .../DataBrokerProtectionSecureVault.swift | 22 +- .../Storage/Mappers.swift | 4 +- .../DataBrokerProtection/UI/UIMapper.swift | 44 +- .../DataBrokerOperationActionTests.swift | 38 +- .../DataBrokerOperationsCreatorTests.swift | 81 ++++ ...kerProfileQueryOperationManagerTests.swift | 170 ++++---- ...DataBrokerProtectionEventPixelsTests.swift | 38 +- .../DataBrokerProtectionProfileTests.swift | 6 +- .../MismatchCalculatorUseCaseTests.swift | 4 +- .../DataBrokerProtectionTests/Mocks.swift | 104 ++++- 31 files changed, 972 insertions(+), 834 deletions(-) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/{DebugScanOperation.swift => DebugScanJob.swift} (98%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/{BrokerOperationData.swift => BrokerJobData.swift} (93%) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{DataBrokerOperationRunner.swift => DataBrokerJobRunner.swift} (95%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{DataBrokerOperationRunnerProvider.swift => DataBrokerJobRunnerProvider.swift} (80%) delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{OptOutOperation.swift => OptOutJob.swift} (98%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{ScanOperation.swift => ScanJob.swift} (98%) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index c1cbafd4a8..8b293965af 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -27,7 +27,7 @@ protocol DataBrokerProtectionRepository { func fetchChildBrokers(for parentBroker: String) throws -> [DataBroker] - func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws + func saveOptOutJob(optOut: OptOutJobData, extractedProfile: ExtractedProfile) throws func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) throws -> BrokerProfileQueryData? func fetchAllBrokerProfileQueryData() throws -> [BrokerProfileQueryData] @@ -133,20 +133,20 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let broker = try vault.fetchBroker(with: brokerId), let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), - let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { + let scanJob = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { let error = DataBrokerProtectionError.dataNotInDatabase os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.brokerProfileQueryData for brokerId and profileQueryId")) throw error } - let optOutOperations = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) + let optOutJobs = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) return BrokerProfileQueryData( dataBroker: broker, profileQuery: profileQuery, - scanOperationData: scanOperation, - optOutOperationsData: optOutOperations + scanJobData: scanJob, + optOutJobData: optOutJobs ) } catch { os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) @@ -258,14 +258,14 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { for broker in brokers { for profileQuery in profileQueries { if let brokerId = broker.id, let profileQueryId = profileQuery.id { - guard let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { continue } - let optOutOperations = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) + guard let scanJob = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { continue } + let optOutJobs = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) let brokerProfileQueryData = BrokerProfileQueryData( dataBroker: broker, profileQuery: profileQuery, - scanOperationData: scanOperation, - optOutOperationsData: optOutOperations + scanJobData: scanJob, + optOutJobData: optOutJobs ) brokerProfileQueryDataList.append(brokerProfileQueryData) @@ -281,7 +281,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { } } - func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { + func saveOptOutJob(optOut: OptOutJobData, extractedProfile: ExtractedProfile) throws { do { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift index a3bf0d1547..36eaf1b8ac 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift @@ -57,15 +57,15 @@ final class DataBrokerDatabaseBrowserViewModel: ObservableObject { let dataBrokers = Array(Set(profileBrokers)).sorted { $0.id ?? 0 < $1.id ?? 0 } let profileQuery = Array(Set(data.map { $0.profileQuery })) - let scanOperations = data.map { $0.scanOperationData } - let optOutOperations = data.flatMap { $0.optOutOperationsData } + let scanJobs = data.map { $0.scanJobData } + let optOutJobs = data.flatMap { $0.optOutJobData } let extractedProfiles = data.flatMap { $0.extractedProfiles } let events = data.flatMap { $0.events } let brokersTable = createTable(using: dataBrokers, tableName: "DataBrokers") let profileQueriesTable = createTable(using: profileQuery, tableName: "ProfileQuery") - let scansTable = createTable(using: scanOperations, tableName: "ScanOperation") - let optOutsTable = createTable(using: optOutOperations, tableName: "OptOutOperation") + let scansTable = createTable(using: scanJobs, tableName: "ScanOperation") + let optOutsTable = createTable(using: optOutJobs, tableName: "OptOutOperation") let extractedProfilesTable = createTable(using: extractedProfiles, tableName: "ExtractedProfile") let eventsTable = createTable(using: events.sorted(by: { $0.date < $1.date }), tableName: "Events") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 6b5894d5e1..4aa99bcb0f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -143,7 +143,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { let brokers: [DataBroker] - private let runnerProvider: OperationRunnerProvider + private let runnerProvider: JobRunnerProvider private let privacyConfigManager: PrivacyConfigurationManaging private let fakePixelHandler: EventMapping = EventMapping { event, _, _, _ in print(event) @@ -168,7 +168,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { sessionKey: sessionKey, featureToggles: features) - self.runnerProvider = DataBrokerOperationRunnerProvider( + self.runnerProvider = DataBrokerJobRunnerProvider( privacyConfigManager: privacyConfigurationManager, contentScopeProperties: contentScopeProperties, emailService: EmailService(), @@ -190,13 +190,13 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { try await withThrowingTaskGroup(of: DebugScanReturnValue.self) { group in for queryData in brokerProfileQueryData { - let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, prefs: self.contentScopeProperties, query: queryData) { + let debugScanJob = DebugScanJob(privacyConfig: self.privacyConfigManager, prefs: self.contentScopeProperties, query: queryData) { true } group.addTask { do { - return try await debugScanOperation.run(inputValue: (), showWebView: false) + return try await debugScanJob.run(inputValue: (), showWebView: false) } catch { return DebugScanReturnValue(brokerURL: "ERROR - with broker: \(queryData.dataBroker.name)", extractedProfiles: [ExtractedProfile](), brokerProfileQueryData: queryData) } @@ -342,7 +342,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { let dataBroker = try decoder.decode(DataBroker.self, from: data) self.selectedDataBroker = dataBroker let brokerProfileQueryData = createBrokerProfileQueryData(for: dataBroker) - let runner = runnerProvider.getOperationRunner() + let runner = runnerProvider.getJobRunner() let group = DispatchGroup() for query in brokerProfileQueryData { @@ -378,11 +378,11 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } func runOptOut(scanResult: ScanResult) { - let runner = runnerProvider.getOperationRunner() + let runner = runnerProvider.getJobRunner() let brokerProfileQueryData = BrokerProfileQueryData( dataBroker: scanResult.dataBroker, profileQuery: scanResult.profileQuery, - scanOperationData: ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent]()) + scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent]()) ) Task { do { @@ -414,9 +414,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { var profileQueryIndex: Int64 = 1 for profileQuery in profileQueries { - let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) + let fakeScanJobData = ScanJobData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) brokerProfileQueryData.append( - .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData) + .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanJobData: fakeScanJobData) ) profileQueryIndex += 1 @@ -438,10 +438,10 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { var profileQueryIndex: Int64 = 1 for profileQuery in profileQueries { - let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) + let fakeScanJobData = ScanJobData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) for broker in brokers { brokerProfileQueryData.append( - .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData) + .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanJobData: fakeScanJobData) ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift similarity index 98% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift index 3d4e4b779c..de2ed0b350 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift @@ -1,5 +1,5 @@ // -// DebugScanOperation.swift +// DebugScanJob.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -48,7 +48,7 @@ struct EmptyCookieHandler: CookieHandler { } } -final class DebugScanOperation: DataBrokerOperation { +final class DebugScanJob: DataBrokerJob { typealias ReturnValue = DebugScanReturnValue typealias InputValue = Void diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift index 322af7070d..25219b64e8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift @@ -37,13 +37,13 @@ final class DataBrokerForceOptOutViewModel: ObservableObject { } self.optOutData = brokerProfileData .flatMap { profileData in - profileData.optOutOperationsData.map { ($0, profileData.dataBroker.name) } + profileData.optOutJobData.map { ($0, profileData.dataBroker.name) } } .filter { operationData, _ in operationData.extractedProfile.removedDate == nil } .map { operationData, brokerName in - OptOutViewData(optOutOperationData: operationData, brokerName: brokerName) + OptOutViewData(optOutJobData: operationData, brokerName: brokerName) }.sorted(by: { $0.brokerName < $1.brokerName }) } } @@ -57,16 +57,16 @@ final class DataBrokerForceOptOutViewModel: ObservableObject { struct OptOutViewData: Identifiable { let id: UUID - let optOutOperationData: OptOutOperationData + let optOutJobData: OptOutJobData let profileName: String let brokerName: String let extractedProfileID: Int64? - internal init(optOutOperationData: OptOutOperationData, brokerName: String) { - self.optOutOperationData = optOutOperationData - self.extractedProfileID = optOutOperationData.extractedProfile.id + internal init(optOutJobData: OptOutJobData, brokerName: String) { + self.optOutJobData = optOutJobData + self.extractedProfileID = optOutJobData.extractedProfile.id self.brokerName = brokerName - self.profileName = "\(extractedProfileID ?? 0) \(optOutOperationData.extractedProfile.fullName ?? "No Name")" + self.profileName = "\(extractedProfileID ?? 0) \(optOutJobData.extractedProfile.fullName ?? "No Name")" self.id = UUID() } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerOperationData.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerJobData.swift similarity index 93% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerOperationData.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerJobData.swift index 258d4cba50..3a564ddc52 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerOperationData.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerJobData.swift @@ -1,5 +1,5 @@ // -// BrokerOperationData.swift +// BrokerJobData.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,7 +18,7 @@ import Foundation -protocol BrokerOperationData { +protocol BrokerJobData { var brokerId: Int64 { get } var profileQueryId: Int64 { get } var lastRunDate: Date? { get } @@ -26,7 +26,7 @@ protocol BrokerOperationData { var historyEvents: [HistoryEvent] { get } } -struct ScanOperationData: BrokerOperationData, Sendable { +struct ScanJobData: BrokerJobData, Sendable { let brokerId: Int64 let profileQueryId: Int64 let preferredRunDate: Date? @@ -67,7 +67,7 @@ struct ScanOperationData: BrokerOperationData, Sendable { } } -struct OptOutOperationData: BrokerOperationData, Sendable { +struct OptOutJobData: BrokerJobData, Sendable { let brokerId: Int64 let profileQueryId: Int64 let preferredRunDate: Date? diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift index 375f581128..45a23c5d28 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift @@ -22,15 +22,15 @@ import Common public struct BrokerProfileQueryData: Sendable { let dataBroker: DataBroker let profileQuery: ProfileQuery - let scanOperationData: ScanOperationData - let optOutOperationsData: [OptOutOperationData] + let scanJobData: ScanJobData + let optOutJobData: [OptOutJobData] - var operationsData: [BrokerOperationData] { - optOutOperationsData + [scanOperationData] + var operationsData: [BrokerJobData] { + optOutJobData + [scanJobData] } var extractedProfiles: [ExtractedProfile] { - optOutOperationsData.map { $0.extractedProfile } + optOutJobData.map { $0.extractedProfile } } var events: [HistoryEvent] { @@ -38,16 +38,16 @@ public struct BrokerProfileQueryData: Sendable { } var hasMatches: Bool { - !optOutOperationsData.isEmpty + !optOutJobData.isEmpty } init(dataBroker: DataBroker, profileQuery: ProfileQuery, - scanOperationData: ScanOperationData, - optOutOperationsData: [OptOutOperationData] = [OptOutOperationData]()) { + scanJobData: ScanJobData, + optOutJobData: [OptOutJobData] = [OptOutJobData]()) { self.profileQuery = profileQuery self.dataBroker = dataBroker - self.scanOperationData = scanOperationData - self.optOutOperationsData = optOutOperationsData + self.scanJobData = scanJobData + self.optOutJobData = optOutJobData } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift new file mode 100644 index 0000000000..993b02aa87 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift @@ -0,0 +1,311 @@ +// +// DataBrokerJob.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 WebKit +import BrowserServicesKit +import UserScript +import Common + +protocol DataBrokerJob: CCFCommunicationDelegate { + associatedtype ReturnValue + associatedtype InputValue + + var privacyConfig: PrivacyConfigurationManaging { get } + var prefs: ContentScopeProperties { get } + var query: BrokerProfileQueryData { get } + var emailService: EmailServiceProtocol { get } + var captchaService: CaptchaServiceProtocol { get } + var cookieHandler: CookieHandler { get } + var stageCalculator: StageDurationCalculator { get } + var pixelHandler: EventMapping { get } + + var webViewHandler: WebViewHandler? { get set } + var actionsHandler: ActionsHandler? { get } + var continuation: CheckedContinuation? { get set } + var extractedProfile: ExtractedProfile? { get set } + var shouldRunNextStep: () -> Bool { get } + var retriesCountOnError: Int { get set } + var clickAwaitTime: TimeInterval { get } + var postLoadingSiteStartTime: Date? { get set } + + func run(inputValue: InputValue, + webViewHandler: WebViewHandler?, + actionsHandler: ActionsHandler?, + showWebView: Bool) async throws -> ReturnValue + + func executeNextStep() async + func executeCurrentAction() async +} + +extension DataBrokerJob { + func run(inputValue: InputValue, + webViewHandler: WebViewHandler?, + actionsHandler: ActionsHandler?, + shouldRunNextStep: @escaping () -> Bool) async throws -> ReturnValue { + + try await run(inputValue: inputValue, + webViewHandler: webViewHandler, + actionsHandler: actionsHandler, + showWebView: false) + } +} + +extension DataBrokerJob { + + // MARK: - Shared functions + + // swiftlint:disable:next cyclomatic_complexity + func runNextAction(_ action: Action) async { + switch action { + case is GetCaptchaInfoAction: + stageCalculator.setStage(.captchaParse) + case is ClickAction: + stageCalculator.setStage(.fillForm) + case is FillFormAction: + stageCalculator.setStage(.fillForm) + case is ExpectationAction: + stageCalculator.setStage(.submit) + default: () + } + + if let emailConfirmationAction = action as? EmailConfirmationAction { + do { + stageCalculator.fireOptOutSubmit() + try await runEmailConfirmationAction(action: emailConfirmationAction) + await executeNextStep() + } catch { + await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) + } + + return + } + + if action as? SolveCaptchaAction != nil, let captchaTransactionId = actionsHandler?.captchaTransactionId { + actionsHandler?.captchaTransactionId = nil + stageCalculator.setStage(.captchaSolve) + if let captchaData = try? await captchaService.submitCaptchaToBeResolved(for: captchaTransactionId, + attemptId: stageCalculator.attemptId, + shouldRunNextStep: shouldRunNextStep) { + stageCalculator.fireOptOutCaptchaSolve() + await webViewHandler?.execute(action: action, data: .solveCaptcha(CaptchaToken(token: captchaData))) + } else { + await onError(error: DataBrokerProtectionError.captchaServiceError(CaptchaServiceError.nilDataWhenFetchingCaptchaResult)) + } + + return + } + + if action.needsEmail { + do { + stageCalculator.setStage(.emailGenerate) + let emailData = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url, attemptId: stageCalculator.attemptId) + extractedProfile?.email = emailData.emailAddress + stageCalculator.setEmailPattern(emailData.pattern) + stageCalculator.fireOptOutEmailGenerate() + } catch { + await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) + return + } + } + + await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile)) + } + + private func runEmailConfirmationAction(action: EmailConfirmationAction) async throws { + if let email = extractedProfile?.email { + stageCalculator.setStage(.emailReceive) + let url = try await emailService.getConfirmationLink( + from: email, + numberOfRetries: 100, // Move to constant + pollingInterval: action.pollingTime, + attemptId: stageCalculator.attemptId, + shouldRunNextStep: shouldRunNextStep + ) + stageCalculator.fireOptOutEmailReceive() + stageCalculator.setStage(.emailReceive) + do { + try await webViewHandler?.load(url: url) + } catch { + await onError(error: error) + return + } + + stageCalculator.fireOptOutEmailConfirm() + } else { + throw EmailError.cantFindEmail + } + } + + func complete(_ value: ReturnValue) { + self.firePostLoadingDurationPixel(hasError: false) + self.continuation?.resume(returning: value) + self.continuation = nil + } + + func failed(with error: Error) { + self.firePostLoadingDurationPixel(hasError: true) + self.continuation?.resume(throwing: error) + self.continuation = nil + } + + func initialize(handler: WebViewHandler?, + isFakeBroker: Bool = false, + showWebView: Bool) async { + if let handler = handler { // This help us swapping up the WebViewHandler on tests + self.webViewHandler = handler + } else { + self.webViewHandler = await DataBrokerProtectionWebViewHandler(privacyConfig: privacyConfig, prefs: prefs, delegate: self, isFakeBroker: isFakeBroker) + } + + await webViewHandler?.initializeWebView(showWebView: showWebView) + } + + // MARK: - CSSCommunicationDelegate + + func loadURL(url: URL) async { + let webSiteStartLoadingTime = Date() + + do { + // https://app.asana.com/0/1204167627774280/1206912494469284/f + if query.dataBroker.url == "spokeo.com" { + if let cookies = await cookieHandler.getAllCookiesFromDomain(url) { + await webViewHandler?.setCookies(cookies) + } + } + try await webViewHandler?.load(url: url) + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: false) + postLoadingSiteStartTime = Date() + await executeNextStep() + } catch { + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: true) + await onError(error: error) + } + } + + private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { + if stageCalculator.isManualScan { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + } + } + + func firePostLoadingDurationPixel(hasError: Bool) { + if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + } + } + + func success(actionId: String, actionType: ActionType) async { + switch actionType { + case .click: + stageCalculator.fireOptOutFillForm() + // We wait 40 seconds before tapping + try? await Task.sleep(nanoseconds: UInt64(clickAwaitTime) * 1_000_000_000) + await executeNextStep() + case .fillForm: + stageCalculator.fireOptOutFillForm() + await executeNextStep() + default: await executeNextStep() + } + } + + func captchaInformation(captchaInfo: GetCaptchaInfoResponse) async { + do { + stageCalculator.fireOptOutCaptchaParse() + stageCalculator.setStage(.captchaSend) + actionsHandler?.captchaTransactionId = try await captchaService.submitCaptchaInformation( + captchaInfo, + attemptId: stageCalculator.attemptId, + shouldRunNextStep: shouldRunNextStep) + stageCalculator.fireOptOutCaptchaSend() + await executeNextStep() + } catch { + if let captchaError = error as? CaptchaServiceError { + await onError(error: DataBrokerProtectionError.captchaServiceError(captchaError)) + } else { + await onError(error: DataBrokerProtectionError.captchaServiceError(.errorWhenSubmittingCaptcha)) + } + } + } + + func solveCaptcha(with response: SolveCaptchaResponse) async { + do { + try await webViewHandler?.evaluateJavaScript(response.callback.eval) + + await executeNextStep() + } catch { + await onError(error: DataBrokerProtectionError.solvingCaptchaWithCallbackError) + } + } + + func onError(error: Error) async { + if retriesCountOnError > 0 { + await executeCurrentAction() + } else { + await webViewHandler?.finish() + failed(with: error) + } + } + + func executeCurrentAction() async { + let waitTimeUntilRunningTheActionAgain: TimeInterval = 3 + try? await Task.sleep(nanoseconds: UInt64(waitTimeUntilRunningTheActionAgain) * 1_000_000_000) + + if let currentAction = self.actionsHandler?.currentAction() { + retriesCountOnError -= 1 + await runNextAction(currentAction) + } else { + retriesCountOnError = 0 + await onError(error: DataBrokerProtectionError.unknown("No current action to execute")) + } + } +} + +protocol CookieHandler { + func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? +} + +struct BrokerCookieHandler: CookieHandler { + + func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? { + guard let domainURL = extractSchemeAndHostAsURL(from: url.absoluteString) else { return nil } + do { + let (_, response) = try await URLSession.shared.data(from: domainURL) + guard let httpResponse = response as? HTTPURLResponse, + let allHeaderFields = httpResponse.allHeaderFields as? [String: String] else { return nil } + + let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: domainURL) + return cookies + } catch { + print("Error fetching data: \(error)") + } + + return nil + } + + private func extractSchemeAndHostAsURL(from url: String) -> URL? { + if let urlComponents = URLComponents(string: url), let scheme = urlComponents.scheme, let host = urlComponents.host { + return URL(string: "\(scheme)://\(host)") + } + return nil + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift similarity index 95% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift index 52a64c3fac..24c2e0ee0b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift @@ -1,5 +1,5 @@ // -// DataBrokerOperationRunner.swift +// DataBrokerJobRunner.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,7 +20,7 @@ import Foundation import BrowserServicesKit import Common -protocol WebOperationRunner { +protocol WebJobRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, @@ -36,7 +36,7 @@ protocol WebOperationRunner { shouldRunNextStep: @escaping () -> Bool) async throws } -extension WebOperationRunner { +extension WebJobRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, @@ -66,7 +66,7 @@ extension WebOperationRunner { } @MainActor -final class DataBrokerOperationRunner: WebOperationRunner { +final class DataBrokerJobRunner: WebJobRunner { let privacyConfigManager: PrivacyConfigurationManaging let contentScopeProperties: ContentScopeProperties let emailService: EmailServiceProtocol @@ -87,7 +87,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { - let scan = ScanOperation( + let scan = ScanJob( privacyConfig: privacyConfigManager, prefs: contentScopeProperties, query: profileQuery, @@ -106,7 +106,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { pixelHandler: EventMapping, showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { - let optOut = OptOutOperation( + let optOut = OptOutJob( privacyConfig: privacyConfigManager, prefs: contentScopeProperties, query: profileQuery, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunnerProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunnerProvider.swift similarity index 80% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunnerProvider.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunnerProvider.swift index c9899856ab..d9bf0b8682 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunnerProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunnerProvider.swift @@ -1,5 +1,5 @@ // -// DataBrokerOperationRunnerProvider.swift +// DataBrokerJobRunnerProvider.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,15 +19,19 @@ import Foundation import BrowserServicesKit -struct DataBrokerOperationRunnerProvider: OperationRunnerProvider { +protocol JobRunnerProvider { + func getJobRunner() -> WebJobRunner +} + +struct DataBrokerJobRunnerProvider: JobRunnerProvider { var privacyConfigManager: PrivacyConfigurationManaging var contentScopeProperties: ContentScopeProperties var emailService: EmailServiceProtocol var captchaService: CaptchaServiceProtocol @MainActor - func getOperationRunner() -> WebOperationRunner { - DataBrokerOperationRunner(privacyConfigManager: privacyConfigManager, + func getJobRunner() -> WebJobRunner { + DataBrokerJobRunner(privacyConfigManager: privacyConfigManager, contentScopeProperties: contentScopeProperties, emailService: emailService, captchaService: captchaService diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 893ed9e350..c5032a2df3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -17,295 +17,208 @@ // import Foundation -import WebKit -import BrowserServicesKit -import UserScript import Common -protocol DataBrokerOperation: CCFCommunicationDelegate { - associatedtype ReturnValue - associatedtype InputValue - - var privacyConfig: PrivacyConfigurationManaging { get } - var prefs: ContentScopeProperties { get } - var query: BrokerProfileQueryData { get } - var emailService: EmailServiceProtocol { get } - var captchaService: CaptchaServiceProtocol { get } - var cookieHandler: CookieHandler { get } - var stageCalculator: StageDurationCalculator { get } - var pixelHandler: EventMapping { get } - - var webViewHandler: WebViewHandler? { get set } - var actionsHandler: ActionsHandler? { get } - var continuation: CheckedContinuation? { get set } - var extractedProfile: ExtractedProfile? { get set } - var shouldRunNextStep: () -> Bool { get } - var retriesCountOnError: Int { get set } - var clickAwaitTime: TimeInterval { get } - var postLoadingSiteStartTime: Date? { get set } - - func run(inputValue: InputValue, - webViewHandler: WebViewHandler?, - actionsHandler: ActionsHandler?, - showWebView: Bool) async throws -> ReturnValue - - func executeNextStep() async - func executeCurrentAction() async +enum OperationType { + case manualScan + case optOut + case all } -extension DataBrokerOperation { - func run(inputValue: InputValue, - webViewHandler: WebViewHandler?, - actionsHandler: ActionsHandler?, - shouldRunNextStep: @escaping () -> Bool) async throws -> ReturnValue { - - try await run(inputValue: inputValue, - webViewHandler: webViewHandler, - actionsHandler: actionsHandler, - showWebView: false) - } +protocol DataBrokerOperationDependencies { + var database: DataBrokerProtectionRepository { get } + var brokerTimeInterval: TimeInterval { get } + var runnerProvider: JobRunnerProvider { get } + var notificationCenter: NotificationCenter { get } + var pixelHandler: EventMapping { get } + var userNotificationService: DataBrokerProtectionUserNotificationService { get } } -extension DataBrokerOperation { - - // MARK: - Shared functions - - // swiftlint:disable:next cyclomatic_complexity - func runNextAction(_ action: Action) async { - switch action { - case is GetCaptchaInfoAction: - stageCalculator.setStage(.captchaParse) - case is ClickAction: - stageCalculator.setStage(.fillForm) - case is FillFormAction: - stageCalculator.setStage(.fillForm) - case is ExpectationAction: - stageCalculator.setStage(.submit) - default: () - } - - if let emailConfirmationAction = action as? EmailConfirmationAction { - do { - stageCalculator.fireOptOutSubmit() - try await runEmailConfirmationAction(action: emailConfirmationAction) - await executeNextStep() - } catch { - await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) - } - - return - } - - if action as? SolveCaptchaAction != nil, let captchaTransactionId = actionsHandler?.captchaTransactionId { - actionsHandler?.captchaTransactionId = nil - stageCalculator.setStage(.captchaSolve) - if let captchaData = try? await captchaService.submitCaptchaToBeResolved(for: captchaTransactionId, - attemptId: stageCalculator.attemptId, - shouldRunNextStep: shouldRunNextStep) { - stageCalculator.fireOptOutCaptchaSolve() - await webViewHandler?.execute(action: action, data: .solveCaptcha(CaptchaToken(token: captchaData))) - } else { - await onError(error: DataBrokerProtectionError.captchaServiceError(CaptchaServiceError.nilDataWhenFetchingCaptchaResult)) - } +struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { + let database: DataBrokerProtectionRepository + let brokerTimeInterval: TimeInterval + let runnerProvider: JobRunnerProvider + let notificationCenter: NotificationCenter + let pixelHandler: EventMapping + let userNotificationService: DataBrokerProtectionUserNotificationService +} +final class DataBrokerOperation: Operation { + + public var error: Error? + + private let dataBrokerID: Int64 + private let database: DataBrokerProtectionRepository + private let id = UUID() + private var _isExecuting = false + private var _isFinished = false + private let brokerTimeInterval: TimeInterval? // The time in seconds to wait in-between operations + private let priorityDate: Date? // The date to filter and sort operations priorities + private let operationType: OperationType + private let notificationCenter: NotificationCenter + private let runner: WebJobRunner + private let pixelHandler: EventMapping + private let showWebView: Bool + private let userNotificationService: DataBrokerProtectionUserNotificationService + + deinit { + os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) + } + + init(dataBrokerID: Int64, + operationType: OperationType, + priorityDate: Date? = nil, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies) { + + self.dataBrokerID = dataBrokerID + self.priorityDate = priorityDate + self.operationType = operationType + self.showWebView = showWebView + self.database = operationDependencies.database + self.brokerTimeInterval = operationDependencies.brokerTimeInterval + self.runner = operationDependencies.runnerProvider.getJobRunner() + self.notificationCenter = operationDependencies.notificationCenter + self.pixelHandler = operationDependencies.pixelHandler + self.userNotificationService = operationDependencies.userNotificationService + super.init() + } + + override func start() { + if isCancelled { + finish() return } - if action.needsEmail { - do { - stageCalculator.setStage(.emailGenerate) - let emailData = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url, attemptId: stageCalculator.attemptId) - extractedProfile?.email = emailData.emailAddress - stageCalculator.setEmailPattern(emailData.pattern) - stageCalculator.fireOptOutEmailGenerate() - } catch { - await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) - return - } - } + willChangeValue(forKey: #keyPath(isExecuting)) + _isExecuting = true + didChangeValue(forKey: #keyPath(isExecuting)) - await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile)) + main() } - private func runEmailConfirmationAction(action: EmailConfirmationAction) async throws { - if let email = extractedProfile?.email { - stageCalculator.setStage(.emailReceive) - let url = try await emailService.getConfirmationLink( - from: email, - numberOfRetries: 100, // Move to constant - pollingInterval: action.pollingTime, - attemptId: stageCalculator.attemptId, - shouldRunNextStep: shouldRunNextStep - ) - stageCalculator.fireOptOutEmailReceive() - stageCalculator.setStage(.emailReceive) - do { - try await webViewHandler?.load(url: url) - } catch { - await onError(error: error) - return - } - - stageCalculator.fireOptOutEmailConfirm() - } else { - throw EmailError.cantFindEmail - } + override var isAsynchronous: Bool { + return true } - func complete(_ value: ReturnValue) { - self.firePostLoadingDurationPixel(hasError: false) - self.continuation?.resume(returning: value) - self.continuation = nil + override var isExecuting: Bool { + return _isExecuting } - func failed(with error: Error) { - self.firePostLoadingDurationPixel(hasError: true) - self.continuation?.resume(throwing: error) - self.continuation = nil + override var isFinished: Bool { + return _isFinished } - func initialize(handler: WebViewHandler?, - isFakeBroker: Bool = false, - showWebView: Bool) async { - if let handler = handler { // This help us swapping up the WebViewHandler on tests - self.webViewHandler = handler - } else { - self.webViewHandler = await DataBrokerProtectionWebViewHandler(privacyConfig: privacyConfig, prefs: prefs, delegate: self, isFakeBroker: isFakeBroker) + override func main() { + Task { + await runOperation() + finish() } - - await webViewHandler?.initializeWebView(showWebView: showWebView) } - // MARK: - CSSCommunicationDelegate + private func filterAndSortOperationsData(brokerProfileQueriesData: [BrokerProfileQueryData], operationType: OperationType, priorityDate: Date?) -> [BrokerJobData] { + let operationsData: [BrokerJobData] - func loadURL(url: URL) async { - let webSiteStartLoadingTime = Date() - - do { - // https://app.asana.com/0/1204167627774280/1206912494469284/f - if query.dataBroker.url == "spokeo.com" { - if let cookies = await cookieHandler.getAllCookiesFromDomain(url) { - await webViewHandler?.setCookies(cookies) - } - } - try await webViewHandler?.load(url: url) - fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: false) - postLoadingSiteStartTime = Date() - await executeNextStep() - } catch { - fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: true) - await onError(error: error) + switch operationType { + case .optOut: + operationsData = brokerProfileQueriesData.flatMap { $0.optOutJobData } + case .manualScan: + operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanJobData } + case .all: + operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } } - } - private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { - if stageCalculator.isManualScan { - let dataBrokerURL = self.query.dataBroker.url - let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) - pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) - } - } + let filteredAndSortedOperationsData: [BrokerJobData] - func firePostLoadingDurationPixel(hasError: Bool) { - if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { - let dataBrokerURL = self.query.dataBroker.url - let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) - pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) + if let priorityDate = priorityDate { + filteredAndSortedOperationsData = operationsData + .filter { $0.preferredRunDate != nil && $0.preferredRunDate! <= priorityDate } + .sorted { $0.preferredRunDate! < $1.preferredRunDate! } + } else { + filteredAndSortedOperationsData = operationsData } - } - func success(actionId: String, actionType: ActionType) async { - switch actionType { - case .click: - stageCalculator.fireOptOutFillForm() - // We wait 40 seconds before tapping - try? await Task.sleep(nanoseconds: UInt64(clickAwaitTime) * 1_000_000_000) - await executeNextStep() - case .fillForm: - stageCalculator.fireOptOutFillForm() - await executeNextStep() - default: await executeNextStep() - } + return filteredAndSortedOperationsData } - func captchaInformation(captchaInfo: GetCaptchaInfoResponse) async { - do { - stageCalculator.fireOptOutCaptchaParse() - stageCalculator.setStage(.captchaSend) - actionsHandler?.captchaTransactionId = try await captchaService.submitCaptchaInformation( - captchaInfo, - attemptId: stageCalculator.attemptId, - shouldRunNextStep: shouldRunNextStep) - stageCalculator.fireOptOutCaptchaSend() - await executeNextStep() - } catch { - if let captchaError = error as? CaptchaServiceError { - await onError(error: DataBrokerProtectionError.captchaServiceError(captchaError)) - } else { - await onError(error: DataBrokerProtectionError.captchaServiceError(.errorWhenSubmittingCaptcha)) - } - } - } + // swiftlint:disable:next function_body_length + private func runOperation() async { + let allBrokerProfileQueryData: [BrokerProfileQueryData] - func solveCaptcha(with response: SolveCaptchaResponse) async { do { - try await webViewHandler?.evaluateJavaScript(response.callback.eval) - - await executeNextStep() + allBrokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() } catch { - await onError(error: DataBrokerProtectionError.solvingCaptchaWithCallbackError) + os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) + return } - } - func onError(error: Error) async { - if retriesCountOnError > 0 { - await executeCurrentAction() - } else { - await webViewHandler?.finish() - failed(with: error) - } - } + let brokerProfileQueriesData = allBrokerProfileQueryData.filter { $0.dataBroker.id == dataBrokerID } - func executeCurrentAction() async { - let waitTimeUntilRunningTheActionAgain: TimeInterval = 3 - try? await Task.sleep(nanoseconds: UInt64(waitTimeUntilRunningTheActionAgain) * 1_000_000_000) + let filteredAndSortedOperationsData = filterAndSortOperationsData(brokerProfileQueriesData: brokerProfileQueriesData, + operationType: operationType, + priorityDate: priorityDate) - if let currentAction = self.actionsHandler?.currentAction() { - retriesCountOnError -= 1 - await runNextAction(currentAction) - } else { - retriesCountOnError = 0 - await onError(error: DataBrokerProtectionError.unknown("No current action to execute")) - } - } -} + os_log("filteredAndSortedOperationsData count: %{public}d for brokerID %{public}d", log: .dataBrokerProtection, filteredAndSortedOperationsData.count, dataBrokerID) -protocol CookieHandler { - func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? -} + for operationData in filteredAndSortedOperationsData { + if isCancelled { + os_log("Cancelled operation, returning...", log: .dataBrokerProtection) + return + } -struct BrokerCookieHandler: CookieHandler { + let brokerProfileData = brokerProfileQueriesData.filter { + $0.dataBroker.id == operationData.brokerId && $0.profileQuery.id == operationData.profileQueryId + }.first - func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? { - guard let domainURL = extractSchemeAndHostAsURL(from: url.absoluteString) else { return nil } - do { - let (_, response) = try await URLSession.shared.data(from: domainURL) - guard let httpResponse = response as? HTTPURLResponse, - let allHeaderFields = httpResponse.allHeaderFields as? [String: String] else { return nil } + guard let brokerProfileData = brokerProfileData else { + continue + } + do { + os_log("Running operation: %{public}@", log: .dataBrokerProtection, String(describing: operationData)) + + try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, + brokerProfileQueryData: brokerProfileData, + database: database, + notificationCenter: notificationCenter, + runner: runner, + pixelHandler: pixelHandler, + showWebView: showWebView, + isManualScan: operationType == .manualScan, + userNotificationService: userNotificationService, + shouldRunNextStep: { [weak self] in + guard let self = self else { return false } + return !self.isCancelled + }) + + if let sleepInterval = brokerTimeInterval { + os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) + try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) + } - let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: domainURL) - return cookies - } catch { - print("Error fetching data: \(error)") + } catch { + os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + self.error = error + + if let error = error as? DataBrokerProtectionError, + let dataBrokerName = brokerProfileQueriesData.first?.dataBroker.name { + pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) + } + } } - return nil + finish() } - private func extractSchemeAndHostAsURL(from url: String) -> URL? { - if let urlComponents = URLComponents(string: url), let scheme = urlComponents.scheme, let host = urlComponents.host { - return URL(string: "\(scheme)://\(host)") - } - return nil + private func finish() { + willChangeValue(forKey: #keyPath(isExecuting)) + willChangeValue(forKey: #keyPath(isFinished)) + + _isExecuting = false + _isFinished = true + + didChangeValue(forKey: #keyPath(isExecuting)) + didChangeValue(forKey: #keyPath(isFinished)) + + os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift deleted file mode 100644 index d585da0cd7..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// DataBrokerOperationsCollection.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 Common - -protocol DataBrokerOperationsCollectionErrorDelegate: AnyObject { - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, - didError error: Error, - whileRunningBrokerOperationData: BrokerOperationData, - withDataBrokerName dataBrokerName: String?) - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, - didErrorBeforeStartingBrokerOperations error: Error) -} - -enum OperationType { - case manualScan - case optOut - case all -} - -final class DataBrokerOperationsCollection: Operation { - public var error: Error? - public weak var errorDelegate: DataBrokerOperationsCollectionErrorDelegate? - - private let dataBrokerID: Int64 - private let database: DataBrokerProtectionRepository - private let id = UUID() - private var _isExecuting = false - private var _isFinished = false - private let intervalBetweenOperations: TimeInterval? // The time in seconds to wait in-between operations - private let priorityDate: Date? // The date to filter and sort operations priorities - private let operationType: OperationType - private let notificationCenter: NotificationCenter - private let runner: WebOperationRunner - private let pixelHandler: EventMapping - private let showWebView: Bool - private let userNotificationService: DataBrokerProtectionUserNotificationService - - deinit { - os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) - } - - init(dataBrokerID: Int64, - database: DataBrokerProtectionRepository, - operationType: OperationType, - intervalBetweenOperations: TimeInterval? = nil, - priorityDate: Date? = nil, - notificationCenter: NotificationCenter = NotificationCenter.default, - runner: WebOperationRunner, - pixelHandler: EventMapping, - userNotificationService: DataBrokerProtectionUserNotificationService, - showWebView: Bool) { - - self.dataBrokerID = dataBrokerID - self.database = database - self.intervalBetweenOperations = intervalBetweenOperations - self.priorityDate = priorityDate - self.operationType = operationType - self.notificationCenter = notificationCenter - self.runner = runner - self.pixelHandler = pixelHandler - self.showWebView = showWebView - self.userNotificationService = userNotificationService - super.init() - } - - override func start() { - if isCancelled { - finish() - return - } - - willChangeValue(forKey: #keyPath(isExecuting)) - _isExecuting = true - didChangeValue(forKey: #keyPath(isExecuting)) - - main() - } - - override var isAsynchronous: Bool { - return true - } - - override var isExecuting: Bool { - return _isExecuting - } - - override var isFinished: Bool { - return _isFinished - } - - override func main() { - Task { - await runOperation() - finish() - } - } - - private func filterAndSortOperationsData(brokerProfileQueriesData: [BrokerProfileQueryData], operationType: OperationType, priorityDate: Date?) -> [BrokerOperationData] { - let operationsData: [BrokerOperationData] - - switch operationType { - case .optOut: - operationsData = brokerProfileQueriesData.flatMap { $0.optOutOperationsData } - case .manualScan: - operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanOperationData } - case .all: - operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } - } - - let filteredAndSortedOperationsData: [BrokerOperationData] - - if let priorityDate = priorityDate { - filteredAndSortedOperationsData = operationsData - .filter { $0.preferredRunDate != nil && $0.preferredRunDate! <= priorityDate } - .sorted { $0.preferredRunDate! < $1.preferredRunDate! } - } else { - filteredAndSortedOperationsData = operationsData - } - - return filteredAndSortedOperationsData - } - - // swiftlint:disable:next function_body_length - private func runOperation() async { - let allBrokerProfileQueryData: [BrokerProfileQueryData] - - do { - allBrokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() - } catch { - os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) - errorDelegate?.dataBrokerOperationsCollection(self, didErrorBeforeStartingBrokerOperations: error) - return - } - - let brokerProfileQueriesData = allBrokerProfileQueryData.filter { $0.dataBroker.id == dataBrokerID } - - let filteredAndSortedOperationsData = filterAndSortOperationsData(brokerProfileQueriesData: brokerProfileQueriesData, - operationType: operationType, - priorityDate: priorityDate) - - os_log("filteredAndSortedOperationsData count: %{public}d for brokerID %{public}d", log: .dataBrokerProtection, filteredAndSortedOperationsData.count, dataBrokerID) - - for operationData in filteredAndSortedOperationsData { - if isCancelled { - os_log("Cancelled operation, returning...", log: .dataBrokerProtection) - return - } - - let brokerProfileData = brokerProfileQueriesData.filter { - $0.dataBroker.id == operationData.brokerId && $0.profileQuery.id == operationData.profileQueryId - }.first - - guard let brokerProfileData = brokerProfileData else { - continue - } - do { - os_log("Running operation: %{public}@", log: .dataBrokerProtection, String(describing: operationData)) - - try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, - brokerProfileQueryData: brokerProfileData, - database: database, - notificationCenter: notificationCenter, - runner: runner, - pixelHandler: pixelHandler, - showWebView: showWebView, - isManualScan: operationType == .manualScan, - userNotificationService: userNotificationService, - shouldRunNextStep: { [weak self] in - guard let self = self else { return false } - return !self.isCancelled - }) - - if let sleepInterval = intervalBetweenOperations { - os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) - try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) - } - - } catch { - os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - self.error = error - errorDelegate?.dataBrokerOperationsCollection(self, - didError: error, - whileRunningBrokerOperationData: operationData, - withDataBrokerName: brokerProfileQueriesData.first?.dataBroker.name) - } - } - - finish() - } - - private func finish() { - willChangeValue(forKey: #keyPath(isExecuting)) - willChangeValue(forKey: #keyPath(isFinished)) - - _isExecuting = false - _isFinished = true - - didChangeValue(forKey: #keyPath(isExecuting)) - didChangeValue(forKey: #keyPath(isFinished)) - - os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index f7eeb24d2b..c1975440f3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -28,11 +28,11 @@ protocol OperationsManager { // We want to refactor this to return a NSOperation in the future // so we have more control of stopping/starting the queue // for the time being, shouldRunNextStep: @escaping () -> Bool is being used - func runOperation(operationData: BrokerOperationData, + func runOperation(operationData: BrokerJobData, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, - runner: WebOperationRunner, + runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool, isManualScan: Bool, @@ -41,11 +41,11 @@ protocol OperationsManager { } extension OperationsManager { - func runOperation(operationData: BrokerOperationData, + func runOperation(operationData: BrokerJobData, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, - runner: WebOperationRunner, + runner: WebJobRunner, pixelHandler: EventMapping, userNotificationService: DataBrokerProtectionUserNotificationService, isManual: Bool, @@ -66,18 +66,18 @@ extension OperationsManager { struct DataBrokerProfileQueryOperationManager: OperationsManager { - internal func runOperation(operationData: BrokerOperationData, + internal func runOperation(operationData: BrokerJobData, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter = NotificationCenter.default, - runner: WebOperationRunner, + runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool = false, isManualScan: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { - if operationData as? ScanOperationData != nil { + if operationData as? ScanJobData != nil { try await runScanOperation(on: runner, brokerProfileQueryData: brokerProfileQueryData, database: database, @@ -87,8 +87,8 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { isManual: isManualScan, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) - } else if let optOutOperationData = operationData as? OptOutOperationData { - try await runOptOutOperation(for: optOutOperationData.extractedProfile, + } else if let optOutJobData = operationData as? OptOutJobData { + try await runOptOutOperation(for: optOutJobData.extractedProfile, on: runner, brokerProfileQueryData: brokerProfileQueryData, database: database, @@ -101,7 +101,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } // swiftlint:disable:next cyclomatic_complexity function_body_length - internal func runScanOperation(on runner: WebOperationRunner, + internal func runScanOperation(on runner: WebJobRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, @@ -169,13 +169,13 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // This is done inside a transaction on the database side. We insert the extracted profile and then // we insert the opt-out operation, we do not want to do things separately in case creating an opt-out fails // causing the extracted profile to be orphan. - let optOutOperationData = OptOutOperationData(brokerId: brokerId, + let optOutJobData = OptOutJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: preferredRunOperation, historyEvents: [HistoryEvent](), extractedProfile: extractedProfile) - try database.saveOptOutOperation(optOut: optOutOperationData, extractedProfile: extractedProfile) + try database.saveOptOutJob(optOut: optOutJobData, extractedProfile: extractedProfile) os_log("Creating new opt-out operation data for: %@", log: .dataBrokerProtection, String(describing: extractedProfile.name)) } @@ -269,7 +269,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // swiftlint:disable:next function_body_length internal func runOptOutOperation(for extractedProfile: ExtractedProfile, - on runner: WebOperationRunner, + on runner: WebJobRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift index 73cb7fb2b1..134de2edd0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift @@ -50,21 +50,21 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { guard let brokerProfileQuery = try database.brokerProfileQueryData(for: brokerId, and: profileQueryId) else { return } - try updateScanOperationDataDates(origin: origin, + try updateScanJobDataDates(origin: origin, + brokerId: brokerId, + profileQueryId: profileQueryId, + extractedProfileId: extractedProfileId, + schedulingConfig: schedulingConfig, + brokerProfileQuery: brokerProfileQuery) + + // We only need to update the optOut date if we have an extracted profile ID + if let extractedProfileId = extractedProfileId { + try updateOptOutJobDataDates(origin: origin, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId, schedulingConfig: schedulingConfig, brokerProfileQuery: brokerProfileQuery) - - // We only need to update the optOut date if we have an extracted profile ID - if let extractedProfileId = extractedProfileId { - try updateOptOutOperationDataDates(origin: origin, - brokerId: brokerId, - profileQueryId: profileQueryId, - extractedProfileId: extractedProfileId, - schedulingConfig: schedulingConfig, - brokerProfileQuery: brokerProfileQuery) } } @@ -88,14 +88,14 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } } - private func updateScanOperationDataDates(origin: OperationPreferredDateUpdaterOrigin, - brokerId: Int64, - profileQueryId: Int64, - extractedProfileId: Int64?, - schedulingConfig: DataBrokerScheduleConfig, - brokerProfileQuery: BrokerProfileQueryData) throws { + private func updateScanJobDataDates(origin: OperationPreferredDateUpdaterOrigin, + brokerId: Int64, + profileQueryId: Int64, + extractedProfileId: Int64?, + schedulingConfig: DataBrokerScheduleConfig, + brokerProfileQuery: BrokerProfileQueryData) throws { - let currentScanPreferredRunDate = brokerProfileQuery.scanOperationData.preferredRunDate + let currentScanPreferredRunDate = brokerProfileQuery.scanJobData.preferredRunDate var newScanPreferredRunDate = try calculator.dateForScanOperation(currentPreferredRunDate: currentScanPreferredRunDate, historyEvents: brokerProfileQuery.events, @@ -114,15 +114,15 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } } - private func updateOptOutOperationDataDates(origin: OperationPreferredDateUpdaterOrigin, - brokerId: Int64, - profileQueryId: Int64, - extractedProfileId: Int64?, - schedulingConfig: DataBrokerScheduleConfig, - brokerProfileQuery: BrokerProfileQueryData) throws { + private func updateOptOutJobDataDates(origin: OperationPreferredDateUpdaterOrigin, + brokerId: Int64, + profileQueryId: Int64, + extractedProfileId: Int64?, + schedulingConfig: DataBrokerScheduleConfig, + brokerProfileQuery: BrokerProfileQueryData) throws { - let optOutOperation = brokerProfileQuery.optOutOperationsData.filter { $0.extractedProfile.id == extractedProfileId }.first - let currentOptOutPreferredRunDate = optOutOperation?.preferredRunDate + let optOutJob = brokerProfileQuery.optOutJobData.filter { $0.extractedProfile.id == extractedProfileId }.first + let currentOptOutPreferredRunDate = optOutJob?.preferredRunDate var newOptOutPreferredDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: currentOptOutPreferredRunDate, historyEvents: brokerProfileQuery.events, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift similarity index 98% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift index 0a957a62f8..7ffb40e858 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift @@ -1,5 +1,5 @@ // -// OptOutOperation.swift +// OptOutJob.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +22,7 @@ import BrowserServicesKit import UserScript import Common -final class OptOutOperation: DataBrokerOperation { +final class OptOutJob: DataBrokerJob { typealias ReturnValue = Void typealias InputValue = ExtractedProfile diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift index d4d83f7b2a..fae7c89425 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift @@ -52,14 +52,14 @@ struct MismatchCalculatorUseCase { let parentBrokerProfileQueryData = brokerProfileQueryData.filter { $0.dataBroker.parent == nil } for parent in parentBrokerProfileQueryData { - guard let parentMatches = parent.scanOperationData.historyEvents.matchesForLastEvent() else { continue } + guard let parentMatches = parent.scanJobData.historyEvents.matchesForLastEvent() else { continue } let children = brokerProfileQueryData.filter { $0.dataBroker.parent == parent.dataBroker.url && $0.profileQuery.id == parent.profileQuery.id } for child in children { - guard let childMatches = child.scanOperationData.historyEvents.matchesForLastEvent() else { continue } + guard let childMatches = child.scanJobData.historyEvents.matchesForLastEvent() else { continue } let mismatchValue = MismatchValues.calculate(parent: parentMatches, child: childMatches) pixelHandler.fire( diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift similarity index 98% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift index f7bb9de995..f491370fd8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift @@ -1,5 +1,5 @@ // -// ScanOperation.swift +// ScanJob.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +22,7 @@ import BrowserServicesKit import UserScript import Common -final class ScanOperation: DataBrokerOperation { +final class ScanJob: DataBrokerJob { typealias ReturnValue = [ExtractedProfile] typealias InputValue = Void diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift index 589ab33afb..ea371e0d27 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift @@ -102,7 +102,7 @@ final class DataBrokerProtectionEventPixels { var removalsInTheLastWeek = 0 for query in data { - let allHistoryEventsForQuery = query.scanOperationData.historyEvents + query.optOutOperationsData.flatMap { $0.historyEvents } + let allHistoryEventsForQuery = query.scanJobData.historyEvents + query.optOutJobData.flatMap { $0.historyEvents } let historyEventsInThePastWeek = allHistoryEventsForQuery.filter { !didWeekPassedBetweenDates(start: $0.date, end: Date()) } @@ -132,7 +132,7 @@ final class DataBrokerProtectionEventPixels { } private func hadScanThisWeek(_ brokerProfileQuery: BrokerProfileQueryData) -> Bool { - return brokerProfileQuery.scanOperationData.historyEvents.contains { historyEvent in + return brokerProfileQuery.scanJobData.historyEvents.contains { historyEvent in !didWeekPassedBetweenDates(start: historyEvent.date, end: Date()) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift new file mode 100644 index 0000000000..41d02970af --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift @@ -0,0 +1,56 @@ +// +// DataBrokerOperationsCreator.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 Common +import Foundation + +protocol DataBrokerOperationsCreator { + func operations(forOperationType operationType: OperationType, + withPriorityDate priorityDate: Date?, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] +} + +final class DefaultDataBrokerOperationsCreator: DataBrokerOperationsCreator { + + func operations(forOperationType operationType: OperationType, + withPriorityDate priorityDate: Date?, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] { + + let brokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() + var operations: [DataBrokerOperation] = [] + var visitedDataBrokerIDs: Set = [] + + for queryData in brokerProfileQueryData { + guard let dataBrokerID = queryData.dataBroker.id else { continue } + + if !visitedDataBrokerIDs.contains(dataBrokerID) { + let collection = DataBrokerOperation(dataBrokerID: dataBrokerID, + operationType: operationType, + priorityDate: priorityDate, + showWebView: showWebView, + operationDependencies: operationDependencies) + operations.append(collection) + visitedDataBrokerIDs.insert(dataBrokerID) + } + } + + return operations + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index c96a3c31d8..b8cf875adf 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -20,14 +20,10 @@ import Foundation import Common import BrowserServicesKit -protocol OperationRunnerProvider { - func getOperationRunner() -> WebOperationRunner -} - final class DataBrokerProtectionProcessor { private let database: DataBrokerProtectionRepository private let config: DataBrokerProtectionProcessorConfiguration - private let operationRunnerProvider: OperationRunnerProvider + private let jobRunnerProvider: JobRunnerProvider private let notificationCenter: NotificationCenter private let operationQueue: OperationQueue private var pixelHandler: EventMapping @@ -37,14 +33,14 @@ final class DataBrokerProtectionProcessor { init(database: DataBrokerProtectionRepository, config: DataBrokerProtectionProcessorConfiguration = DataBrokerProtectionProcessorConfiguration(), - operationRunnerProvider: OperationRunnerProvider, + jobRunnerProvider: JobRunnerProvider, notificationCenter: NotificationCenter = NotificationCenter.default, pixelHandler: EventMapping, userNotificationService: DataBrokerProtectionUserNotificationService) { self.database = database self.config = config - self.operationRunnerProvider = operationRunnerProvider + self.jobRunnerProvider = jobRunnerProvider self.notificationCenter = notificationCenter self.operationQueue = OperationQueue() self.pixelHandler = pixelHandler @@ -125,17 +121,25 @@ final class DataBrokerProtectionProcessor { // This will try to fire the event weekly report pixels eventPixels.tryToFireWeeklyPixels() - let dataBrokerOperationCollections: [DataBrokerOperationsCollection] + let operations: [DataBrokerOperation] do { - let brokersProfileData = try database.fetchAllBrokerProfileQueryData() - dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, - operationType: operationType, - priorityDate: priorityDate, - showWebView: showWebView) - - for collection in dataBrokerOperationCollections { - operationQueue.addOperation(collection) + // Note: The next task in this project will inject the dependencies & builder into our new 'QueueManager' type + + let dependencies = DefaultDataBrokerOperationDependencies(database: database, + brokerTimeInterval: config.intervalBetweenSameBrokerOperations, + runnerProvider: jobRunnerProvider, + notificationCenter: notificationCenter, + pixelHandler: pixelHandler, + userNotificationService: userNotificationService) + + operations = try DefaultDataBrokerOperationsCreator().operations(forOperationType: operationType, + withPriorityDate: priorityDate, + showWebView: showWebView, + operationDependencies: dependencies) + + for operation in operations { + operationQueue.addOperation(operation) } } catch { os_log("DataBrokerProtectionProcessor error: runOperations, error: %{public}@", log: .error, error.localizedDescription) @@ -146,64 +150,13 @@ final class DataBrokerProtectionProcessor { } operationQueue.addBarrierBlock { - let operationErrors = dataBrokerOperationCollections.compactMap { $0.error } + let operationErrors = operations.compactMap { $0.error } let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionSchedulerErrorCollection(operationErrors: operationErrors) : nil completion(errorCollection) } } - private func createDataBrokerOperationCollections(from brokerProfileQueriesData: [BrokerProfileQueryData], - operationType: OperationType, - priorityDate: Date?, - showWebView: Bool) -> [DataBrokerOperationsCollection] { - - var collections: [DataBrokerOperationsCollection] = [] - var visitedDataBrokerIDs: Set = [] - - for queryData in brokerProfileQueriesData { - guard let dataBrokerID = queryData.dataBroker.id else { continue } - - if !visitedDataBrokerIDs.contains(dataBrokerID) { - let collection = DataBrokerOperationsCollection(dataBrokerID: dataBrokerID, - database: database, - operationType: operationType, - intervalBetweenOperations: config.intervalBetweenSameBrokerOperations, - priorityDate: priorityDate, - notificationCenter: notificationCenter, - runner: operationRunnerProvider.getOperationRunner(), - pixelHandler: pixelHandler, - userNotificationService: userNotificationService, - showWebView: showWebView) - collection.errorDelegate = self - collections.append(collection) - - visitedDataBrokerIDs.insert(dataBrokerID) - } - } - - return collections - } - deinit { os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) } } - -extension DataBrokerProtectionProcessor: DataBrokerOperationsCollectionErrorDelegate { - - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, didErrorBeforeStartingBrokerOperations error: Error) { - - } - - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, - didError error: Error, - whileRunningBrokerOperationData: BrokerOperationData, - withDataBrokerName dataBrokerName: String?) { - if let error = error as? DataBrokerProtectionError, - let dataBrokerName = dataBrokerName { - pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) - } else { - os_log("Cant handle error", log: .dataBrokerProtection) - } - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 509b13272c..fd0b66bbdc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -144,13 +144,13 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch private lazy var dataBrokerProcessor: DataBrokerProtectionProcessor = { - let runnerProvider = DataBrokerOperationRunnerProvider(privacyConfigManager: privacyConfigManager, + let runnerProvider = DataBrokerJobRunnerProvider(privacyConfigManager: privacyConfigManager, contentScopeProperties: contentScopeProperties, emailService: emailService, captchaService: captchaService) return DataBrokerProtectionProcessor(database: dataManager.database, - operationRunnerProvider: runnerProvider, + jobRunnerProvider: runnerProvider, notificationCenter: notificationCenter, pixelHandler: pixelHandler, userNotificationService: userNotificationService) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift index c854055aab..3001b6624b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift @@ -56,16 +56,16 @@ protocol DataBrokerProtectionSecureVault: SecureVault { func save(brokerId: Int64, profileQueryId: Int64, lastRunDate: Date?, preferredRunDate: Date?) throws func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws - func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanOperationData? - func fetchAllScans() throws -> [ScanOperationData] + func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanJobData? + func fetchAllScans() throws -> [ScanJobData] func save(brokerId: Int64, profileQueryId: Int64, extractedProfile: ExtractedProfile, lastRunDate: Date?, preferredRunDate: Date?) throws func save(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64, lastRunDate: Date?, preferredRunDate: Date?) throws func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws - func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutOperationData? - func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutOperationData] - func fetchAllOptOuts() throws -> [OptOutOperationData] + func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutJobData? + func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutJobData] + func fetchAllOptOuts() throws -> [OptOutJobData] func save(historyEvent: HistoryEvent, brokerId: Int64, profileQueryId: Int64) throws func save(historyEvent: HistoryEvent, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws @@ -206,7 +206,7 @@ final class DefaultDataBrokerProtectionSecureVault ScanOperationData? { + func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanJobData? { if let scanDB = try self.providers.database.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) { let scanEvents = try self.providers.database.fetchScanEvents(brokerId: brokerId, profileQueryId: profileQueryId) let mapper = MapperToModel(mechanism: l2Decrypt(data:)) @@ -217,9 +217,9 @@ final class DefaultDataBrokerProtectionSecureVault [ScanOperationData] { + func fetchAllScans() throws -> [ScanJobData] { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) - var scans = [ScanOperationData]() + var scans = [ScanJobData]() let scansDB = try self.providers.database.fetchAllScans() for scan in scansDB { @@ -260,7 +260,7 @@ final class DefaultDataBrokerProtectionSecureVault OptOutOperationData? { + func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutJobData? { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) if let optOutResult = try self.providers.database.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) { let optOutEvents = try self.providers.database.fetchOptOutEvents(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) @@ -270,7 +270,7 @@ final class DefaultDataBrokerProtectionSecureVault [OptOutOperationData] { + func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutJobData] { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) return try self.providers.database.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId).map { @@ -283,7 +283,7 @@ final class DefaultDataBrokerProtectionSecureVault [OptOutOperationData] { + func fetchAllOptOuts() throws -> [OptOutJobData] { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) return try self.providers.database.fetchAllOptOuts().map { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 81cf02f70d..37bae0a2f5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -199,7 +199,7 @@ struct MapperToModel { ) } - func mapToModel(_ scanDB: ScanDB, events: [ScanHistoryEventDB]) throws -> ScanOperationData { + func mapToModel(_ scanDB: ScanDB, events: [ScanHistoryEventDB]) throws -> ScanJobData { .init( brokerId: scanDB.brokerId, profileQueryId: scanDB.profileQueryId, @@ -209,7 +209,7 @@ struct MapperToModel { ) } - func mapToModel(_ optOutDB: OptOutDB, extractedProfileDB: ExtractedProfileDB, events: [OptOutHistoryEventDB]) throws -> OptOutOperationData { + func mapToModel(_ optOutDB: OptOutDB, extractedProfileDB: ExtractedProfileDB, events: [OptOutHistoryEventDB]) throws -> OptOutJobData { .init( brokerId: optOutDB.brokerId, profileQueryId: optOutDB.profileQueryId, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index ee89f641f2..27ef5477a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -105,9 +105,9 @@ struct MapperToUI { brokerProfileQueryData.forEach { let dataBroker = $0.dataBroker - let scanOperation = $0.scanOperationData - for optOutOperation in $0.optOutOperationsData { - let extractedProfile = optOutOperation.extractedProfile + let scanJob = $0.scanJobData + for optOutJob in $0.optOutJobData { + let extractedProfile = optOutJob.extractedProfile let profileMatch = mapToUI(dataBroker, extractedProfile: extractedProfile) if extractedProfile.removedDate == nil { @@ -116,7 +116,7 @@ struct MapperToUI { removedProfiles.append(profileMatch) } - if let closestMatchesFoundEvent = scanOperation.closestMatchesFoundEvent() { + if let closestMatchesFoundEvent = scanJob.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) @@ -160,8 +160,8 @@ struct MapperToUI { format: String = "dd/MM/yyyy") -> DBPUIScanDate { let eightDaysBeforeToday = currentDate.addingTimeInterval(-8 * 24 * 60 * 60) let scansInTheLastEightDays = brokerProfileQueryData - .filter { $0.scanOperationData.lastRunDate != nil && $0.scanOperationData.lastRunDate! <= currentDate && $0.scanOperationData.lastRunDate! > eightDaysBeforeToday } - .sorted { $0.scanOperationData.lastRunDate! < $1.scanOperationData.lastRunDate! } + .filter { $0.scanJobData.lastRunDate != nil && $0.scanJobData.lastRunDate! <= currentDate && $0.scanJobData.lastRunDate! > eightDaysBeforeToday } + .sorted { $0.scanJobData.lastRunDate! < $1.scanJobData.lastRunDate! } .reduce(into: [BrokerProfileQueryData]()) { result, element in if !result.contains(where: { $0.dataBroker.url == element.dataBroker.url }) { result.append(element) @@ -169,10 +169,10 @@ struct MapperToUI { } .flatMap { var brokers = [DBPUIDataBroker]() - brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanOperationData.lastRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970)) - for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanOperationData.lastRunDate! { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanOperationData.lastRunDate!.timeIntervalSince1970)) + for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanJobData.lastRunDate! { + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970)) } return brokers @@ -190,8 +190,8 @@ struct MapperToUI { format: String = "dd/MM/yyyy") -> DBPUIScanDate { let eightDaysAfterToday = currentDate.addingTimeInterval(8 * 24 * 60 * 60) let scansHappeningInTheNextEightDays = brokerProfileQueryData - .filter { $0.scanOperationData.preferredRunDate != nil && $0.scanOperationData.preferredRunDate! > currentDate && $0.scanOperationData.preferredRunDate! < eightDaysAfterToday } - .sorted { $0.scanOperationData.preferredRunDate! < $1.scanOperationData.preferredRunDate! } + .filter { $0.scanJobData.preferredRunDate != nil && $0.scanJobData.preferredRunDate! > currentDate && $0.scanJobData.preferredRunDate! < eightDaysAfterToday } + .sorted { $0.scanJobData.preferredRunDate! < $1.scanJobData.preferredRunDate! } .reduce(into: [BrokerProfileQueryData]()) { result, element in if !result.contains(where: { $0.dataBroker.url == element.dataBroker.url }) { result.append(element) @@ -199,15 +199,15 @@ struct MapperToUI { } .flatMap { var brokers = [DBPUIDataBroker]() - brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanOperationData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) for mirrorSite in $0.dataBroker.mirrorSites { if let removedDate = mirrorSite.removedAt { - if removedDate > $0.scanOperationData.preferredRunDate! { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanOperationData.preferredRunDate!.timeIntervalSince1970)) + if removedDate > $0.scanJobData.preferredRunDate! { + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) } } else { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanOperationData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) } } @@ -313,8 +313,8 @@ fileprivate extension BrokerProfileQueryData { } var sitesScanned: [String] { - if scanOperationData.lastRunDate != nil { - let scanEvents = scanOperationData.scanStartedEvents() + if scanJobData.lastRunDate != nil { + let scanEvents = scanJobData.scanStartedEvents() var sitesScanned = [dataBroker.name] for mirrorSite in dataBroker.mirrorSites { @@ -344,7 +344,7 @@ fileprivate extension Array where Element == BrokerProfileQueryData { var currentScans: Int { guard let broker = self.first?.dataBroker else { return 0 } - let didAllQueriesFinished = allSatisfy { $0.scanOperationData.lastRunDate != nil } + let didAllQueriesFinished = allSatisfy { $0.scanJobData.lastRunDate != nil } if !didAllQueriesFinished { return 0 @@ -353,7 +353,7 @@ fileprivate extension Array where Element == BrokerProfileQueryData { } } - var lastOperation: BrokerOperationData? { + var lastOperation: BrokerJobData? { let allOperations = flatMap { $0.operationsData } let lastOperation = allOperations.sorted(by: { if let date1 = $0.lastRunDate, let date2 = $1.lastRunDate { @@ -378,7 +378,7 @@ fileprivate extension Array where Element == BrokerProfileQueryData { return lastError } - var lastStartedOperation: BrokerOperationData? { + var lastStartedOperation: BrokerJobData? { let allOperations = flatMap { $0.operationsData } return allOperations.sorted(by: { @@ -393,9 +393,9 @@ fileprivate extension Array where Element == BrokerProfileQueryData { } } -fileprivate extension BrokerOperationData { +fileprivate extension BrokerJobData { var toString: String { - if (self as? OptOutOperationData) != nil { + if (self as? OptOutJobData) != nil { return "optOut" } else { return "scan" diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index cdbb776170..1d5126cf5f 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -40,7 +40,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let emailConfirmationAction = EmailConfirmationAction(id: "", actionType: .emailConfirmation, pollingTime: 1, dataSource: nil) let step = Step(type: .optOut, actions: [emailConfirmationAction]) let extractedProfile = ExtractedProfile(email: "test@duck.com") - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -65,7 +65,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let emailConfirmationAction = EmailConfirmationAction(id: "", actionType: .emailConfirmation, pollingTime: 1, dataSource: nil) let step = Step(type: .optOut, actions: [emailConfirmationAction]) let noEmailExtractedProfile = ExtractedProfile() - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -97,7 +97,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let step = Step(type: .optOut, actions: [emailConfirmationAction]) let extractedProfile = ExtractedProfile(email: "test@duck.com") emailService.shouldThrow = true - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -127,7 +127,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 step = Step(type: .optOut, actions: [fillFormAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -150,7 +150,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 step = Step(type: .optOut, actions: [fillFormAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -178,7 +178,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenClickActionSucceeds_thenWeWaitForWebViewToLoad() async { - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -199,7 +199,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenAnActionThatIsNotClickSucceeds_thenWeDoNotWaitForWebViewToLoad() async { - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -221,7 +221,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenSolveCaptchaActionIsRun_thenCaptchaIsResolved() async { let solveCaptchaAction = SolveCaptchaAction(id: "1", actionType: .solveCaptcha, selector: "g-captcha", dataSource: nil) let step = Step(type: .optOut, actions: [solveCaptchaAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -244,7 +244,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenSolveCapchaActionFailsToSubmitDataToTheBackend_thenOperationFails() async { let solveCaptchaAction = SolveCaptchaAction(id: "1", actionType: .solveCaptcha, selector: "g-captcha", dataSource: nil) let step = Step(type: .optOut, actions: [solveCaptchaAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -274,7 +274,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenCaptchaInformationIsReturned_thenWeSubmitItTotTheBackend() async { let getCaptchaResponse = GetCaptchaInfoResponse(siteKey: "siteKey", url: "url", type: "recaptcha") let step = Step(type: .optOut, actions: [Action]()) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -297,7 +297,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenCaptchaInformationFailsToBeSubmitted_thenTheOperationFails() async { let getCaptchaResponse = GetCaptchaInfoResponse(siteKey: "siteKey", url: "url", type: "recaptcha") let step = Step(type: .optOut, actions: [Action]()) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -321,7 +321,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenRunningActionWithoutExtractedProfile_thenExecuteIsCalledWithProfileData() async { let expectationAction = ExpectationAction(id: "1", actionType: .expectation, expectations: [Item](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -340,7 +340,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenLoadURLDelegateIsCalled_thenCorrectMethodIsExecutedOnWebViewHandler() async { - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -361,7 +361,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenGetCaptchaActionRuns_thenStageIsSetToCaptchaParse() async { let mockStageCalculator = MockStageDurationCalculator() let captchaAction = GetCaptchaInfoAction(id: "1", actionType: .getCaptchaInfo, selector: "captcha", dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -381,7 +381,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenClickActionRuns_thenStageIsSetToSubmit() async { let mockStageCalculator = MockStageDurationCalculator() let clickAction = ClickAction(id: "1", actionType: .click, elements: [PageElement](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -401,7 +401,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenExpectationActionRuns_thenStageIsSetToSubmit() async { let mockStageCalculator = MockStageDurationCalculator() let expectationAction = ExpectationAction(id: "1", actionType: .expectation, expectations: [Item](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -421,7 +421,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenFillFormActionRuns_thenStageIsSetToFillForm() async { let mockStageCalculator = MockStageDurationCalculator() let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "", elements: [PageElement](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -440,7 +440,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenLoadUrlOnSpokeo_thenSetCookiesIsCalled() async { let mockCookieHandler = MockCookieHandler() - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(url: "spokeo.com"), @@ -462,7 +462,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenLoadUrlOnOtherBroker_thenSetCookiesIsNotCalled() async { let mockCookieHandler = MockCookieHandler() - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(url: "verecor.com"), diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift new file mode 100644 index 0000000000..3caeee6475 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -0,0 +1,81 @@ +// +// DataBrokerOperationsCreatorTests.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. +// + +@testable import DataBrokerProtection +import XCTest + +final class DataBrokerOperationsCreatorTests: XCTestCase { + + private let sut: DataBrokerOperationsCreator = DefaultDataBrokerOperationsCreator() + + // Dependencies + private var mockDatabase: MockDatabase! + private var mockSchedulerConfig = DataBrokerProtectionProcessorConfiguration() + private var mockRunnerProvider: MockRunnerProvider! + private var mockPixelHandler: MockPixelHandler! + private var mockUserNotification: MockUserNotification! + var mockDependencies: DefaultDataBrokerOperationDependencies! + + override func setUpWithError() throws { + mockDatabase = MockDatabase() + mockRunnerProvider = MockRunnerProvider() + mockPixelHandler = MockPixelHandler() + mockUserNotification = MockUserNotification() + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + brokerTimeInterval: mockSchedulerConfig.intervalBetweenSameBrokerOperations, + runnerProvider: mockRunnerProvider, + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockUserNotification) + } + + func testWhenBuildOperations_andBrokerQueryDataHasDuplicateBrokers_thenDuplicatesAreIgnored() throws { + // Given + let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock(withId: 1), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 1)), + .init(dataBroker: .mock(withId: 1), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 1)), + .init(dataBroker: .mock(withId: 2), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + .init(dataBroker: .mock(withId: 2), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + .init(dataBroker: .mock(withId: 2), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + .init(dataBroker: .mock(withId: 3), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + ] + mockDatabase.brokerProfileQueryDataToReturn = dataBrokerProfileQueries + + // When + let result = try! sut.operations(forOperationType: .manualScan, + withPriorityDate: Date(), + showWebView: false, + operationDependencies: mockDependencies) + + // Then + XCTAssert(result.count == 3) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 19d92e74d6..7468cfdb9d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -26,7 +26,7 @@ import PixelKit final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let sut = DataBrokerProfileQueryOperationManager() - let mockWebOperationRunner = MockWebOperationRunner() + let mockWebOperationRunner = MockWebJobRunner() let mockDatabase = MockDatabase() let mockUserNotification = MockUserNotification() @@ -50,16 +50,16 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) let extractedProfileSaved = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc") - let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved)] + let optOutData = [OptOutJobData.mock(with: extractedProfileSaved)] let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, - scanOperationData: mockScanOperation, - optOutOperationsData: optOutData) + scanJobData: mockScanOperation, + optOutJobData: optOutData) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] mockWebOperationRunner.scanResults = [] @@ -68,8 +68,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: extractedProfileSaved)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: extractedProfileSaved)] ), database: mockDatabase, notificationCenter: .default, @@ -98,18 +98,18 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc", identifier: "abc") let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz", identifier: "zxz") - let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + let optOutData = [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, - scanOperationData: mockScanOperation, - optOutOperationsData: optOutData) + scanJobData: mockScanOperation, + optOutJobData: optOutData) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] mockWebOperationRunner.scanResults = [extractedProfileSaved1] @@ -118,9 +118,9 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] ), database: mockDatabase, notificationCenter: .default, @@ -149,18 +149,18 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc") let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz") - let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + let optOutData = [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, - scanOperationData: mockScanOperation, - optOutOperationsData: optOutData) + scanJobData: mockScanOperation, + optOutJobData: optOutData) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] mockWebOperationRunner.scanResults = [extractedProfileSaved1, extractedProfileSaved2] @@ -169,9 +169,9 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] ), database: mockDatabase, notificationCenter: .default, @@ -195,7 +195,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mockWithoutId, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, @@ -217,7 +217,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithoutId, profileQuery: .mock, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, @@ -238,7 +238,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, @@ -259,7 +259,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, @@ -282,8 +282,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -308,8 +308,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -332,8 +332,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -356,8 +356,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -379,8 +379,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -404,8 +404,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -430,8 +430,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -459,8 +459,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithoutId, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -483,8 +483,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mockWithoutId, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -507,8 +507,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutId)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutId)] ), database: mockDatabase, notificationCenter: .default, @@ -531,8 +531,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -555,8 +555,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithParentOptOut, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -579,8 +579,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -602,8 +602,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -626,8 +626,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -652,8 +652,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithParentOptOut, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -685,8 +685,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -724,8 +724,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -763,8 +763,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, @@ -894,9 +894,9 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) - let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanOperationData: mockScanOperation) + let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanJobData: mockScanOperation) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] try sut.updateOperationDataDates(origin: .optOut, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId, schedulingConfig: config, database: mockDatabase) @@ -919,9 +919,9 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) - let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanOperationData: mockScanOperation) + let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanJobData: mockScanOperation) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] try sut.updateOperationDataDates(origin: .scan, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId, schedulingConfig: config, database: mockDatabase) @@ -932,7 +932,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { } } -final class MockWebOperationRunner: WebOperationRunner { +final class MockWebJobRunner: WebJobRunner { var shouldScanThrow = false var shouldOptOutThrow = false var scanResults = [ExtractedProfile]() @@ -966,24 +966,9 @@ final class MockWebOperationRunner: WebOperationRunner { } } -extension ScanOperationData { +extension OptOutJobData { - static var mock: ScanOperationData { - .init( - brokerId: 1, - profileQueryId: 1, - historyEvents: [HistoryEvent]() - ) - } - - static func mockWith(historyEvents: [HistoryEvent]) -> ScanOperationData { - ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) - } -} - -extension OptOutOperationData { - - static func mock(with extractedProfile: ExtractedProfile) -> OptOutOperationData { + static func mock(with extractedProfile: ExtractedProfile) -> OptOutJobData { .init(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent](), extractedProfile: extractedProfile) } } @@ -1055,17 +1040,6 @@ extension DataBroker { } } -extension ProfileQuery { - - static var mock: ProfileQuery { - .init(id: 1, firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) - } - - static var mockWithoutId: ProfileQuery { - .init(firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) - } -} - extension ExtractedProfile { static var mockWithRemovedDate: ExtractedProfile { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift index 42c3bc94be..b825cad871 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift @@ -96,7 +96,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance @@ -120,7 +120,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance @@ -144,7 +144,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [newMatchesPriorToThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [newMatchesPriorToThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches @@ -168,7 +168,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [newMatchesThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [newMatchesThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches @@ -192,7 +192,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [removalsPriorToThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [removalsPriorToThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals @@ -217,7 +217,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [removalThisWeekEventOne, removalThisWeekEventTwo])) + scanJobData: .mockWith(historyEvents: [removalThisWeekEventOne, removalThisWeekEventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals @@ -244,7 +244,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne, eventTwo, eventThree, eventFour])) + scanJobData: .mockWith(historyEvents: [eventOne, eventTwo, eventThree, eventFour])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries @@ -274,16 +274,16 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mockWithURL("www.brokerone.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokertwo.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokerthree.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokerfour.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])) + scanJobData: .mockWith(historyEvents: [eventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries @@ -313,16 +313,16 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mockWithURL("www.brokerone.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokertwo.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokerthree.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokerfour.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])) + scanJobData: .mockWith(historyEvents: [eventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries @@ -352,16 +352,16 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mockWithURL("www.brokerone.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokertwo.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokerthree.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokerfour.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])) + scanJobData: .mockWith(historyEvents: [eventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift index 0668f38d35..db8e447d4d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift @@ -194,8 +194,8 @@ final class DataBrokerProtectionProfileTests: XCTestCase { vault.brokers = [DataBroker.mock] vault.profileQueries = [ProfileQuery.mock] - vault.scanOperationData = [ScanOperationData.mock] - vault.optOutOperationData = [OptOutOperationData.mock(with: ExtractedProfile.mockWithoutRemovedDate)] + vault.scanJobData = [ScanJobData.mock] + vault.optOutJobData = [OptOutJobData.mock(with: ExtractedProfile.mockWithoutRemovedDate)] vault.profile = DataBrokerProtectionProfile( names: [ @@ -238,7 +238,7 @@ final class DataBrokerProtectionProfileTests: XCTestCase { vault.brokers = [DataBroker.mock] vault.profileQueries = [ProfileQuery.mock] - vault.scanOperationData = [ScanOperationData.mock] + vault.scanJobData = [ScanJobData.mock] vault.profile = DataBrokerProtectionProfile( names: [ diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index f10a26fb67..396034791a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -158,7 +158,7 @@ extension BrokerProfileQueryData { schedulingConfig: DataBrokerScheduleConfig.mock ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), - scanOperationData: ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) + scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) ) } @@ -173,7 +173,7 @@ extension BrokerProfileQueryData { parent: "parent.com" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), - scanOperationData: ScanOperationData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) + scanJobData: ScanJobData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) ) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 9b60fe4812..5e71a5b755 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -45,12 +45,12 @@ extension BrokerProfileQueryData { mirrorSites: mirrorSites ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50, deprecated: deprecated), - scanOperationData: ScanOperationData(brokerId: 1, + scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, preferredRunDate: preferredRunDate, historyEvents: scanHistoryEvents, lastRunDate: lastRunDate), - optOutOperationsData: extractedProfile != nil ? [.mock(with: extractedProfile!)] : [OptOutOperationData]() + optOutJobData: extractedProfile != nil ? [.mock(with: extractedProfile!)] : [OptOutJobData]() ) } } @@ -478,8 +478,8 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault var profile: DataBrokerProtectionProfile? var profileQueries = [ProfileQuery]() var brokers = [DataBroker]() - var scanOperationData = [ScanOperationData]() - var optOutOperationData = [OptOutOperationData]() + var scanJobData = [ScanJobData]() + var optOutJobData = [OptOutJobData]() var lastPreferredRunDateOnScan: Date? typealias DatabaseProvider = SecureStorageDatabaseProviderMock @@ -498,8 +498,8 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault profile = nil profileQueries.removeAll() brokers.removeAll() - scanOperationData.removeAll() - optOutOperationData.removeAll() + scanJobData.removeAll() + optOutJobData.removeAll() lastPreferredRunDateOnScan = nil } @@ -565,12 +565,12 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { } - func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanOperationData? { - scanOperationData.first + func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanJobData? { + scanJobData.first } - func fetchAllScans() throws -> [ScanOperationData] { - return scanOperationData + func fetchAllScans() throws -> [ScanJobData] { + return scanJobData } func save(brokerId: Int64, profileQueryId: Int64, extractedProfile: ExtractedProfile, lastRunDate: Date?, preferredRunDate: Date?) throws { @@ -585,16 +585,16 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { } - func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutOperationData? { - optOutOperationData.first + func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutJobData? { + optOutJobData.first } - func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutOperationData] { - return optOutOperationData + func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutJobData] { + return optOutJobData } - func fetchAllOptOuts() throws -> [OptOutOperationData] { - return optOutOperationData + func fetchAllOptOuts() throws -> [OptOutJobData] { + return optOutJobData } func save(historyEvent: HistoryEvent, brokerId: Int64, profileQueryId: Int64) throws { @@ -739,7 +739,7 @@ final class MockDatabase: DataBrokerProtectionRepository { wasDeleteProfileDataCalled = true } - func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { + func saveOptOutJob(optOut: OptOutJobData, extractedProfile: ExtractedProfile) throws { wasSaveOptOutOperationCalled = true } @@ -751,9 +751,9 @@ final class MockDatabase: DataBrokerProtectionRepository { } if let lastHistoryEventToReturn = self.lastHistoryEventToReturn { - let scanOperationData = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, historyEvents: [lastHistoryEventToReturn]) + let scanJobData = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, historyEvents: [lastHistoryEventToReturn]) - return BrokerProfileQueryData(dataBroker: .mock, profileQuery: .mock, scanOperationData: scanOperationData) + return BrokerProfileQueryData(dataBroker: .mock, profileQuery: .mock, scanJobData: scanJobData) } else { return nil } @@ -960,3 +960,69 @@ final class MockDataBrokerProtectionBackendServicePixels: DataBrokerProtectionBa statusCode = nil } } + +final class MockRunnerProvider: JobRunnerProvider { + + func getJobRunner() -> any WebJobRunner { + MockWebJobRunner() + } +} + +final class MockPixelHandler: EventMapping { + + init() { + super.init { event, _, _, _ in } + } +} + +extension ProfileQuery { + + static var mock: ProfileQuery { + .init(id: 1, firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) + } + + static var mockWithoutId: ProfileQuery { + .init(firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) + } +} + +extension ScanJobData { + + static var mock: ScanJobData { + .init( + brokerId: 1, + profileQueryId: 1, + historyEvents: [HistoryEvent]() + ) + } + + static func mockWith(historyEvents: [HistoryEvent]) -> ScanJobData { + ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) + } + + static func mock(withBrokerId brokerId: Int64) -> ScanJobData { + .init( + brokerId: brokerId, + profileQueryId: 1, + historyEvents: [HistoryEvent]() + ) + } +} + +extension DataBroker { + + static func mock(withId id: Int64) -> DataBroker { + DataBroker( + id: id, + name: "Test broker", + url: "testbroker.com", + steps: [Step](), + version: "1.0", + schedulingConfig: DataBrokerScheduleConfig( + retryError: 0, + confirmOptOutScan: 0, + maintenanceScan: 0 + ) + ) + } +} From 74e390d2a0f5e95bc7dde4d9e93c29d6a5a85882 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Thu, 9 May 2024 21:44:15 +0100 Subject: [PATCH 02/42] Rework DBP XPC interface (#2758) Task/Issue URL: https://app.asana.com/0/0/1207231867963535/f Tech Design URL: https://app.asana.com/0/0/1207198317317333/f CC: **Description**: 1. Moves the existing XPC interface ("DataBrokerProtectionScheduler") from the scheduler into the BackgroundAgentManager 2. Renames said manager to AgentManager 3. Renames the XPC interface to DataBrokerProtectionAgentInterface 4. Completely changes the composition of the interface, separating it out into two different protocols: DataBrokerProtectionAgentAppEvents, DataBrokerProtectionAgentDebugCommands 5. As those protocols imply, changes the XPC interface methods to be events based, to move decision making from the main app to the agent. Except for the debug commands, which are now clearly separated as for debugging purposes only. 6. Removes two of the many XPC layers since they were operating solely as a passthrough (i.e. boilerplate for nothing) 7. Uses protocol composition to further cut down on the repetition in those layers 8. Changes LoginItemScheduler to rather than be a class, a protocol that inherits the AgentInterface 9. Removes existing completions from the XPC interface since they were unused 10. Replaces them with new completions that are solely for reporting the reliability of XPC It still needs the pixels for #9, including clean up of the old pixels **Steps to test this PR**: 1. Test DBP still works, particularly removing the login item, entirely and starting fresh (since when testing at one point I managed to break that) 2. Otherwise don't test too much since this PR is not intended to stand alone/be merged into main as is. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 30 ++-- DuckDuckGo/DBP/DBPHomeViewController.swift | 2 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 42 +----- .../DataBrokerProtectionFeatureDisabler.swift | 10 +- ...taBrokerProtectionLoginItemInterface.swift | 101 +++++++++++++ ...taBrokerProtectionLoginItemScheduler.swift | 93 ------------ .../DBP/DataBrokerProtectionManager.swift | 11 +- ...=> DataBrokerProtectionAgentManager.swift} | 98 ++++++++++--- ...kDuckGoDBPBackgroundAgentAppDelegate.swift | 4 +- .../IPCServiceManager.swift | 129 ---------------- .../DataBrokerProtectionDataManager.swift | 5 +- .../DataBrokerProtectionAgentInterface.swift | 81 ++++++++++ .../IPC/DataBrokerProtectionIPCClient.swift | 138 ++++++------------ .../DataBrokerProtectionIPCScheduler.swift | 74 ---------- .../IPC/DataBrokerProtectionIPCServer.swift | 129 ++++++---------- .../Model/DBPUIViewModel.swift | 18 +-- .../DataBrokerProtectionNoOpScheduler.swift | 43 ------ .../DataBrokerProtectionProcessor.swift | 14 +- .../DataBrokerProtectionScheduler.swift | 104 +------------ .../DataBrokerProtectionViewController.swift | 6 +- 20 files changed, 396 insertions(+), 736 deletions(-) create mode 100644 DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift delete mode 100644 DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift rename DuckDuckGoDBPBackgroundAgent/{DataBrokerProtectionBackgroundManager.swift => DataBrokerProtectionAgentManager.swift} (60%) delete mode 100644 DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a5a233c122..e26e8314b9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -162,12 +162,11 @@ 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 3158B14D2B0BF74D00AF130C /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */; }; + 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.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 */; }; 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */; }; - 315A023D2B64216B00BFA577 /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 315A023F2B6421AE00BFA577 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 315A023E2B6421AE00BFA577 /* Networking */; }; 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */; }; 3168506D2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; @@ -209,7 +208,7 @@ 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; - 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */; }; + 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */; }; 31EF1E832B63FFCA00E6DB17 /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; }; 31EF1E842B63FFD100E6DB17 /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */; }; @@ -1542,7 +1541,6 @@ 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; - 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; @@ -1691,8 +1689,8 @@ 9D9AE9202AAA3B450026E7DC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */; }; 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9172AAA3B450026E7DC /* UserText.swift */; }; 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9172AAA3B450026E7DC /* UserText.swift */; }; - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */; }; - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */; }; + 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */; }; + 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */; }; 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; @@ -3296,7 +3294,7 @@ 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; 7B5291882A1697680022E406 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeveloperID.xcconfig; sourceTree = ""; }; - 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemScheduler.swift; sourceTree = ""; }; + 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemInterface.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 = ""; }; @@ -3322,7 +3320,6 @@ 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; - 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPCServiceManager.swift; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; @@ -3447,7 +3444,7 @@ 9D9AE9182AAA3B450026E7DC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D9AE9192AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoDBPBackgroundAgentAppStore.entitlements; sourceTree = ""; }; 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoDBPBackgroundAgent.entitlements; sourceTree = ""; }; - 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; + 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAgentManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; @@ -4531,7 +4528,7 @@ 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, - 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */, + 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */, 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */, 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, @@ -6241,8 +6238,7 @@ isa = PBXGroup; children = ( 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, - 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, - 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, + 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */, 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, @@ -10019,7 +10015,7 @@ 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, B6ABD0CB2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, - 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, + 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, B6B140892ABDBCC1004F8E85 /* HoverTrackingArea.swift in Sources */, 3706FC36293F65D500E42796 /* LongPressButton.swift in Sources */, 3706FC37293F65D500E42796 /* CoreDataStore.swift in Sources */, @@ -10719,10 +10715,9 @@ 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, + 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, - 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10733,10 +10728,9 @@ 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, + 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, - 315A023D2B64216B00BFA577 /* IPCServiceManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10929,7 +10923,7 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, + 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index cf9e05e900..37691d9cac 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -61,7 +61,7 @@ final class DBPHomeViewController: NSViewController { featureToggles: features) return DataBrokerProtectionViewController( - scheduler: dataBrokerProtectionManager.scheduler, + agentInterface: dataBrokerProtectionManager.loginItemInterface, dataManager: dataBrokerProtectionManager.dataManager, privacyConfig: privacyConfigurationManager, prefs: prefs, diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index b22ee8eac2..5882131727 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -208,57 +208,21 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running queued operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.runQueuedOperations(showWebView: showWebView) { errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Queued operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Queued operations finished, operation errors count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } else { - os_log("Queued operations finished", log: .dataBrokerProtection) - } - } + DataBrokerProtectionManager.shared.loginItemInterface.runQueuedOperations(showWebView: showWebView) } @objc private func runScanOperations(_ sender: NSMenuItem) { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView, startTime: Date()) { errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("scan operations finished, operation errors count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } else { - os_log("Scan operations finished", log: .dataBrokerProtection) - } - } + DataBrokerProtectionManager.shared.loginItemInterface.startManualScan(showWebView: showWebView) } @objc private func runOptoutOperations(_ sender: NSMenuItem) { os_log("Running Optout operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.optOutAllBrokers(showWebView: showWebView) { errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Optout operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Optout operations finished, operation errors count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } else { - os_log("Optout operations finished", log: .dataBrokerProtection) - } - } + DataBrokerProtectionManager.shared.loginItemInterface.runAllOptOuts(showWebView: showWebView) } @objc private func backgroundAgentRestart() { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index 83489b21bb..8d5ea0401a 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -31,20 +31,18 @@ protocol DataBrokerProtectionFeatureDisabling { } struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling { - private let scheduler: DataBrokerProtectionLoginItemScheduler + private let loginItemInterface: DataBrokerProtectionLoginItemInterface private let dataManager: InMemoryDataCacheDelegate - init(scheduler: DataBrokerProtectionLoginItemScheduler = DataBrokerProtectionManager.shared.scheduler, + init(loginItemInterface: DataBrokerProtectionLoginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface, dataManager: InMemoryDataCacheDelegate = DataBrokerProtectionManager.shared.dataManager) { self.dataManager = dataManager - self.scheduler = scheduler + self.loginItemInterface = loginItemInterface } func disableAndDelete() { if !DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist { - scheduler.stopScheduler() - - scheduler.disableLoginItem() + loginItemInterface.disableLoginItem() do { try dataManager.removeAllData() diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift new file mode 100644 index 0000000000..87652a1aa6 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift @@ -0,0 +1,101 @@ +// +// DataBrokerProtectionLoginItemInterface.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. +// + +#if DBP + +import Foundation +import DataBrokerProtection +import Common + +protocol DataBrokerProtectionLoginItemInterface: DataBrokerProtectionAgentInterface { + func disableLoginItem() + func enableLoginItem() +} + +/// Launches a login item and then communicates with it through IPC +/// +final class DefaultDataBrokerProtectionLoginItemInterface { + private let ipcClient: DataBrokerProtectionIPCClient + private let loginItemsManager: LoginItemsManager + + init(ipcClient: DataBrokerProtectionIPCClient, loginItemsManager: LoginItemsManager = .init()) { + self.ipcClient = ipcClient + self.loginItemsManager = loginItemsManager + } +} + +extension DefaultDataBrokerProtectionLoginItemInterface: DataBrokerProtectionLoginItemInterface { + + // MARK: - Login Item Management + + func disableLoginItem() { + DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerDisableLoginItemDaily, frequency: .daily) + loginItemsManager.disableLoginItems([.dbpBackgroundAgent]) + } + + func enableLoginItem() { + DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerEnableLoginItemDaily, frequency: .daily) + loginItemsManager.enableLoginItems([.dbpBackgroundAgent], log: .dbp) + } + + // MARK: - DataBrokerProtectionAgentInterface + // MARK: - DataBrokerProtectionAgentAppEvents + + func profileSaved() { + enableLoginItem() + ipcClient.profileSaved { error in + // TODO + } + } + + func dataDeleted() { + ipcClient.dataDeleted { error in + // TODO + } + } + + func appLaunched() { + ipcClient.appLaunched { error in + // TODO + } + } + + // MARK: - DataBrokerProtectionAgentDebugCommands + + func openBrowser(domain: String) { + ipcClient.openBrowser(domain: domain) + } + + func startManualScan(showWebView: Bool) { + ipcClient.startManualScan(showWebView: showWebView) + } + + func runQueuedOperations(showWebView: Bool) { + ipcClient.runQueuedOperations(showWebView: showWebView) + } + + func runAllOptOuts(showWebView: Bool) { + ipcClient.runAllOptOuts(showWebView: showWebView) + } + + func getDebugMetadata() async -> DataBrokerProtection.DBPBackgroundAgentMetadata? { + return await ipcClient.getDebugMetadata() + } +} + +#endif diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift deleted file mode 100644 index 50b75e9fac..0000000000 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// DataBrokerProtectionLoginItemScheduler.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. -// - -#if DBP - -import Foundation -import DataBrokerProtection -import Common - -/// A scheduler that launches a login item and the communicates with it through an IPC scheduler. -/// -final class DataBrokerProtectionLoginItemScheduler { - private let ipcScheduler: DataBrokerProtectionIPCScheduler - private let loginItemsManager: LoginItemsManager - - init(ipcScheduler: DataBrokerProtectionIPCScheduler, loginItemsManager: LoginItemsManager = .init()) { - self.ipcScheduler = ipcScheduler - self.loginItemsManager = loginItemsManager - } - - // MARK: - Login Item Management - - func disableLoginItem() { - DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerDisableLoginItemDaily, frequency: .daily) - loginItemsManager.disableLoginItems([.dbpBackgroundAgent]) - } - - func enableLoginItem() { - DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerEnableLoginItemDaily, frequency: .daily) - loginItemsManager.enableLoginItems([.dbpBackgroundAgent], log: .dbp) - } -} - -extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler { - var status: DataBrokerProtection.DataBrokerProtectionSchedulerStatus { - ipcScheduler.status - } - - var statusPublisher: Published.Publisher { - ipcScheduler.statusPublisher - } - - func startManualScan(showWebView: Bool, - startTime: Date, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - enableLoginItem() - ipcScheduler.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) - } - - func startScheduler(showWebView: Bool) { - enableLoginItem() - ipcScheduler.startScheduler(showWebView: showWebView) - } - - func stopScheduler() { - ipcScheduler.stopScheduler() - } - - func optOutAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - ipcScheduler.optOutAllBrokers(showWebView: showWebView, completion: completion) - } - - func runAllOperations(showWebView: Bool) { - ipcScheduler.runAllOperations(showWebView: showWebView) - } - - func runQueuedOperations(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - ipcScheduler.runQueuedOperations(showWebView: showWebView, completion: completion) - } - - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - ipcScheduler.getDebugMetadata(completion: completion) - } -} - -#endif diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f3b8705674..41844bb3cc 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -48,11 +48,8 @@ public final class DataBrokerProtectionManager { loginItemStatusChecker: loginItemStatusChecker) }() - lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { - - let ipcScheduler = DataBrokerProtectionIPCScheduler(ipcClient: ipcClient) - - return DataBrokerProtectionLoginItemScheduler(ipcScheduler: ipcScheduler) + lazy var loginItemInterface: DataBrokerProtectionLoginItemInterface = { + return DefaultDataBrokerProtectionLoginItemInterface(ipcClient: ipcClient) }() private init() { @@ -74,14 +71,14 @@ public final class DataBrokerProtectionManager { extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { public func dataBrokerProtectionDataManagerDidUpdateData() { - scheduler.startScheduler() + loginItemInterface.profileSaved() let dbpDateStore = DefaultWaitlistActivationDateStore(source: .dbp) dbpDateStore.setActivationDateIfNecessary() } public func dataBrokerProtectionDataManagerDidDeleteData() { - scheduler.stopScheduler() + loginItemInterface.dataDeleted() } } diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionAgentManager.swift similarity index 60% rename from DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift rename to DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionAgentManager.swift index 2bdc06947a..2c43ba5592 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionAgentManager.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionBackgroundManager.swift +// DataBrokerProtectionAgentManager.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,9 +22,9 @@ import BrowserServicesKit import DataBrokerProtection import PixelKit -public final class DataBrokerProtectionBackgroundManager { +public final class DataBrokerProtectionAgentManager { - static let shared = DataBrokerProtectionBackgroundManager() + static let shared = DataBrokerProtectionAgentManager() private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() @@ -32,14 +32,19 @@ public final class DataBrokerProtectionBackgroundManager { private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() private let redeemUseCase: DataBrokerProtectionRedeemUseCase private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() + private lazy var browserWindowManager = BrowserWindowManager() - private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) + private lazy var ipcServer: DataBrokerProtectionIPCServer = { + let server = DataBrokerProtectionIPCServer(machServiceName: Bundle.main.bundleIdentifier!) + server.serverDelegate = self + return server + }() lazy var dataManager: DataBrokerProtectionDataManager = { DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) }() - lazy var scheduler: DataBrokerProtectionScheduler = { + lazy var scheduler: DefaultDataBrokerProtectionScheduler = { let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, @@ -72,10 +77,10 @@ public final class DataBrokerProtectionBackgroundManager { private init() { self.redeemUseCase = RedeemUseCase(authenticationService: authenticationService, authenticationRepository: authenticationRepository) - _ = ipcServiceManager + ipcServer.activate() } - public func runOperationsAndStartSchedulerIfPossible() { + public func agentFinishedLaunching() { pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossible) do { @@ -90,24 +95,71 @@ public final class DataBrokerProtectionBackgroundManager { return } - scheduler.runQueuedOperations(showWebView: false) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), error: %{public}@", - log: .dataBrokerProtection, - oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, - functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - return - } - + scheduler.runQueuedOperations(showWebView: false) { [weak self] _ in self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler) self?.scheduler.startScheduler() } } } + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { + + public func profileSaved() { + scheduler.startManualScan(startTime: Date()) { _ in + + } + } + + public func dataDeleted() { + scheduler.stopScheduler() + } + + public func appLaunched() { + scheduler.runQueuedOperations() + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentDebugCommands { + public func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } + + public func startManualScan(showWebView: Bool) { + scheduler.startManualScan(startTime: Date()) { _ in + + } + } + + public func runQueuedOperations(showWebView: Bool) { + scheduler.runQueuedOperations(showWebView: showWebView) + } + + public func runAllOptOuts(showWebView: Bool) { + scheduler.optOutAllBrokers(showWebView: showWebView) { _ in + + } + } + + public func getDebugMetadata() async -> DataBrokerProtection.DBPBackgroundAgentMetadata? { + + if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { + + return DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", + isAgentRunning: scheduler.status == .running, + agentSchedulerState: scheduler.status.toString, + lastSchedulerSessionStartTimestamp: scheduler.lastSchedulerSessionStartTimestamp?.timeIntervalSince1970) + } else { + return DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", + isAgentRunning: scheduler.status == .running, + agentSchedulerState: scheduler.status.toString, + lastSchedulerSessionStartTimestamp: scheduler.lastSchedulerSessionStartTimestamp?.timeIntervalSince1970) + } + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentInterface { + +} diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 0a3e777138..f1b42dad3a 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -85,8 +85,8 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) - let manager = DataBrokerProtectionBackgroundManager.shared - manager.runOperationsAndStartSchedulerIfPossible() + let manager = DataBrokerProtectionAgentManager.shared + manager.agentFinishedLaunching() setupStatusBarMenu() } diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift deleted file mode 100644 index 8c39f28e07..0000000000 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// IPCServiceManager.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 Combine -import Common -import DataBrokerProtection -import Foundation -import PixelKit - -/// Manages the IPC service for the Agent app -/// -/// This class will handle all interactions between IPC requests and the classes those requests -/// demand interaction with. -/// -final class IPCServiceManager { - private var browserWindowManager: BrowserWindowManager - private let ipcServer: DataBrokerProtectionIPCServer - private let scheduler: DataBrokerProtectionScheduler - private let pixelHandler: EventMapping - private var cancellables = Set() - - init(ipcServer: DataBrokerProtectionIPCServer = .init(machServiceName: Bundle.main.bundleIdentifier!), - scheduler: DataBrokerProtectionScheduler, - pixelHandler: EventMapping) { - - self.ipcServer = ipcServer - self.scheduler = scheduler - self.pixelHandler = pixelHandler - - browserWindowManager = BrowserWindowManager() - - ipcServer.serverDelegate = self - ipcServer.activate() - } - - private func subscribeToSchedulerStatusChanges() { - scheduler.statusPublisher - .subscribe(on: DispatchQueue.main) - .sink { [weak self] status in - self?.ipcServer.schedulerStatusChanges(status) - } - .store(in: &cancellables) - } -} - -extension IPCServiceManager: IPCServerInterface { - - func register() { - // When a new client registers, send the last known status - ipcServer.schedulerStatusChanges(scheduler.status) - } - - func startScheduler(showWebView: Bool) { - pixelHandler.fire(.ipcServerStartSchedulerReceivedByAgent) - scheduler.startScheduler(showWebView: showWebView) - } - - func stopScheduler() { - pixelHandler.fire(.ipcServerStopSchedulerReceivedByAgent) - scheduler.stopScheduler() - } - - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerOptOutAllBrokers) - scheduler.optOutAllBrokers(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerOptOutAllBrokersCompletion(error: errors?.oneTimeError)) - completion(errors) - } - } - - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerScanAllBrokersReceivedByAgent) - scheduler.startManualScan(showWebView: showWebView, startTime: startTime) { errors in - if let error = errors?.oneTimeError { - switch error { - case DataBrokerProtectionSchedulerError.operationsInterrupted: - self.pixelHandler.fire(.ipcServerScanAllBrokersInterruptedOnAgent) - default: - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletedOnAgentWithError(error: error)) - } - } else { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletedOnAgentWithoutError) - } - completion(errors) - } - } - - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerRunQueuedOperations) - scheduler.runQueuedOperations(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) - completion(errors) - } - } - - func runAllOperations(showWebView: Bool) { - pixelHandler.fire(.ipcServerRunAllOperations) - scheduler.runAllOperations(showWebView: showWebView) - } - - func openBrowser(domain: String) { - Task { @MainActor in - browserWindowManager.show(domain: domain) - } - } - - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - scheduler.getDebugMetadata(completion: completion) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index b006af7433..5a33e63831 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -294,7 +294,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { } func startScanAndOptOut() -> Bool { - return scanDelegate?.startScan(startDate: Date()) ?? false + // This is now unusused as we decided the web UI shouldn't issue commands directly + // The background agent itself instead decides to start scans based on events + // This should be removed once we can remove it from the web side + return true } func getInitialScanState() async -> DBPUIInitialScanState { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift new file mode 100644 index 0000000000..3ef3bfc69f --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift @@ -0,0 +1,81 @@ +// +// DataBrokerProtectionAgentInterface.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public enum DataBrokerProtectionAgentInterfaceError: Error { + case loginItemDoesNotHaveNecessaryPermissions + case appInWrongDirectory + case operationsInterrupted +} + +@objc +public class DataBrokerProtectionAgentErrorCollection: NSObject, NSSecureCoding { + /* + This needs to be an NSObject (rather than a struct) so it can be represented in Objective C + and confrom to NSSecureCoding for the IPC layer. + */ + + private enum NSSecureCodingKeys { + static let oneTimeError = "oneTimeError" + static let operationErrors = "operationErrors" + } + + public let oneTimeError: Error? + public let operationErrors: [Error]? + + public init(oneTimeError: Error? = nil, operationErrors: [Error]? = nil) { + self.oneTimeError = oneTimeError + self.operationErrors = operationErrors + super.init() + } + + // MARK: - NSSecureCoding + + public static var supportsSecureCoding: Bool { + return true + } + + public func encode(with coder: NSCoder) { + coder.encode(oneTimeError, forKey: NSSecureCodingKeys.oneTimeError) + coder.encode(operationErrors, forKey: NSSecureCodingKeys.operationErrors) + } + + public required init?(coder: NSCoder) { + oneTimeError = coder.decodeObject(of: NSError.self, forKey: NSSecureCodingKeys.oneTimeError) + operationErrors = coder.decodeArrayOfObjects(ofClass: NSError.self, forKey: NSSecureCodingKeys.operationErrors) + } +} + +public protocol DataBrokerProtectionAgentAppEvents { + func profileSaved() + func dataDeleted() + func appLaunched() +} + +public protocol DataBrokerProtectionAgentDebugCommands { + func openBrowser(domain: String) + func startManualScan(showWebView: Bool) + func runQueuedOperations(showWebView: Bool) + func runAllOptOuts(showWebView: Bool) + func getDebugMetadata() async -> DBPBackgroundAgentMetadata? +} + +public protocol DataBrokerProtectionAgentInterface: AnyObject, DataBrokerProtectionAgentAppEvents, DataBrokerProtectionAgentDebugCommands { + +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 1f1d2451b2..564e33b1b5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -24,7 +24,6 @@ import XPCHelper /// This protocol describes the server-side IPC interface for controlling the tunnel /// public protocol IPCClientInterface: AnyObject { - func schedulerStatusChanges(_ status: DataBrokerProtectionSchedulerStatus) } public protocol DBPLoginItemStatusChecker { @@ -35,7 +34,6 @@ public protocol DBPLoginItemStatusChecker { /// This is the XPC interface with parameters that can be packed properly @objc protocol XPCClientInterface: NSObjectProtocol { - func schedulerStatusChanged(_ payload: Data) } public final class DataBrokerProtectionIPCClient: NSObject { @@ -47,15 +45,6 @@ public final class DataBrokerProtectionIPCClient: NSObject { let xpc: XPCClient - // MARK: - Scheduler Status - - @Published - private(set) public var schedulerStatus: DataBrokerProtectionSchedulerStatus = .idle - - public var schedulerStatusPublisher: Published.Publisher { - $schedulerStatus - } - // MARK: - Initializers public init(machServiceName: String, pixelHandler: EventMapping, loginItemStatusChecker: DBPLoginItemStatusChecker) { @@ -100,137 +89,94 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func startScheduler(showWebView: Bool) { - self.pixelHandler.fire(.ipcServerStartSchedulerCalledByApp) + // MARK: - DataBrokerProtectionAgentAppEvents + + public func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.startScheduler(showWebView: showWebView) + server.profileSaved(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerStartSchedulerXPCError(error: error)) + os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) } - public func stopScheduler() { - self.pixelHandler.fire(.ipcServerStopSchedulerCalledByApp) + public func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.stopScheduler() + server.dataDeleted(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerStopSchedulerXPCError(error: error)) + os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) } - public func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerOptOutAllBrokers) + public func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.optOutAllBrokers(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) - completion(errors) - } + server.appLaunched(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! }) } - public func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerScanAllBrokersCalledByApp) - - guard loginItemStatusChecker.doesHaveNecessaryPermissions() else { - self.pixelHandler.fire(.ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions) - let errors = DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.loginItemDoesNotHaveNecessaryPermissions) - completion(errors) - return - } - - guard loginItemStatusChecker.isInCorrectDirectory() else { - self.pixelHandler.fire(.ipcServerScanAllBrokersAttemptedToCallInWrongDirectory) - let errors = DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.appInWrongDirectory) - completion(errors) - return - } + // MARK: - DataBrokerProtectionAgentDebugCommands + public func openBrowser(domain: String) { xpc.execute(call: { server in - server.startManualScan(showWebView: showWebView, startTime: startTime) { errors in - if let error = errors?.oneTimeError { - let nsError = error as NSError - let interruptedError = DataBrokerProtectionSchedulerError.operationsInterrupted as NSError - if nsError.domain == interruptedError.domain, - nsError.code == interruptedError.code { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption) - } else { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: error)) - } - } else { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppWithoutError) - } - completion(errors) - } + server.openBrowser(domain: domain) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerScanAllBrokersXPCError(error: error)) - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! }) } - public func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerRunQueuedOperations) + public func startManualScan(showWebView: Bool) { xpc.execute(call: { server in - server.runQueuedOperations(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) - completion(errors) - } + server.startManualScan(showWebView: showWebView) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) - }) - } - - public func runAllOperations(showWebView: Bool) { - self.pixelHandler.fire(.ipcServerRunAllOperations) - xpc.execute(call: { server in - server.runAllOperations(showWebView: showWebView) - }, xpcReplyErrorHandler: { _ in + os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) } - public func openBrowser(domain: String) { - self.pixelHandler.fire(.ipcServerRunAllOperations) + public func runQueuedOperations(showWebView: Bool) { xpc.execute(call: { server in - server.openBrowser(domain: domain) + server.runQueuedOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! - }) - } + }) } - public func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + public func runAllOptOuts(showWebView: Bool) { xpc.execute(call: { server in - server.getDebugMetadata(completion: completion) + server.runAllOptOuts(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") - completion(nil) + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! }) } + + public func getDebugMetadata() async -> DBPBackgroundAgentMetadata? { + await withCheckedContinuation { continuation in + xpc.execute(call: { server in + server.getDebugMetadata { metaData in + continuation.resume(returning: metaData) + } + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + continuation.resume(returning: nil) + }) + } + } } // MARK: - Incoming communication from the server extension DataBrokerProtectionIPCClient: XPCClientInterface { - func schedulerStatusChanged(_ payload: Data) { - guard let status = try? JSONDecoder().decode(DataBrokerProtectionSchedulerStatus.self, from: payload) else { - - return - } - - schedulerStatus = status - } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift deleted file mode 100644 index ea73c3e1a0..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// DataBrokerProtectionIPCScheduler.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 Common - -/// A scheduler that works through IPC to request the scheduling to a different process -/// -public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionScheduler { - private let ipcClient: DataBrokerProtectionIPCClient - - public init(ipcClient: DataBrokerProtectionIPCClient) { - self.ipcClient = ipcClient - } - - public var status: DataBrokerProtectionSchedulerStatus { - ipcClient.schedulerStatus - } - - public var statusPublisher: Published.Publisher { - ipcClient.schedulerStatusPublisher - } - - public func startScheduler(showWebView: Bool) { - ipcClient.startScheduler(showWebView: showWebView) - } - - public func stopScheduler() { - ipcClient.stopScheduler() - } - - public func optOutAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - let completion = completion ?? { _ in } - ipcClient.optOutAllBrokers(showWebView: showWebView, completion: completion) - } - - public func startManualScan(showWebView: Bool, - startTime: Date, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - let completion = completion ?? { _ in } - ipcClient.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) - } - - public func runQueuedOperations(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - let completion = completion ?? { _ in } - ipcClient.runQueuedOperations(showWebView: showWebView, completion: completion) - } - - public func runAllOperations(showWebView: Bool) { - ipcClient.runAllOperations(showWebView: showWebView) - } - - public func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - ipcClient.getDebugMetadata(completion: completion) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 7f4ae3e840..53e260cdcf 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -35,10 +35,10 @@ public final class DBPBackgroundAgentMetadata: NSObject, NSSecureCoding { let agentSchedulerState: String let lastSchedulerSessionStartTimestamp: Double? - init(backgroundAgentVersion: String, - isAgentRunning: Bool, - agentSchedulerState: String, - lastSchedulerSessionStartTimestamp: Double?) { + public init(backgroundAgentVersion: String, + isAgentRunning: Bool, + agentSchedulerState: String, + lastSchedulerSessionStartTimestamp: Double?) { self.backgroundAgentVersion = backgroundAgentVersion self.isAgentRunning = isAgentRunning self.agentSchedulerState = agentSchedulerState @@ -75,40 +75,18 @@ public final class DBPBackgroundAgentMetadata: NSObject, NSSecureCoding { /// This protocol describes the server-side IPC interface for controlling the tunnel /// -public protocol IPCServerInterface: AnyObject { +public protocol IPCServerInterface: AnyObject, DataBrokerProtectionAgentDebugCommands { /// Registers a connection with the server. /// /// This is the point where the server will start sending status updates to the client. /// func register() - // MARK: - Scheduler + // MARK: - DataBrokerProtectionAgentAppEvents - /// Start the scheduler - /// - func startScheduler(showWebView: Bool) - - /// Stop the scheduler - /// - func stopScheduler() - - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runAllOperations(showWebView: Bool) - - // MARK: - Debugging Features - - /// Opens a browser window with the specified domain - /// - func openBrowser(domain: String) - - /// Returns background agent metadata for debugging purposes - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) + func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) } /// This protocol describes the server-side XPC interface. @@ -124,31 +102,21 @@ protocol XPCServerInterface { /// func register() - // MARK: - Scheduler - - /// Start the scheduler - /// - func startScheduler(showWebView: Bool) - - /// Stop the scheduler - /// - func stopScheduler() + // MARK: - DataBrokerProtectionAgentAppEvents - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runAllOperations(showWebView: Bool) + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) + func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) - // MARK: - Debugging Features + // MARK: - DataBrokerProtectionAgentDebugCommands /// Opens a browser window with the specified domain /// func openBrowser(domain: String) + func startManualScan(showWebView: Bool) + func runQueuedOperations(showWebView: Bool) + func runAllOptOuts(showWebView: Bool) func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } @@ -157,7 +125,7 @@ public final class DataBrokerProtectionIPCServer { /// The delegate. /// - public weak var serverDelegate: IPCServerInterface? + public weak var serverDelegate: DataBrokerProtectionAgentInterface? public init(machServiceName: String) { let clientInterface = NSXPCInterface(with: XPCClientInterface.self) @@ -179,62 +147,55 @@ public final class DataBrokerProtectionIPCServer { // MARK: - Outgoing communication to the clients extension DataBrokerProtectionIPCServer: IPCClientInterface { - - public func schedulerStatusChanges(_ status: DataBrokerProtectionSchedulerStatus) { - let payload: Data - - do { - payload = try JSONEncoder().encode(status) - } catch { - return - } - - xpc.forEachClient { client in - client.schedulerStatusChanged(payload) - } - } } // MARK: - Incoming communication from a client extension DataBrokerProtectionIPCServer: XPCServerInterface { + func register() { - serverDelegate?.register() + } - func startScheduler(showWebView: Bool) { - serverDelegate?.startScheduler(showWebView: showWebView) + // MARK: - DataBrokerProtectionAgentAppEvents + + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + xpcMessageReceivedCompletion(nil) + serverDelegate?.profileSaved() } - func stopScheduler() { - serverDelegate?.stopScheduler() + func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + xpcMessageReceivedCompletion(nil) + serverDelegate?.dataDeleted() } - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.optOutAllBrokers(showWebView: showWebView, completion: completion) + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + xpcMessageReceivedCompletion(nil) + serverDelegate?.appLaunched() } - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) + // MARK: - DataBrokerProtectionAgentDebugCommands + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) } - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.runQueuedOperations(showWebView: showWebView, completion: completion) + func startManualScan(showWebView: Bool) { + serverDelegate?.startManualScan(showWebView: showWebView) } - func runAllOperations(showWebView: Bool) { - serverDelegate?.runAllOperations(showWebView: showWebView) + func runQueuedOperations(showWebView: Bool) { + serverDelegate?.runQueuedOperations(showWebView: showWebView) } - func openBrowser(domain: String) { - serverDelegate?.openBrowser(domain: domain) + func runAllOptOuts(showWebView: Bool) { + serverDelegate?.runAllOptOuts(showWebView: showWebView) } func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - serverDelegate?.getDebugMetadata(completion: completion) + Task { + let metaData = await serverDelegate?.getDebugMetadata() + completion(metaData) + } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index a43f6d9ae0..b66973d3d1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -23,14 +23,13 @@ import BrowserServicesKit import Common protocol DBPUIScanOps: AnyObject { - func startScan(startDate: Date) -> Bool func updateCacheWithCurrentScans() async func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? } final class DBPUIViewModel { private let dataManager: DataBrokerProtectionDataManaging - private let scheduler: DataBrokerProtectionScheduler + private let agentInterface: DataBrokerProtectionAgentInterface private let privacyConfig: PrivacyConfigurationManaging? private let prefs: ContentScopeProperties? @@ -40,13 +39,13 @@ final class DBPUIViewModel { private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() init(dataManager: DataBrokerProtectionDataManaging, - scheduler: DataBrokerProtectionScheduler, + agentInterface: DataBrokerProtectionAgentInterface, webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable, privacyConfig: PrivacyConfigurationManaging? = nil, prefs: ContentScopeProperties? = nil, webView: WKWebView? = nil) { self.dataManager = dataManager - self.scheduler = scheduler + self.agentInterface = agentInterface self.webUISettings = webUISettings self.privacyConfig = privacyConfig self.prefs = prefs @@ -74,9 +73,8 @@ final class DBPUIViewModel { } extension DBPUIViewModel: DBPUIScanOps { - func startScan(startDate: Date) -> Bool { - scheduler.startManualScan(startTime: startDate) - return true + func profileSaved() { + agentInterface.profileSaved() } func updateCacheWithCurrentScans() async { @@ -89,10 +87,6 @@ extension DBPUIViewModel: DBPUIScanOps { } func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? { - return await withCheckedContinuation { continuation in - scheduler.getDebugMetadata { metadata in - continuation.resume(returning: metadata) - } - } + return await agentInterface.getDebugMetadata() } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift deleted file mode 100644 index 41e02edaca..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// DataBrokerProtectionNoOpScheduler.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 - -/// Convenience class for SwiftUI previews. -/// -/// Do not use this for any production code. -/// -final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { - - private(set) public var status: DataBrokerProtectionSchedulerStatus = .idle - - private var internalStatusPublisher: Published = .init(initialValue: .idle) - - public var statusPublisher: Published.Publisher { - internalStatusPublisher.projectedValue - } - - func startScheduler(showWebView: Bool) { } - func stopScheduler() { } - func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func runQueuedOperations(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func runAllOperations(showWebView: Bool) { } - func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index b8cf875adf..5cc6c42ea0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -51,7 +51,7 @@ final class DataBrokerProtectionProcessor { // MARK: - Public functions func startManualScans(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() runOperations(operationType: .manualScan, @@ -69,7 +69,7 @@ final class DataBrokerProtectionProcessor { } func runAllOptOutOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() runOperations(operationType: .optOut, priorityDate: nil, @@ -80,7 +80,7 @@ final class DataBrokerProtectionProcessor { } func runQueuedOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil ) { runOperations(operationType: .all, priorityDate: Date(), showWebView: showWebView) { errors in @@ -90,7 +90,7 @@ final class DataBrokerProtectionProcessor { } func runAllOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil ) { runOperations(operationType: .all, priorityDate: nil, showWebView: showWebView) { errors in @@ -107,7 +107,7 @@ final class DataBrokerProtectionProcessor { private func runOperations(operationType: OperationType, priorityDate: Date?, showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { + completion: @escaping ((DataBrokerProtectionAgentErrorCollection?) -> Void)) { self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsFor(operationType) // Before running new operations we check if there is any updates to the broker files. @@ -144,14 +144,14 @@ final class DataBrokerProtectionProcessor { } catch { os_log("DataBrokerProtectionProcessor error: runOperations, error: %{public}@", log: .error, error.localizedDescription) operationQueue.addBarrierBlock { - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) + completion(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) } return } operationQueue.addBarrierBlock { let operationErrors = operations.compactMap { $0.error } - let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionSchedulerErrorCollection(operationErrors: operationErrors) : nil + let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionAgentErrorCollection(operationErrors: operationErrors) : nil completion(errorCollection) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index fd0b66bbdc..d9ac509201 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -27,83 +27,7 @@ public enum DataBrokerProtectionSchedulerStatus: Codable { case running } -public enum DataBrokerProtectionSchedulerError: Error { - case loginItemDoesNotHaveNecessaryPermissions - case appInWrongDirectory - case operationsInterrupted -} - -@objc -public class DataBrokerProtectionSchedulerErrorCollection: NSObject, NSSecureCoding { - /* - This needs to be an NSObject (rather than a struct) so it can be represented in Objective C - and confrom to NSSecureCoding for the IPC layer. - */ - - private enum NSSecureCodingKeys { - static let oneTimeError = "oneTimeError" - static let operationErrors = "operationErrors" - } - - public let oneTimeError: Error? - public let operationErrors: [Error]? - - public init(oneTimeError: Error? = nil, operationErrors: [Error]? = nil) { - self.oneTimeError = oneTimeError - self.operationErrors = operationErrors - super.init() - } - - // MARK: - NSSecureCoding - - public static var supportsSecureCoding: Bool { - return true - } - - public func encode(with coder: NSCoder) { - coder.encode(oneTimeError, forKey: NSSecureCodingKeys.oneTimeError) - coder.encode(operationErrors, forKey: NSSecureCodingKeys.operationErrors) - } - - public required init?(coder: NSCoder) { - oneTimeError = coder.decodeObject(of: NSError.self, forKey: NSSecureCodingKeys.oneTimeError) - operationErrors = coder.decodeArrayOfObjects(ofClass: NSError.self, forKey: NSSecureCodingKeys.operationErrors) - } -} - -public protocol DataBrokerProtectionScheduler { - - var status: DataBrokerProtectionSchedulerStatus { get } - var statusPublisher: Published.Publisher { get } - - func startScheduler(showWebView: Bool) - func stopScheduler() - - func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func runAllOperations(showWebView: Bool) - - /// Debug operations - - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) -} - -extension DataBrokerProtectionScheduler { - public func startScheduler() { - startScheduler(showWebView: false) - } - - public func runAllOperations() { - runAllOperations(showWebView: false) - } - - public func startManualScan(startTime: Date) { - startManualScan(showWebView: false, startTime: startTime, completion: nil) - } -} - -public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionScheduler { +public final class DefaultDataBrokerProtectionScheduler { private enum SchedulerCycle { // Arbitrary numbers for now @@ -140,7 +64,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public var statusPublisher: Published.Publisher { $status } - private var lastSchedulerSessionStartTimestamp: Date? + public var lastSchedulerSessionStartTimestamp: Date? private lazy var dataBrokerProcessor: DataBrokerProtectionProcessor = { @@ -253,7 +177,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } public func runQueuedOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { guard self.currentOperation != .manualScan else { os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) return @@ -281,7 +205,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public func startManualScan(showWebView: Bool = false, startTime: Date, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) let backgroundAgentManualScanStartTime = Date() stopScheduler() @@ -306,7 +230,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch if let errors = errors { if let oneTimeError = errors.oneTimeError { switch oneTimeError { - case DataBrokerProtectionSchedulerError.operationsInterrupted: + case DataBrokerProtectionAgentInterfaceError.operationsInterrupted: os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) default: os_log("Error during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) @@ -336,7 +260,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } public func optOutAllBrokers(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { guard self.currentOperation != .manualScan else { os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) @@ -361,23 +285,9 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch completion?(errors) }) } - - public func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { - if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { - completion(DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", - isAgentRunning: status == .running, - agentSchedulerState: status.toString, - lastSchedulerSessionStartTimestamp: lastSchedulerSessionStartTimestamp?.timeIntervalSince1970)) - } else { - completion(DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", - isAgentRunning: status == .running, - agentSchedulerState: status.toString, - lastSchedulerSessionStartTimestamp: lastSchedulerSessionStartTimestamp?.timeIntervalSince1970)) - } - } } -extension DataBrokerProtectionSchedulerStatus { +public extension DataBrokerProtectionSchedulerStatus { var toString: String { switch self { case .idle: diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index 4f26b33b8e..9a240074b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -26,7 +26,6 @@ import Combine final public class DataBrokerProtectionViewController: NSViewController { private let dataManager: DataBrokerProtectionDataManaging - private let scheduler: DataBrokerProtectionScheduler private var webView: WKWebView? private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable @@ -37,20 +36,19 @@ final public class DataBrokerProtectionViewController: NSViewController { private let openURLHandler: (URL?) -> Void private var reloadObserver: NSObjectProtocol? - public init(scheduler: DataBrokerProtectionScheduler, + public init(agentInterface: DataBrokerProtectionAgentInterface, dataManager: DataBrokerProtectionDataManaging, privacyConfig: PrivacyConfigurationManaging? = nil, prefs: ContentScopeProperties? = nil, webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable, openURLHandler: @escaping (URL?) -> Void) { - self.scheduler = scheduler self.dataManager = dataManager self.openURLHandler = openURLHandler self.webUISettings = webUISettings self.pixelHandler = DataBrokerProtectionPixelsHandler() self.webUIPixel = DataBrokerProtectionWebUIPixels(pixelHandler: pixelHandler) self.webUIViewModel = DBPUIViewModel(dataManager: dataManager, - scheduler: scheduler, + agentInterface: agentInterface, webUISettings: webUISettings, privacyConfig: privacyConfig, prefs: prefs, From e4c8e30080afece64ed559f7802867bac8c21ea4 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Tue, 14 May 2024 15:22:27 +0100 Subject: [PATCH 03/42] Implement Queue Manager for State & Interruption Management (#2757) Task/Issue URL: https://app.asana.com/0/0/1207231867963533/f Tech Design URL: https://app.asana.com/0/0/1207199754528649/f **Description**: This PR introduces `DataBrokerProtectionQueueManager` to manage state and logic relating to the `OperationQueue` which runs DBP operations. --- .../DBP/DataBrokerProtectionDebugMenu.swift | 12 +- ...taBrokerProtectionLoginItemInterface.swift | 8 +- .../DataBrokerProtectionPixelsHandler.swift | 6 +- .../DataBrokerProtectionAgentManager.swift | 16 +- .../DataBrokerRunCustomJSONViewModel.swift | 2 +- .../DataBrokerProtectionAgentInterface.swift | 4 +- .../IPC/DataBrokerProtectionIPCClient.swift | 8 +- .../IPC/DataBrokerProtectionIPCServer.swift | 12 +- .../Operations/DataBrokerJob.swift | 4 +- .../Operations/DataBrokerOperation.swift | 78 ++-- ...taBrokerProfileQueryOperationManager.swift | 10 +- .../DataBrokerProtectionBrokerUpdater.swift | 12 +- ...UseCase.swift => MismatchCalculator.swift} | 9 +- .../Pixels/DataBrokerProtectionPixels.swift | 46 +-- ...kerProtectionStageDurationCalculator.swift | 14 +- .../DataBrokerOperationsCreator.swift | 11 +- .../DataBrokerProtectionProcessor.swift | 162 -------- ...okerProtectionProcessorConfiguration.swift | 2 +- .../DataBrokerProtectionQueueManager.swift | 262 +++++++++++++ .../DataBrokerProtectionScheduler.swift | 103 +++--- .../DataBrokerOperationsCreatorTests.swift | 5 +- ...rotectionProcessorConfigurationTests.swift | 2 +- ...ataBrokerProtectionQueueManagerTests.swift | 350 ++++++++++++++++++ .../DataBrokerProtectionQueueModeTests.swift | 122 ++++++ .../DataBrokerProtectionUpdaterTests.swift | 16 +- .../MismatchCalculatorUseCaseTests.swift | 10 +- .../DataBrokerProtectionTests/Mocks.swift | 185 ++++++++- 27 files changed, 1113 insertions(+), 358 deletions(-) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/{MismatchCalculatorUseCase.swift => MismatchCalculator.swift} (92%) delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 5882131727..e34bac501c 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -105,7 +105,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Operations") { NSMenuItem(title: "Hidden WebView") { menuItem(withTitle: "Run queued operations", - action: #selector(DataBrokerProtectionDebugMenu.runQueuedOperations(_:)), + action: #selector(DataBrokerProtectionDebugMenu.startScheduledOperations(_:)), representedObject: false) menuItem(withTitle: "Run scan operations", @@ -119,7 +119,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Visible WebView") { menuItem(withTitle: "Run queued operations", - action: #selector(DataBrokerProtectionDebugMenu.runQueuedOperations(_:)), + action: #selector(DataBrokerProtectionDebugMenu.startScheduledOperations(_:)), representedObject: true) menuItem(withTitle: "Run scan operations", @@ -204,18 +204,18 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } } - @objc private func runQueuedOperations(_ sender: NSMenuItem) { + @objc private func startScheduledOperations(_ sender: NSMenuItem) { os_log("Running queued operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.loginItemInterface.runQueuedOperations(showWebView: showWebView) + DataBrokerProtectionManager.shared.loginItemInterface.startScheduledOperations(showWebView: showWebView) } @objc private func runScanOperations(_ sender: NSMenuItem) { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.loginItemInterface.startManualScan(showWebView: showWebView) + DataBrokerProtectionManager.shared.loginItemInterface.startImmediateOperations(showWebView: showWebView) } @objc private func runOptoutOperations(_ sender: NSMenuItem) { @@ -295,7 +295,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func forceBrokerJSONFilesUpdate() { - if let updater = DataBrokerProtectionBrokerUpdater.provide() { + if let updater = DefaultDataBrokerProtectionBrokerUpdater.provideForDebug() { updater.updateBrokers() } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift index 87652a1aa6..1ff520cac3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift @@ -81,12 +81,12 @@ extension DefaultDataBrokerProtectionLoginItemInterface: DataBrokerProtectionLog ipcClient.openBrowser(domain: domain) } - func startManualScan(showWebView: Bool) { - ipcClient.startManualScan(showWebView: showWebView) + func startImmediateOperations(showWebView: Bool) { + ipcClient.startImmediateOperations(showWebView: showWebView) } - func runQueuedOperations(showWebView: Bool) { - ipcClient.runQueuedOperations(showWebView: showWebView) + func startScheduledOperations(showWebView: Bool) { + ipcClient.startScheduledOperations(showWebView: showWebView) } func runAllOptOuts(showWebView: Bool) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 2f91fe97a0..37f9f41e29 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -39,7 +39,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double { 0.0 diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift index 3ef3bfc69f..392508440a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift @@ -70,8 +70,8 @@ public protocol DataBrokerProtectionAgentAppEvents { public protocol DataBrokerProtectionAgentDebugCommands { func openBrowser(domain: String) - func startManualScan(showWebView: Bool) - func runQueuedOperations(showWebView: Bool) + func startImmediateOperations(showWebView: Bool) + func startScheduledOperations(showWebView: Bool) func runAllOptOuts(showWebView: Bool) func getDebugMetadata() async -> DBPBackgroundAgentMetadata? } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 564e33b1b5..9f8b565bc1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -133,9 +133,9 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func startManualScan(showWebView: Bool) { + public func startImmediateOperations(showWebView: Bool) { xpc.execute(call: { server in - server.startManualScan(showWebView: showWebView) + server.startImmediateOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block @@ -143,9 +143,9 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func runQueuedOperations(showWebView: Bool) { + public func startScheduledOperations(showWebView: Bool) { xpc.execute(call: { server in - server.runQueuedOperations(showWebView: showWebView) + server.startScheduledOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 53e260cdcf..7213f09986 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -114,8 +114,8 @@ protocol XPCServerInterface { /// func openBrowser(domain: String) - func startManualScan(showWebView: Bool) - func runQueuedOperations(showWebView: Bool) + func startImmediateOperations(showWebView: Bool) + func startScheduledOperations(showWebView: Bool) func runAllOptOuts(showWebView: Bool) func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } @@ -180,12 +180,12 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { serverDelegate?.openBrowser(domain: domain) } - func startManualScan(showWebView: Bool) { - serverDelegate?.startManualScan(showWebView: showWebView) + func startImmediateOperations(showWebView: Bool) { + serverDelegate?.startImmediateOperations(showWebView: showWebView) } - func runQueuedOperations(showWebView: Bool) { - serverDelegate?.runQueuedOperations(showWebView: showWebView) + func startScheduledOperations(showWebView: Bool) { + serverDelegate?.startScheduledOperations(showWebView: showWebView) } func runAllOptOuts(showWebView: Bool) { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift index 993b02aa87..1be8f94200 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift @@ -199,7 +199,7 @@ extension DataBrokerJob { } private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { - if stageCalculator.isManualScan { + if stageCalculator.isImmediateOperation { let dataBrokerURL = self.query.dataBroker.url let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) @@ -207,7 +207,7 @@ extension DataBrokerJob { } func firePostLoadingDurationPixel(hasError: Bool) { - if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { + if stageCalculator.isImmediateOperation, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { let dataBrokerURL = self.query.dataBroker.url let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index c5032a2df3..8d76634d42 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -19,15 +19,9 @@ import Foundation import Common -enum OperationType { - case manualScan - case optOut - case all -} - protocol DataBrokerOperationDependencies { var database: DataBrokerProtectionRepository { get } - var brokerTimeInterval: TimeInterval { get } + var config: DataBrokerProtectionProcessorConfiguration { get } var runnerProvider: JobRunnerProvider { get } var notificationCenter: NotificationCenter { get } var pixelHandler: EventMapping { get } @@ -36,30 +30,36 @@ protocol DataBrokerOperationDependencies { struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { let database: DataBrokerProtectionRepository - let brokerTimeInterval: TimeInterval + var config: DataBrokerProtectionProcessorConfiguration let runnerProvider: JobRunnerProvider let notificationCenter: NotificationCenter let pixelHandler: EventMapping let userNotificationService: DataBrokerProtectionUserNotificationService } -final class DataBrokerOperation: Operation { +enum OperationType { + case scan + case optOut + case all +} + +protocol DataBrokerOperationErrorDelegate: AnyObject { + func dataBrokerOperationDidError(_ error: Error, withBrokerName brokerName: String?) +} - public var error: Error? +// swiftlint:disable explicit_non_final_class +class DataBrokerOperation: Operation { private let dataBrokerID: Int64 - private let database: DataBrokerProtectionRepository + private let operationType: OperationType + private let priorityDate: Date? // The date to filter and sort operations priorities + private let showWebView: Bool + private(set) weak var errorDelegate: DataBrokerOperationErrorDelegate? // Internal read-only to enable mocking + private let operationDependencies: DataBrokerOperationDependencies + private let id = UUID() private var _isExecuting = false private var _isFinished = false - private let brokerTimeInterval: TimeInterval? // The time in seconds to wait in-between operations - private let priorityDate: Date? // The date to filter and sort operations priorities - private let operationType: OperationType - private let notificationCenter: NotificationCenter - private let runner: WebJobRunner - private let pixelHandler: EventMapping - private let showWebView: Bool - private let userNotificationService: DataBrokerProtectionUserNotificationService deinit { os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) @@ -69,18 +69,15 @@ final class DataBrokerOperation: Operation { operationType: OperationType, priorityDate: Date? = nil, showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, operationDependencies: DataBrokerOperationDependencies) { self.dataBrokerID = dataBrokerID self.priorityDate = priorityDate self.operationType = operationType self.showWebView = showWebView - self.database = operationDependencies.database - self.brokerTimeInterval = operationDependencies.brokerTimeInterval - self.runner = operationDependencies.runnerProvider.getJobRunner() - self.notificationCenter = operationDependencies.notificationCenter - self.pixelHandler = operationDependencies.pixelHandler - self.userNotificationService = operationDependencies.userNotificationService + self.errorDelegate = errorDelegate + self.operationDependencies = operationDependencies super.init() } @@ -122,7 +119,7 @@ final class DataBrokerOperation: Operation { switch operationType { case .optOut: operationsData = brokerProfileQueriesData.flatMap { $0.optOutJobData } - case .manualScan: + case .scan: operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanJobData } case .all: operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } @@ -141,12 +138,11 @@ final class DataBrokerOperation: Operation { return filteredAndSortedOperationsData } - // swiftlint:disable:next function_body_length private func runOperation() async { let allBrokerProfileQueryData: [BrokerProfileQueryData] do { - allBrokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() + allBrokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() } catch { os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) return @@ -178,31 +174,26 @@ final class DataBrokerOperation: Operation { try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, brokerProfileQueryData: brokerProfileData, - database: database, - notificationCenter: notificationCenter, - runner: runner, - pixelHandler: pixelHandler, + database: operationDependencies.database, + notificationCenter: operationDependencies.notificationCenter, + runner: operationDependencies.runnerProvider.getJobRunner(), + pixelHandler: operationDependencies.pixelHandler, showWebView: showWebView, - isManualScan: operationType == .manualScan, - userNotificationService: userNotificationService, + isImmediateOperation: operationType == .scan, + userNotificationService: operationDependencies.userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } return !self.isCancelled }) - if let sleepInterval = brokerTimeInterval { - os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) - try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) - } + let sleepInterval = operationDependencies.config.intervalBetweenSameBrokerOperations + os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) + try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) } catch { os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - self.error = error - if let error = error as? DataBrokerProtectionError, - let dataBrokerName = brokerProfileQueriesData.first?.dataBroker.name { - pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) - } + errorDelegate?.dataBrokerOperationDidError(error, withBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } } @@ -222,3 +213,4 @@ final class DataBrokerOperation: Operation { os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) } } +// swiftlint:enable explicit_non_final_class diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index c1975440f3..72b5772a8b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -35,7 +35,7 @@ protocol OperationsManager { runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool, - isManualScan: Bool, + isImmediateOperation: Bool, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -58,7 +58,7 @@ extension OperationsManager { runner: runner, pixelHandler: pixelHandler, showWebView: false, - isManualScan: isManual, + isImmediateOperation: isManual, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } @@ -73,7 +73,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool = false, - isManualScan: Bool = false, + isImmediateOperation: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { @@ -84,7 +84,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, - isManual: isManualScan, + isManual: isImmediateOperation, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } else if let optOutJobData = operationData as? OptOutJobData { @@ -126,7 +126,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler, - isManualScan: isManual) + isImmediateOperation: isManual) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index cc0df841f6..8e47a7b6a7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -98,7 +98,13 @@ final class AppVersionNumber: AppVersionNumberProvider { var versionNumber: String = AppVersion.shared.versionNumber } -public struct DataBrokerProtectionBrokerUpdater { +protocol DataBrokerProtectionBrokerUpdater { + static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? + func updateBrokers() + func checkForUpdatesInBrokerJSONFiles() +} + +public struct DefaultDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrokerUpdater { private let repository: BrokerUpdaterRepository private let resources: ResourcesRepository @@ -118,9 +124,9 @@ public struct DataBrokerProtectionBrokerUpdater { self.pixelHandler = pixelHandler } - public static func provide() -> DataBrokerProtectionBrokerUpdater? { + public static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? { if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { - return DataBrokerProtectionBrokerUpdater(vault: vault) + return DefaultDataBrokerProtectionBrokerUpdater(vault: vault) } os_log("Error when trying to create vault for data broker protection updater debug menu item", log: .dataBrokerProtection) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift similarity index 92% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift index fae7c89425..ae72a5ae18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift @@ -1,5 +1,5 @@ // -// MismatchCalculatorUseCase.swift +// MismatchCalculator.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -36,7 +36,12 @@ enum MismatchValues: Int { } } -struct MismatchCalculatorUseCase { +protocol MismatchCalculator { + init(database: DataBrokerProtectionRepository, pixelHandler: EventMapping) + func calculateMismatches() +} + +struct DefaultMismatchCalculator: MismatchCalculator { let database: DataBrokerProtectionRepository let pixelHandler: EventMapping diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad7f3cd61d..dd4f4c1c4f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -64,7 +64,7 @@ public enum DataBrokerProtectionPixels { static let wasOnWaitlist = "was_on_waitlist" static let httpCode = "http_code" static let backendServiceCallSite = "backend_service_callsite" - static let isManualScan = "is_manual_scan" + static let isImmediateOperation = "is_manual_scan" static let durationInMs = "duration_in_ms" static let profileQueries = "profile_queries" static let hasError = "has_error" @@ -101,8 +101,8 @@ public enum DataBrokerProtectionPixels { case backgroundAgentRunOperationsAndStartSchedulerIfPossible case backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile // There's currently no point firing this because the scheduler never calls the completion with an error - // case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackError(error: Error) - case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler + // case backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackError(error: Error) + case backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler // IPC server events case ipcServerStartSchedulerCalledByApp @@ -128,8 +128,8 @@ public enum DataBrokerProtectionPixels { case ipcServerOptOutAllBrokers case ipcServerOptOutAllBrokersCompletion(error: Error?) - case ipcServerRunQueuedOperations - case ipcServerRunQueuedOperationsCompletion(error: Error?) + case ipcServerStartScheduledOperations + case ipcServerStartScheduledOperationsCompletion(error: Error?) case ipcServerRunAllOperations // DataBrokerProtection User Notifications @@ -143,9 +143,9 @@ public enum DataBrokerProtectionPixels { case dataBrokerProtectionNotificationOpenedAllRecordsRemoved // Scan/Search pixels - case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isManualScan: Bool) - case scanFailed(dataBroker: String, duration: Double, tries: Int, isManualScan: Bool) - case scanError(dataBroker: String, duration: Double, category: String, details: String, isManualScan: Bool) + case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isImmediateOperation: Bool) + case scanFailed(dataBroker: String, duration: Double, tries: Int, isImmediateOperation: Bool) + case scanError(dataBroker: String, duration: Double, category: String, details: String, isImmediateOperation: Bool) // KPIs - engagement case dailyActiveUser @@ -221,7 +221,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentRunOperationsAndStartSchedulerIfPossible: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible" case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_no-saved-profile" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" + case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" case .ipcServerStartSchedulerCalledByApp: return "m_mac_dbp_ipc-server_start-scheduler_called-by-app" case .ipcServerStartSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_start-scheduler_received-by-agent" @@ -245,8 +245,8 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .ipcServerOptOutAllBrokers: return "m_mac_dbp_ipc-server_opt-out-all-brokers" case .ipcServerOptOutAllBrokersCompletion: return "m_mac_dbp_ipc-server_opt-out-all-brokers_completion" - case .ipcServerRunQueuedOperations: return "m_mac_dbp_ipc-server_run-queued-operations" - case .ipcServerRunQueuedOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" + case .ipcServerStartScheduledOperations: return "m_mac_dbp_ipc-server_run-queued-operations" + case .ipcServerStartScheduledOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" case .ipcServerRunAllOperations: return "m_mac_dbp_ipc-server_run-all-operations" // User Notifications @@ -373,7 +373,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentStarted, .backgroundAgentRunOperationsAndStartSchedulerIfPossible, .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler, .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, .dataBrokerProtectionNotificationSentFirstScanComplete, .dataBrokerProtectionNotificationOpenedFirstScanComplete, @@ -417,16 +417,16 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption, .ipcServerOptOutAllBrokers, .ipcServerOptOutAllBrokersCompletion, - .ipcServerRunQueuedOperations, - .ipcServerRunQueuedOperationsCompletion, + .ipcServerStartScheduledOperations, + .ipcServerStartScheduledOperationsCompletion, .ipcServerRunAllOperations: return [Consts.bundleIDParamKey: Bundle.main.bundleIdentifier ?? "nil"] - case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] - case .scanFailed(let dataBroker, let duration, let tries, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] - case .scanError(let dataBroker, let duration, let category, let details, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isManualScan: isManualScan.description] + case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] + case .scanFailed(let dataBroker, let duration, let tries, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] + case .scanError(let dataBroker, let duration, let category, let details, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isImmediateOperation: isImmediateOperation.description] case .generateEmailHTTPErrorDaily(let statusCode, let environment, let wasOnWaitlist): return [Consts.environmentKey: environment, Consts.httpCode: String(statusCode), @@ -470,7 +470,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double func durationSinceStartTime() -> Double @@ -63,7 +63,7 @@ protocol StageDurationCalculator { } final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { - let isManualScan: Bool + let isImmediateOperation: Bool let handler: EventMapping let attemptId: UUID let dataBroker: String @@ -77,13 +77,13 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator startTime: Date = Date(), dataBroker: String, handler: EventMapping, - isManualScan: Bool = false) { + isImmediateOperation: Bool = false) { self.attemptId = attemptId self.startTime = startTime self.lastStateTime = startTime self.dataBroker = dataBroker self.handler = handler - self.isManualScan = isManualScan + self.isImmediateOperation = isImmediateOperation } /// Returned in milliseconds @@ -163,11 +163,11 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator } func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isImmediateOperation: isImmediateOperation)) } func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isImmediateOperation: isImmediateOperation)) } func fireScanError(error: Error) { @@ -205,7 +205,7 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator duration: durationSinceStartTime(), category: errorCategory.toString, details: error.localizedDescription, - isManualScan: isManualScan + isImmediateOperation: isImmediateOperation ) ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift index 41d02970af..aba746a849 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift @@ -23,6 +23,7 @@ protocol DataBrokerOperationsCreator { func operations(forOperationType operationType: OperationType, withPriorityDate priorityDate: Date?, showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] } @@ -31,6 +32,7 @@ final class DefaultDataBrokerOperationsCreator: DataBrokerOperationsCreator { func operations(forOperationType operationType: OperationType, withPriorityDate priorityDate: Date?, showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] { let brokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() @@ -42,10 +44,11 @@ final class DefaultDataBrokerOperationsCreator: DataBrokerOperationsCreator { if !visitedDataBrokerIDs.contains(dataBrokerID) { let collection = DataBrokerOperation(dataBrokerID: dataBrokerID, - operationType: operationType, - priorityDate: priorityDate, - showWebView: showWebView, - operationDependencies: operationDependencies) + operationType: operationType, + priorityDate: priorityDate, + showWebView: showWebView, + errorDelegate: errorDelegate, + operationDependencies: operationDependencies) operations.append(collection) visitedDataBrokerIDs.insert(dataBrokerID) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift deleted file mode 100644 index 5cc6c42ea0..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// DataBrokerProtectionProcessor.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 Common -import BrowserServicesKit - -final class DataBrokerProtectionProcessor { - private let database: DataBrokerProtectionRepository - private let config: DataBrokerProtectionProcessorConfiguration - private let jobRunnerProvider: JobRunnerProvider - private let notificationCenter: NotificationCenter - private let operationQueue: OperationQueue - private var pixelHandler: EventMapping - private let userNotificationService: DataBrokerProtectionUserNotificationService - private let engagementPixels: DataBrokerProtectionEngagementPixels - private let eventPixels: DataBrokerProtectionEventPixels - - init(database: DataBrokerProtectionRepository, - config: DataBrokerProtectionProcessorConfiguration = DataBrokerProtectionProcessorConfiguration(), - jobRunnerProvider: JobRunnerProvider, - notificationCenter: NotificationCenter = NotificationCenter.default, - pixelHandler: EventMapping, - userNotificationService: DataBrokerProtectionUserNotificationService) { - - self.database = database - self.config = config - self.jobRunnerProvider = jobRunnerProvider - self.notificationCenter = notificationCenter - self.operationQueue = OperationQueue() - self.pixelHandler = pixelHandler - self.userNotificationService = userNotificationService - self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) - self.eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) - } - - // MARK: - Public functions - func startManualScans(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { - - operationQueue.cancelAllOperations() - runOperations(operationType: .manualScan, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Scans done", log: .dataBrokerProtection) - completion?(errors) - self.calculateMisMatches() - } - } - - private func calculateMisMatches() { - let mismatchUseCase = MismatchCalculatorUseCase(database: database, pixelHandler: pixelHandler) - mismatchUseCase.calculateMismatches() - } - - func runAllOptOutOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { - operationQueue.cancelAllOperations() - runOperations(operationType: .optOut, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Optouts done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func runQueuedOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil ) { - runOperations(operationType: .all, - priorityDate: Date(), - showWebView: showWebView) { errors in - os_log("Queued operations done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func runAllOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil ) { - runOperations(operationType: .all, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Queued operations done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func stopAllOperations() { - operationQueue.cancelAllOperations() - } - - // MARK: - Private functions - private func runOperations(operationType: OperationType, - priorityDate: Date?, - showWebView: Bool, - completion: @escaping ((DataBrokerProtectionAgentErrorCollection?) -> Void)) { - - self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsFor(operationType) - // Before running new operations we check if there is any updates to the broker files. - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { - let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) - brokerUpdater.checkForUpdatesInBrokerJSONFiles() - } - - // This will fire the DAU/WAU/MAU pixels, - engagementPixels.fireEngagementPixel() - // This will try to fire the event weekly report pixels - eventPixels.tryToFireWeeklyPixels() - - let operations: [DataBrokerOperation] - - do { - // Note: The next task in this project will inject the dependencies & builder into our new 'QueueManager' type - - let dependencies = DefaultDataBrokerOperationDependencies(database: database, - brokerTimeInterval: config.intervalBetweenSameBrokerOperations, - runnerProvider: jobRunnerProvider, - notificationCenter: notificationCenter, - pixelHandler: pixelHandler, - userNotificationService: userNotificationService) - - operations = try DefaultDataBrokerOperationsCreator().operations(forOperationType: operationType, - withPriorityDate: priorityDate, - showWebView: showWebView, - operationDependencies: dependencies) - - for operation in operations { - operationQueue.addOperation(operation) - } - } catch { - os_log("DataBrokerProtectionProcessor error: runOperations, error: %{public}@", log: .error, error.localizedDescription) - operationQueue.addBarrierBlock { - completion(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) - } - return - } - - operationQueue.addBarrierBlock { - let operationErrors = operations.compactMap { $0.error } - let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionAgentErrorCollection(operationErrors: operationErrors) : nil - completion(errorCollection) - } - } - - deinit { - os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift index 3281b66c37..2914b43f5e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift @@ -29,7 +29,7 @@ struct DataBrokerProtectionProcessorConfiguration { switch operation { case .all, .optOut: return concurrentOperationsDifferentBrokers - case .manualScan: + case .scan: return concurrentOperationsOnManualScans } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift new file mode 100644 index 0000000000..ab21bd6d35 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -0,0 +1,262 @@ +// +// DataBrokerProtectionQueueManager.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 Common +import Foundation + +protocol DataBrokerProtectionOperationQueue { + var maxConcurrentOperationCount: Int { get set } + func cancelAllOperations() + func addOperation(_ op: Operation) + func addBarrierBlock(_ barrier: @escaping @Sendable () -> Void) +} + +extension OperationQueue: DataBrokerProtectionOperationQueue {} + +enum DataBrokerProtectionQueueMode { + case idle + case immediate(completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + case scheduled(completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + + var priorityDate: Date? { + switch self { + case .idle, .immediate: + return nil + case .scheduled: + return Date() + } + } + + func canBeInterruptedBy(newMode: DataBrokerProtectionQueueMode) -> Bool { + switch (self, newMode) { + case (.idle, _): + return true + case (_, .immediate): + return true + default: + return false + } + } +} + +enum DataBrokerProtectionQueueError: Error { + case cannotInterrupt +} + +enum DataBrokerProtectionQueueManagerDebugCommand { + case startOptOutOperations(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) +} + +protocol DataBrokerProtectionQueueManager { + + init(operationQueue: DataBrokerProtectionOperationQueue, + operationsCreator: DataBrokerOperationsCreator, + mismatchCalculator: MismatchCalculator, + brokerUpdater: DataBrokerProtectionBrokerUpdater?, + pixelHandler: EventMapping) + + func startImmediateOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + + func stopAllOperations() + + func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) +} + +final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueManager { + + private var operationQueue: DataBrokerProtectionOperationQueue + private let operationsCreator: DataBrokerOperationsCreator + private let mismatchCalculator: MismatchCalculator + private let brokerUpdater: DataBrokerProtectionBrokerUpdater? + private let pixelHandler: EventMapping + + private var mode = DataBrokerProtectionQueueMode.idle + private var operationErrors: [Error] = [] + + init(operationQueue: DataBrokerProtectionOperationQueue, + operationsCreator: DataBrokerOperationsCreator, + mismatchCalculator: MismatchCalculator, + brokerUpdater: DataBrokerProtectionBrokerUpdater?, + pixelHandler: EventMapping) { + + self.operationQueue = operationQueue + self.operationsCreator = operationsCreator + self.mismatchCalculator = mismatchCalculator + self.brokerUpdater = brokerUpdater + self.pixelHandler = pixelHandler + } + + func startImmediateOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + let newMode = DataBrokerProtectionQueueMode.immediate(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: .scan, + showWebView: showWebView, + operationDependencies: operationDependencies) { [weak self] errors in + completion?(errors) + self?.mismatchCalculator.calculateMismatches() + } + } + + func startScheduledOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + let newMode = DataBrokerProtectionQueueMode.scheduled(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: .all, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func stopAllOperations() { + cancelCurrentModeAndResetIfNeeded() + } + + func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) { + guard case .startOptOutOperations(let showWebView, + let operationDependencies, + let completion) = command else { return } + + addOperations(withType: .optOut, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } +} + +private extension DefaultDataBrokerProtectionQueueManager { + + func startOperationsIfPermitted(forNewMode newMode: DataBrokerProtectionQueueMode, + type: OperationType, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + guard mode.canBeInterruptedBy(newMode: newMode) else { + let error = DataBrokerProtectionQueueError.cannotInterrupt + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: error) + completion?(errorCollection) + return + } + + cancelCurrentModeAndResetIfNeeded() + + mode = newMode + + updateBrokerData() + + firePixels(operationDependencies: operationDependencies) + + addOperations(withType: type, + priorityDate: mode.priorityDate, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func cancelCurrentModeAndResetIfNeeded() { + switch mode { + case .immediate(let completion), .scheduled(let completion): + operationQueue.cancelAllOperations() + completion?(errorCollectionForCurrentOperations()) + resetModeAndClearErrors() + default: + break + } + } + + func resetModeAndClearErrors() { + mode = .idle + operationErrors = [] + } + + func updateBrokerData() { + // Update broker files if applicable + brokerUpdater?.checkForUpdatesInBrokerJSONFiles() + } + + func addOperations(withType type: OperationType, + priorityDate: Date? = nil, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + operationQueue.maxConcurrentOperationCount = operationDependencies.config.concurrentOperationsFor(type) + + // Use builder to build operations + let operations: [DataBrokerOperation] + do { + operations = try operationsCreator.operations(forOperationType: type, + withPriorityDate: priorityDate, + showWebView: showWebView, + errorDelegate: self, + operationDependencies: operationDependencies) + + for collection in operations { + operationQueue.addOperation(collection) + } + } catch { + os_log("DataBrokerProtectionProcessor error: addOperations, error: %{public}@", log: .error, error.localizedDescription) + completion?(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) + return + } + + operationQueue.addBarrierBlock { [weak self] in + let errorCollection = self?.errorCollectionForCurrentOperations() + completion?(errorCollection) + self?.resetModeAndClearErrors() + } + } + + func errorCollectionForCurrentOperations() -> DataBrokerProtectionAgentErrorCollection? { + return operationErrors.count != 0 ? DataBrokerProtectionAgentErrorCollection(operationErrors: operationErrors) : nil + } + + func firePixels(operationDependencies: DataBrokerOperationDependencies) { + let database = operationDependencies.database + let pixelHandler = operationDependencies.pixelHandler + + let engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) + let eventPixels = DataBrokerProtectionEventPixels(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() + } +} + +extension DefaultDataBrokerProtectionQueueManager: DataBrokerOperationErrorDelegate { + func dataBrokerOperationDidError(_ error: any Error, withBrokerName brokerName: String?) { + operationErrors.append(error) + + if let error = error as? DataBrokerProtectionError, let dataBrokerName = brokerName { + pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index d9ac509201..aa8fc9e004 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -66,18 +66,35 @@ public final class DefaultDataBrokerProtectionScheduler { public var lastSchedulerSessionStartTimestamp: Date? - private lazy var dataBrokerProcessor: DataBrokerProtectionProcessor = { - + private lazy var queueManager: DataBrokerProtectionQueueManager = { + let operationQueue = OperationQueue() + let operationsBuilder = DefaultDataBrokerOperationsCreator() + let mismatchCalculator = DefaultMismatchCalculator(database: dataManager.database, + pixelHandler: pixelHandler) + + var brokerUpdater: DataBrokerProtectionBrokerUpdater? + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + brokerUpdater = DefaultDataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) + } + + return DefaultDataBrokerProtectionQueueManager(operationQueue: operationQueue, + operationsCreator: operationsBuilder, + mismatchCalculator: mismatchCalculator, + brokerUpdater: brokerUpdater, + pixelHandler: pixelHandler) + }() + + private lazy var operationDependencies: DataBrokerOperationDependencies = { let runnerProvider = DataBrokerJobRunnerProvider(privacyConfigManager: privacyConfigManager, - contentScopeProperties: contentScopeProperties, - emailService: emailService, - captchaService: captchaService) - - return DataBrokerProtectionProcessor(database: dataManager.database, - jobRunnerProvider: runnerProvider, - notificationCenter: notificationCenter, - pixelHandler: pixelHandler, - userNotificationService: userNotificationService) + contentScopeProperties: contentScopeProperties, + emailService: emailService, + captchaService: captchaService) + + return DefaultDataBrokerOperationDependencies(database: dataManager.database, + config: DataBrokerProtectionProcessorConfiguration(), + runnerProvider: runnerProvider, + notificationCenter: notificationCenter, + pixelHandler: pixelHandler, userNotificationService: userNotificationService) }() public init(privacyConfigManager: PrivacyConfigurationManaging, @@ -128,15 +145,15 @@ public final class DefaultDataBrokerProtectionScheduler { self.status = .running os_log("Scheduler running...", log: .dataBrokerProtection) self.currentOperation = .queued - self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in + self.queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, operationDependencies: self.operationDependencies) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { - os_log("Error during startScheduler in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Error during startScheduler in dataBrokerProcessor.startScheduledOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduler")) } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during startScheduler in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during startScheduler in dataBrokerProcessor.startScheduledOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } self?.status = .idle @@ -150,33 +167,10 @@ public final class DefaultDataBrokerProtectionScheduler { os_log("Stopping scheduler...", log: .dataBrokerProtection) activity.invalidate() status = .stopped - dataBrokerProcessor.stopAllOperations() - } - - public func runAllOperations(showWebView: Bool = false) { - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Running all operations...", log: .dataBrokerProtection) - self.currentOperation = .all - self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runAllOperations")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.currentOperation = .idle - } + queueManager.stopAllOperations() } - public func runQueuedOperations(showWebView: Bool = false, + public func startScheduledOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { guard self.currentOperation != .manualScan else { os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) @@ -185,16 +179,17 @@ public final class DefaultDataBrokerProtectionScheduler { os_log("Running queued operations...", log: .dataBrokerProtection) self.currentOperation = .queued - dataBrokerProcessor.runQueuedOperations(showWebView: showWebView, - completion: { [weak self] errors in + queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runQueuedOperations")) + os_log("Error during DefaultDataBrokerProtectionScheduler.startScheduledOperations in dataBrokerProcessor.startScheduledOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduledOperations")) } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startScheduledOperations in dataBrokerProcessor.startScheduledOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } completion?(errors) @@ -203,9 +198,9 @@ public final class DefaultDataBrokerProtectionScheduler { } - public func startManualScan(showWebView: Bool = false, - startTime: Date, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { + public func startImmediateOperations(showWebView: Bool = false, + startTime: Date, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) let backgroundAgentManualScanStartTime = Date() stopScheduler() @@ -213,7 +208,8 @@ public final class DefaultDataBrokerProtectionScheduler { userNotificationService.requestNotificationPermission() self.currentOperation = .manualScan os_log("Scanning all brokers...", log: .dataBrokerProtection) - dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in + queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies) { [weak self] errors in guard let self = self else { return } self.startScheduler(showWebView: showWebView) @@ -231,15 +227,15 @@ public final class DefaultDataBrokerProtectionScheduler { if let oneTimeError = errors.oneTimeError { switch oneTimeError { case DataBrokerProtectionAgentInterfaceError.operationsInterrupted: - os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) default: - os_log("Error during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startManualScan")) + os_log("Error during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startImmediateOperations")) } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } self.currentOperation = .idle @@ -269,8 +265,7 @@ public final class DefaultDataBrokerProtectionScheduler { os_log("Opting out all brokers...", log: .dataBrokerProtection) self.currentOperation = .optOutAll - self.dataBrokerProcessor.runAllOptOutOperations(showWebView: showWebView, - completion: { [weak self] errors in + queueManager.execute(.startOptOutOperations(showWebView: showWebView, operationDependencies: operationDependencies) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { os_log("Error during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift index 3caeee6475..42060fdb9b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -38,7 +38,7 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { mockUserNotification = MockUserNotification() mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, - brokerTimeInterval: mockSchedulerConfig.intervalBetweenSameBrokerOperations, + config: mockSchedulerConfig, runnerProvider: mockRunnerProvider, notificationCenter: .default, pixelHandler: mockPixelHandler, @@ -70,9 +70,10 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { mockDatabase.brokerProfileQueryDataToReturn = dataBrokerProfileQueries // When - let result = try! sut.operations(forOperationType: .manualScan, + let result = try! sut.operations(forOperationType: .scan, withPriorityDate: Date(), showWebView: false, + errorDelegate: MockDataBrokerOperationErrorDelegate(), operationDependencies: mockDependencies) // Then diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift index fe8eec11c1..f8e2be4be2 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift @@ -25,7 +25,7 @@ final class DataBrokerProtectionProcessorConfigurationTests: XCTestCase { private let sut = DataBrokerProtectionProcessorConfiguration() func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { - let value = sut.concurrentOperationsFor(.manualScan) + let value = sut.concurrentOperationsFor(.scan) let expectedValue = 6 XCTAssertEqual(value, expectedValue) } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift new file mode 100644 index 0000000000..eb11ce957c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift @@ -0,0 +1,350 @@ +// +// DataBrokerProtectionQueueManagerTests.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 DataBrokerProtection + +final class DataBrokerProtectionQueueManagerTests: XCTestCase { + + private var sut: DefaultDataBrokerProtectionQueueManager! + + private var mockQueue: MockDataBrokerProtectionOperationQueue! + private var mockOperationsCreator: MockDataBrokerOperationsCreator! + private var mockDatabase: MockDatabase! + private var mockPixelHandler: MockPixelHandler! + private var mockMismatchCalculator: MockMismatchCalculator! + private var mockUpdater: MockDataBrokerProtectionBrokerUpdater! + private var mockSchedulerConfig = DataBrokerProtectionProcessorConfiguration() + private var mockRunnerProvider: MockRunnerProvider! + private var mockUserNotification: MockUserNotification! + private var mockOperationErrorDelegate: MockDataBrokerOperationErrorDelegate! + private var mockDependencies: DefaultDataBrokerOperationDependencies! + + override func setUpWithError() throws { + mockQueue = MockDataBrokerProtectionOperationQueue() + mockOperationsCreator = MockDataBrokerOperationsCreator() + mockDatabase = MockDatabase() + mockPixelHandler = MockPixelHandler() + mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) + mockUpdater = MockDataBrokerProtectionBrokerUpdater() + mockRunnerProvider = MockRunnerProvider() + mockUserNotification = MockUserNotification() + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + config: DataBrokerProtectionProcessorConfiguration(), + runnerProvider: mockRunnerProvider, + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockUserNotification) + } + + func testWhenStartImmediateScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.scan) + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartScheduledScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.all) + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNotNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartImmediateScan_andCurrentModeIsScheduled_thenCurrentOperationsAreInterrupted_andCurrentCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(errorCollection.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) + } + + func testWhenStartImmediateScan_andCurrentModeIsImmediate_thenCurrentOperationsAreInterrupted_andCurrentCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(errorCollection.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) + } + + func testWhenSecondImmedateScanInterruptsFirst_andFirstHadErrors_thenSecondCompletesOnlyWithNewErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + var mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollectionFirst: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollectionFirst = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + var errorCollectionSecond: DataBrokerProtectionAgentErrorCollection! + mockOperationsWithError = (5...6).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + mockOperations = (7...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollectionSecond = errors + } + + mockQueue.completeAllOperations() + + // Then + XCTAssert(errorCollectionFirst.operationErrors?.count == 2) + XCTAssert(errorCollectionSecond.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + } + + func testWhenStartScheduledScan_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + var mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(mockQueue.operationCount == 10) + + // Given + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + let expectedError = DataBrokerProtectionQueueError.cannotInterrupt + var completionCalled = false + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + completionCalled.toggle() + } + + // Then + XCTAssert(mockQueue.didCallCancelCount == 0) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 10) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count == 0) + XCTAssertEqual((errorCollection.oneTimeError as? DataBrokerProtectionQueueError), expectedError) + XCTAssert(completionCalled) + } + + func testWhenOperationBuildingFails_thenCompletionIsCalledOnOperationCreationOneTimeError() async throws { + // Given + mockOperationsCreator.shouldError = true + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let expectation = expectation(description: "Expected completion to be called") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssertNotNil(errorCollection.oneTimeError) + } + + func testWhenOperationsAreRunning_andStopAllIsCalled_thenAllAreCancelled_andCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + let mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + let expectation = expectation(description: "Expected completion to be called") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeOperationsUpTo(index: 2) + + sut.stopAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssert(errorCollection.operationErrors?.count == 2) + } + + func testWhenCallDebugOptOutCommand_thenOptOutOperationsAreCreated() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.optOut) + XCTAssert(mockOperationsCreator.createdType == .scan) + + // When + sut.execute(.startOptOutOperations(showWebView: false, + operationDependencies: mockDependencies, + completion: nil)) + + // Then + XCTAssert(mockOperationsCreator.createdType == .optOut) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift new file mode 100644 index 0000000000..ef2ab05b38 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift @@ -0,0 +1,122 @@ +// +// DataBrokerProtectionQueueModeTests.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. +// + +@testable import DataBrokerProtection +import XCTest + +final class DataBrokerProtectionQueueModeTests: XCTestCase { + + func testCurrentModeIdle_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeIdle_andNewModeScheduled_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeImmediate_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: { _ in })) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeImmediate_andNewModeScheduled_thenInterruptionNotAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertFalse(result) + } + + func testCurrentModeScheduled_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeScheduled_andNewModeScheduled_thenInterruptionNotAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertFalse(result) + } + + func testWhenModeIsIdle_thenPriorityDateIsNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.priorityDate + + // Then + XCTAssertNil(result) + } + + func testWhenModeIsImmediate_thenPriorityDateIsNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.priorityDate + + // Then + XCTAssertNil(result) + } + + func testWhenModeIsScheduled_thenPriorityDateIsNotNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.priorityDate + + // Then + XCTAssertNotNil(result) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index 2036ac9a1d..4f70f8934a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -39,7 +39,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenNoVersionIsStored_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil sut.checkForUpdatesInBrokerJSONFiles() @@ -53,7 +53,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndPatchIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.1")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.1")) repository.lastCheckedVersion = "1.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -67,7 +67,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndMinorIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "1.73.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -81,7 +81,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndMajorIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "0.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -95,7 +95,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndIsEqualOrGreaterThanCurrentOne_thenCheckingUpdatesIsSkipped() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "1.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -109,7 +109,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenSavedBrokerIsOnAnOldVersion_thenWeUpdateIt() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true @@ -127,7 +127,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenSavedBrokerIsOnTheCurrentVersion_thenWeDoNotUpdateIt() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true @@ -144,7 +144,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenFileBrokerIsNotStored_thenWeAddTheBrokerAndScanOperations() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 396034791a..fb2af7862b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -40,7 +40,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -65,7 +65,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -90,7 +90,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -115,7 +115,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -136,7 +136,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { database.brokerProfileQueryDataToReturn = [ .mockParentWith(historyEvents: parentHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 5e71a5b755..a80b15eb19 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -868,7 +868,7 @@ final class MockAppVersion: AppVersionNumberProvider { } final class MockStageDurationCalculator: StageDurationCalculator { - var isManualScan: Bool = false + var isImmediateOperation: Bool = false var attemptId: UUID = UUID() var stage: Stage? @@ -962,7 +962,6 @@ final class MockDataBrokerProtectionBackendServicePixels: DataBrokerProtectionBa } final class MockRunnerProvider: JobRunnerProvider { - func getJobRunner() -> any WebJobRunner { MockWebJobRunner() } @@ -1026,3 +1025,185 @@ extension DataBroker { ) } } + +final class MockDataBrokerProtectionOperationQueue: DataBrokerProtectionOperationQueue { + var maxConcurrentOperationCount = 1 + + var operations: [Operation] = [] + var operationCount: Int { + operations.count + } + + private(set) var didCallCancelCount = 0 + private(set) var didCallAddCount = 0 + private(set) var didCallAddBarrierBlockCount = 0 + + private var barrierBlock: (@Sendable () -> Void)? + + func cancelAllOperations() { + didCallCancelCount += 1 + self.operations.forEach { $0.cancel() } + } + + func addOperation(_ op: Operation) { + didCallAddCount += 1 + self.operations.append(op) + } + + func addBarrierBlock(_ barrier: @escaping @Sendable () -> Void) { + didCallAddBarrierBlockCount += 1 + self.barrierBlock = barrier + } + + func completeAllOperations() { + operations.forEach { $0.start() } + operations.removeAll() + barrierBlock?() + } + + func completeOperationsUpTo(index: Int) { + guard index < operationCount else { return } + + (0.. [DataBrokerOperation] { + guard !shouldError else { throw DataBrokerProtectionError.unknown("")} + self.createdType = operationType + self.priorityDate = priorityDate + return operationCollections + } +} + +final class MockMismatchCalculator: MismatchCalculator { + + private(set) var didCallCalculateMismatches = false + + init(database: any DataBrokerProtectionRepository, pixelHandler: Common.EventMapping) { } + + func calculateMismatches() { + didCallCalculateMismatches = true + } +} + +final class MockDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrokerUpdater { + + private(set) var didCallUpdateBrokers = false + private(set) var didCallCheckForUpdates = false + + static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? { + nil + } + + func updateBrokers() { + didCallUpdateBrokers = true + } + + func checkForUpdatesInBrokerJSONFiles() { + didCallCheckForUpdates = true + } +} From 1ddb5a4073b007beadb6887dfe60e6c8f62284d8 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 15 May 2024 22:04:26 +0100 Subject: [PATCH 04/42] DBP: Restructure AgentManager and scheduler and add tests (#2777) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207175016813449/f Tech Design URL: CC: **Description**: 1. Move all userNotification related calls and anything else unrelated to scheduling up a layer to the agent manager 2. Removes the existing scheduler, and moves the ActivityScheduler management to a new class 3. Make a variety of changes to scheduler behavior, such as running all the time (as long as theirs profile data), changing scheduler frequency to 20 minutes, removes status publishing 4. Fixes us not deleting the login item when the user deleted their data 5. As part of that, removes the dataDeleted IPC method, since it's no longer needed 5. Reworks the agent manager and associated dependancies so it can be unit tested 6. Adds those unit tests. Pixels are still outstanding But everything else should be done now (integrating with Pete's latest PR etc) **Steps to test this PR**: 1. Generally test that DBP works, but not too thoroughly since we will do that for the whole feature branch at once. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Pete --- DuckDuckGo.xcodeproj/project.pbxproj | 18 -- .../DBP/DataBrokerProtectionAppEvents.swift | 5 + .../DataBrokerProtectionFeatureDisabler.swift | 2 +- ...taBrokerProtectionLoginItemInterface.swift | 48 ++- .../DBP/DataBrokerProtectionManager.swift | 2 +- .../DataBrokerProtectionPixelsHandler.swift | 38 +-- .../DataBrokerProtectionAgentManager.swift | 165 ---------- ...kDuckGoDBPBackgroundAgentAppDelegate.swift | 5 +- .../DataBrokerRunCustomJSONViewModel.swift | 77 ----- ...BrokerProtectionAppToAgentInterface.swift} | 12 +- .../IPC/DataBrokerProtectionIPCClient.swift | 22 +- .../IPC/DataBrokerProtectionIPCServer.swift | 29 +- .../Model/DBPUIViewModel.swift | 4 +- .../Operations/DataBrokerOperation.swift | 4 +- .../Pixels/DataBrokerProtectionPixels.swift | 165 ++++------ .../Scheduler}/BrowserWindowManager.swift | 0 .../Scheduler}/DBPMocks.swift | 14 +- ....swift => DataBrokerExecutionConfig.swift} | 11 +- .../DataBrokerProtectionAgentManager.swift | 285 ++++++++++++++++ ...rotectionBackgroundActivityScheduler.swift | 58 ++++ .../DataBrokerProtectionQueueManager.swift | 27 +- .../DataBrokerProtectionScheduler.swift | 296 ----------------- .../DataBrokerProtectionViewController.swift | 2 +- ...t => DataBrokerExecutionConfigTests.swift} | 4 +- .../DataBrokerOperationsCreatorTests.swift | 8 +- ...kerProfileQueryOperationManagerTests.swift | 105 ++---- ...ataBrokerProtectionAgentManagerTests.swift | 306 ++++++++++++++++++ ...ataBrokerProtectionQueueManagerTests.swift | 52 +-- .../DataBrokerProtectionTests/Mocks.swift | 164 +++++++++- 29 files changed, 1030 insertions(+), 898 deletions(-) delete mode 100644 DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionAgentManager.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/{DataBrokerProtectionAgentInterface.swift => DataBrokerProtectionAppToAgentInterface.swift} (86%) rename {DuckDuckGoDBPBackgroundAgent => LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler}/BrowserWindowManager.swift (100%) rename {DuckDuckGoDBPBackgroundAgent => LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler}/DBPMocks.swift (81%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/{DataBrokerProtectionProcessorConfiguration.swift => DataBrokerExecutionConfig.swift} (79%) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift rename LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/{DataBrokerProtectionProcessorConfigurationTests.swift => DataBrokerExecutionConfigTests.swift} (90%) create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e26e8314b9..5140727a30 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1461,14 +1461,12 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; - 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; - 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; @@ -1689,10 +1687,6 @@ 9D9AE9202AAA3B450026E7DC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */; }; 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9172AAA3B450026E7DC /* UserText.swift */; }; 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9172AAA3B450026E7DC /* UserText.swift */; }; - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */; }; - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */; }; - 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; - 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; @@ -3276,7 +3270,6 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; - 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; @@ -3444,8 +3437,6 @@ 9D9AE9182AAA3B450026E7DC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D9AE9192AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoDBPBackgroundAgentAppStore.entitlements; sourceTree = ""; }; 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoDBPBackgroundAgent.entitlements; sourceTree = ""; }; - 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAgentManager.swift; sourceTree = ""; }; - 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F0FFFB32BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkAllTabsDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; @@ -6238,9 +6229,6 @@ isa = PBXGroup; children = ( 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, - 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift */, - 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, - 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */, @@ -10713,9 +10701,6 @@ buildActionMask = 2147483647; files = ( 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, - 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, ); @@ -10726,9 +10711,6 @@ buildActionMask = 2147483647; files = ( 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, - 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, - 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionAgentManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, ); diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 07a579367f..a86c213dc4 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -34,6 +34,7 @@ struct DataBrokerProtectionAppEvents { func applicationDidFinishLaunching() { let loginItemsManager = LoginItemsManager() let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() + let loginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } @@ -52,6 +53,10 @@ struct DataBrokerProtectionAppEvents { if let profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), profileQueriesCount > 0 { restartBackgroundAgent(loginItemsManager: loginItemsManager) + + // Wait to make sure the agent has had time to restart before attempting to call a method on it + try await Task.sleep(nanoseconds: 1_000_000_000) + loginItemInterface.appLaunched() } else { featureVisibility.disableAndDeleteForWaitlistUsers() } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index 8d5ea0401a..18a1c89b86 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -42,10 +42,10 @@ struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling func disableAndDelete() { if !DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist { - loginItemInterface.disableLoginItem() do { try dataManager.removeAllData() + // the dataManagers delegate handles login item disabling } catch { os_log("DataBrokerProtectionFeatureDisabler error: disableAndDelete, error: %{public}@", log: .error, error.localizedDescription) } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift index 1ff520cac3..c4e86483c9 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift @@ -22,9 +22,8 @@ import Foundation import DataBrokerProtection import Common -protocol DataBrokerProtectionLoginItemInterface: DataBrokerProtectionAgentInterface { - func disableLoginItem() - func enableLoginItem() +protocol DataBrokerProtectionLoginItemInterface: DataBrokerProtectionAppToAgentInterface { + func dataDeleted() } /// Launches a login item and then communicates with it through IPC @@ -32,10 +31,14 @@ protocol DataBrokerProtectionLoginItemInterface: DataBrokerProtectionAgentInterf final class DefaultDataBrokerProtectionLoginItemInterface { private let ipcClient: DataBrokerProtectionIPCClient private let loginItemsManager: LoginItemsManager + private let pixelHandler: EventMapping - init(ipcClient: DataBrokerProtectionIPCClient, loginItemsManager: LoginItemsManager = .init()) { + init(ipcClient: DataBrokerProtectionIPCClient, + loginItemsManager: LoginItemsManager = .init(), + pixelHandler: EventMapping) { self.ipcClient = ipcClient self.loginItemsManager = loginItemsManager + self.pixelHandler = pixelHandler } } @@ -43,35 +46,50 @@ extension DefaultDataBrokerProtectionLoginItemInterface: DataBrokerProtectionLog // MARK: - Login Item Management - func disableLoginItem() { + private func disableLoginItem() { DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerDisableLoginItemDaily, frequency: .daily) loginItemsManager.disableLoginItems([.dbpBackgroundAgent]) } - func enableLoginItem() { + private func enableLoginItem() { DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerEnableLoginItemDaily, frequency: .daily) loginItemsManager.enableLoginItems([.dbpBackgroundAgent], log: .dbp) } - // MARK: - DataBrokerProtectionAgentInterface + // MARK: - DataBrokerProtectionLoginItemInterface + + func dataDeleted() { + disableLoginItem() + } + + // MARK: - DataBrokerProtectionAppToAgentInterface // MARK: - DataBrokerProtectionAgentAppEvents func profileSaved() { enableLoginItem() - ipcClient.profileSaved { error in - // TODO - } - } - func dataDeleted() { - ipcClient.dataDeleted { error in - // TODO + Task { + // Wait to make sure the agent has had time to launch + try await Task.sleep(nanoseconds: 1_000_000_000) + pixelHandler.fire(.ipcServerProfileSavedCalledByApp) + ipcClient.profileSaved { error in + if let error = error { + self.pixelHandler.fire(.ipcServerProfileSavedXPCError(error: error)) + } else { + self.pixelHandler.fire(.ipcServerProfileSavedReceivedByAgent) + } + } } } func appLaunched() { + pixelHandler.fire(.ipcServerAppLaunchedCalledByApp) ipcClient.appLaunched { error in - // TODO + if let error = error { + self.pixelHandler.fire(.ipcServerAppLaunchedXPCError(error: error)) + } else { + self.pixelHandler.fire(.ipcServerAppLaunchedReceivedByAgent) + } } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index 41844bb3cc..47353e52a4 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -49,7 +49,7 @@ public final class DataBrokerProtectionManager { }() lazy var loginItemInterface: DataBrokerProtectionLoginItemInterface = { - return DefaultDataBrokerProtectionLoginItemInterface(ipcClient: ipcClient) + return DefaultDataBrokerProtectionLoginItemInterface(ipcClient: ipcClient, pixelHandler: pixelHandler) }() private init() { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 37f9f41e29..494d141be5 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -33,26 +33,20 @@ public class DataBrokerProtectionPixelsHandler: EventMapping = DataBrokerProtectionPixelsHandler() - - private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() - private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() - private let redeemUseCase: DataBrokerProtectionRedeemUseCase - private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() - private lazy var browserWindowManager = BrowserWindowManager() - - private lazy var ipcServer: DataBrokerProtectionIPCServer = { - let server = DataBrokerProtectionIPCServer(machServiceName: Bundle.main.bundleIdentifier!) - server.serverDelegate = self - return server - }() - - lazy var dataManager: DataBrokerProtectionDataManager = { - DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) - }() - - lazy var scheduler: DefaultDataBrokerProtectionScheduler = { - let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned - let features = ContentScopeFeatureToggles(emailProtection: false, - emailProtectionIncontextSignup: false, - credentialsAutofill: false, - identitiesAutofill: false, - creditCardsAutofill: false, - credentialsSaving: false, - passwordGeneration: false, - inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) - - let sessionKey = UUID().uuidString - let prefs = ContentScopeProperties(gpcEnabled: false, - sessionKey: sessionKey, - featureToggles: features) - - let pixelHandler = DataBrokerProtectionPixelsHandler() - - let userNotificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) - - return DefaultDataBrokerProtectionScheduler(privacyConfigManager: privacyConfigurationManager, - contentScopeProperties: prefs, - dataManager: dataManager, - notificationCenter: NotificationCenter.default, - pixelHandler: pixelHandler, - redeemUseCase: redeemUseCase, - userNotificationService: userNotificationService) - }() - - private init() { - self.redeemUseCase = RedeemUseCase(authenticationService: authenticationService, - authenticationRepository: authenticationRepository) - ipcServer.activate() - } - - public func agentFinishedLaunching() { - pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossible) - - do { - // If there's no saved profile we don't need to start the scheduler - guard (try dataManager.fetchProfile()) != nil else { - pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile) - return - } - } catch { - pixelHandler.fire(.generalError(error: error, - functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) - return - } - - scheduler.startScheduledOperations(showWebView: false) { [weak self] _ in - self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler) - self?.scheduler.startScheduler() - } - } -} - -extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { - - public func profileSaved() { - scheduler.startImmediateOperations(startTime: Date()) { _ in - - } - } - - public func dataDeleted() { - scheduler.stopScheduler() - } - - public func appLaunched() { - scheduler.startScheduledOperations() - } -} - -extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentDebugCommands { - public func openBrowser(domain: String) { - Task { @MainActor in - browserWindowManager.show(domain: domain) - } - } - - public func startImmediateOperations(showWebView: Bool) { - scheduler.startImmediateOperations(startTime: Date()) { _ in - - } - } - - public func startScheduledOperations(showWebView: Bool) { - scheduler.startScheduledOperations(showWebView: showWebView) - } - - public func runAllOptOuts(showWebView: Bool) { - scheduler.optOutAllBrokers(showWebView: showWebView) { _ in - - } - } - - public func getDebugMetadata() async -> DataBrokerProtection.DBPBackgroundAgentMetadata? { - - if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, - let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { - - return DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", - isAgentRunning: scheduler.status == .running, - agentSchedulerState: scheduler.status.toString, - lastSchedulerSessionStartTimestamp: scheduler.lastSchedulerSessionStartTimestamp?.timeIntervalSince1970) - } else { - return DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", - isAgentRunning: scheduler.status == .running, - agentSchedulerState: scheduler.status.toString, - lastSchedulerSessionStartTimestamp: scheduler.lastSchedulerSessionStartTimestamp?.timeIntervalSince1970) - } - } -} - -extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentInterface { - -} diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index f1b42dad3a..3fc1a405bb 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -80,13 +80,14 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele private let settings = DataBrokerProtectionSettings() private var cancellables = Set() private var statusBarMenu: StatusBarMenu? + private var manager: DataBrokerProtectionAgentManager? @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) - let manager = DataBrokerProtectionAgentManager.shared - manager.agentFinishedLaunching() + manager = DataBrokerProtectionAgentManagerProvider.agentManager() + manager?.agentFinishedLaunching() setupStatusBarMenu() } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 6bfa6a9d8e..34c1eaaecb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -541,83 +541,6 @@ final class FakeStageDurationCalculator: StageDurationCalculator { } } -/* - I wasn't able to import this mock from the background agent project, so I had to re-use it here. - */ -private final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { - - var data: Data { - let configString = """ - { - "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1693838894358, - "features": { - "brokerProtection": { - "state": "enabled", - "exceptions": [], - "settings": {} - } - }, - "unprotectedTemporary": [] - } - """ - let data = configString.data(using: .utf8) - return data! - } - - var currentConfig: Data { - data - } - - var updatesPublisher: AnyPublisher = .init(Just(())) - - var privacyConfig: BrowserServicesKit.PrivacyConfiguration { - guard let privacyConfigurationData = try? PrivacyConfigurationData(data: data) else { - fatalError("Could not retrieve privacy configuration data") - } - let privacyConfig = privacyConfiguration(withData: privacyConfigurationData, - internalUserDecider: internalUserDecider, - toggleProtectionsCounter: toggleProtectionsCounter) - return privacyConfig - } - - var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) - - var toggleProtectionsCounter: ToggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in - }) - - func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { - .downloaded - } -} - -func privacyConfiguration(withData data: PrivacyConfigurationData, - internalUserDecider: InternalUserDecider, - toggleProtectionsCounter: ToggleProtectionsCounter) -> PrivacyConfiguration { - let domain = MockDomainsProtectionStore() - return AppPrivacyConfiguration(data: data, - identifier: UUID().uuidString, - localProtection: domain, - internalUserDecider: internalUserDecider, - toggleProtectionsCounter: toggleProtectionsCounter) -} - -final class MockDomainsProtectionStore: DomainsProtectionStore { - var unprotectedDomains = Set() - - func disableProtection(forDomain domain: String) { - unprotectedDomains.insert(domain) - } - - func enableProtection(forDomain domain: String) { - unprotectedDomains.remove(domain) - } -} - -final class InternalUserDeciderStoreMock: InternalUserStoring { - var isInternalUser: Bool = false -} - extension DataBroker { func toJSONString() -> String { do { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift similarity index 86% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift index 392508440a..72d630b0fc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionAgentInterface.swift +// DataBrokerProtectionAppToAgentInterface.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,10 +18,9 @@ import Foundation -public enum DataBrokerProtectionAgentInterfaceError: Error { +public enum DataBrokerProtectionAppToAgentInterfaceError: Error { case loginItemDoesNotHaveNecessaryPermissions case appInWrongDirectory - case operationsInterrupted } @objc @@ -47,9 +46,7 @@ public class DataBrokerProtectionAgentErrorCollection: NSObject, NSSecureCoding // MARK: - NSSecureCoding - public static var supportsSecureCoding: Bool { - return true - } + public static let supportsSecureCoding = true public func encode(with coder: NSCoder) { coder.encode(oneTimeError, forKey: NSSecureCodingKeys.oneTimeError) @@ -64,7 +61,6 @@ public class DataBrokerProtectionAgentErrorCollection: NSObject, NSSecureCoding public protocol DataBrokerProtectionAgentAppEvents { func profileSaved() - func dataDeleted() func appLaunched() } @@ -76,6 +72,6 @@ public protocol DataBrokerProtectionAgentDebugCommands { func getDebugMetadata() async -> DBPBackgroundAgentMetadata? } -public protocol DataBrokerProtectionAgentInterface: AnyObject, DataBrokerProtectionAgentAppEvents, DataBrokerProtectionAgentDebugCommands { +public protocol DataBrokerProtectionAppToAgentInterface: AnyObject, DataBrokerProtectionAgentAppEvents, DataBrokerProtectionAgentDebugCommands { } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 9f8b565bc1..22ba75d0af 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -94,31 +94,13 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { public func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in server.profileSaved(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) - }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! - }) - } - - public func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { - xpc.execute(call: { server in - server.dataDeleted(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) - }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! - }) + }, xpcReplyErrorHandler: xpcMessageReceivedCompletion) } public func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in server.appLaunched(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) - }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! - }) + }, xpcReplyErrorHandler: xpcMessageReceivedCompletion) } // MARK: - DataBrokerProtectionAgentDebugCommands diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 7213f09986..0e7ca83d81 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -85,7 +85,6 @@ public protocol IPCServerInterface: AnyObject, DataBrokerProtectionAgentDebugCom // MARK: - DataBrokerProtectionAgentAppEvents func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) - func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) } @@ -105,7 +104,6 @@ protocol XPCServerInterface { // MARK: - DataBrokerProtectionAgentAppEvents func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) - func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) // MARK: - DataBrokerProtectionAgentDebugCommands @@ -120,12 +118,18 @@ protocol XPCServerInterface { func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } -public final class DataBrokerProtectionIPCServer { +protocol DataBrokerProtectionIPCServer: IPCClientInterface, XPCServerInterface { + var serverDelegate: DataBrokerProtectionAppToAgentInterface? { get set } + + init(machServiceName: String) + + func activate() +} + +public final class DefaultDataBrokerProtectionIPCServer: DataBrokerProtectionIPCServer { let xpc: XPCServer - /// The delegate. - /// - public weak var serverDelegate: DataBrokerProtectionAgentInterface? + public weak var serverDelegate: DataBrokerProtectionAppToAgentInterface? public init(machServiceName: String) { let clientInterface = NSXPCInterface(with: XPCClientInterface.self) @@ -139,6 +143,8 @@ public final class DataBrokerProtectionIPCServer { xpc.delegate = self } + // DataBrokerProtectionIPCServer + public func activate() { xpc.activate() } @@ -146,15 +152,15 @@ public final class DataBrokerProtectionIPCServer { // MARK: - Outgoing communication to the clients -extension DataBrokerProtectionIPCServer: IPCClientInterface { +extension DefaultDataBrokerProtectionIPCServer: IPCClientInterface { } // MARK: - Incoming communication from a client -extension DataBrokerProtectionIPCServer: XPCServerInterface { +extension DefaultDataBrokerProtectionIPCServer: XPCServerInterface { func register() { - + } // MARK: - DataBrokerProtectionAgentAppEvents @@ -164,11 +170,6 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { serverDelegate?.profileSaved() } - func dataDeleted(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { - xpcMessageReceivedCompletion(nil) - serverDelegate?.dataDeleted() - } - func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpcMessageReceivedCompletion(nil) serverDelegate?.appLaunched() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index b66973d3d1..926ee58947 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -29,7 +29,7 @@ protocol DBPUIScanOps: AnyObject { final class DBPUIViewModel { private let dataManager: DataBrokerProtectionDataManaging - private let agentInterface: DataBrokerProtectionAgentInterface + private let agentInterface: DataBrokerProtectionAppToAgentInterface private let privacyConfig: PrivacyConfigurationManaging? private let prefs: ContentScopeProperties? @@ -39,7 +39,7 @@ final class DBPUIViewModel { private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() init(dataManager: DataBrokerProtectionDataManaging, - agentInterface: DataBrokerProtectionAgentInterface, + agentInterface: DataBrokerProtectionAppToAgentInterface, webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable, privacyConfig: PrivacyConfigurationManaging? = nil, prefs: ContentScopeProperties? = nil, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 8d76634d42..5389404896 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -21,7 +21,7 @@ import Common protocol DataBrokerOperationDependencies { var database: DataBrokerProtectionRepository { get } - var config: DataBrokerProtectionProcessorConfiguration { get } + var config: DataBrokerExecutionConfig { get } var runnerProvider: JobRunnerProvider { get } var notificationCenter: NotificationCenter { get } var pixelHandler: EventMapping { get } @@ -30,7 +30,7 @@ protocol DataBrokerOperationDependencies { struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { let database: DataBrokerProtectionRepository - var config: DataBrokerProtectionProcessorConfiguration + var config: DataBrokerExecutionConfig let runnerProvider: JobRunnerProvider let notificationCenter: NotificationCenter let pixelHandler: EventMapping diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index dd4f4c1c4f..56c10044c5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -98,39 +98,22 @@ public enum DataBrokerProtectionPixels { // Backgrond Agent events case backgroundAgentStarted case backgroundAgentStartedStoppingDueToAnotherInstanceRunning - case backgroundAgentRunOperationsAndStartSchedulerIfPossible - case backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile - // There's currently no point firing this because the scheduler never calls the completion with an error - // case backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackError(error: Error) - case backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler // IPC server events - case ipcServerStartSchedulerCalledByApp - case ipcServerStartSchedulerReceivedByAgent - case ipcServerStartSchedulerXPCError(error: Error?) - - case ipcServerStopSchedulerCalledByApp - case ipcServerStopSchedulerReceivedByAgent - case ipcServerStopSchedulerXPCError(error: Error?) - - case ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions - case ipcServerScanAllBrokersAttemptedToCallInWrongDirectory - case ipcServerScanAllBrokersCalledByApp - case ipcServerScanAllBrokersReceivedByAgent - case ipcServerScanAllBrokersXPCError(error: Error?) - - case ipcServerScanAllBrokersCompletedOnAgentWithoutError - case ipcServerScanAllBrokersCompletedOnAgentWithError(error: Error?) - case ipcServerScanAllBrokersCompletionCalledOnAppWithoutError - case ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: Error?) - case ipcServerScanAllBrokersInterruptedOnAgent - case ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption - - case ipcServerOptOutAllBrokers - case ipcServerOptOutAllBrokersCompletion(error: Error?) - case ipcServerStartScheduledOperations - case ipcServerStartScheduledOperationsCompletion(error: Error?) - case ipcServerRunAllOperations + case ipcServerProfileSavedCalledByApp + case ipcServerProfileSavedReceivedByAgent + case ipcServerProfileSavedXPCError(error: Error?) + case ipcServerImmediateScansInterrupted + case ipcServerImmediateScansFinishedWithoutError + case ipcServerImmediateScansFinishedWithError(error: Error?) + + case ipcServerAppLaunchedCalledByApp + case ipcServerAppLaunchedReceivedByAgent + case ipcServerAppLaunchedXPCError(error: Error?) + case ipcServerAppLaunchedScheduledScansBlocked + case ipcServerAppLaunchedScheduledScansInterrupted + case ipcServerAppLaunchedScheduledScansFinishedWithoutError + case ipcServerAppLaunchedScheduledScansFinishedWithError(error: Error?) // DataBrokerProtection User Notifications case dataBrokerProtectionNotificationSentFirstScanComplete @@ -219,35 +202,21 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentStarted: return "m_mac_dbp_background-agent_started" case .backgroundAgentStartedStoppingDueToAnotherInstanceRunning: return "m_mac_dbp_background-agent_started_stopping-due-to-another-instance-running" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossible: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_no-saved-profile" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" - - case .ipcServerStartSchedulerCalledByApp: return "m_mac_dbp_ipc-server_start-scheduler_called-by-app" - case .ipcServerStartSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_start-scheduler_received-by-agent" - case .ipcServerStartSchedulerXPCError: return "m_mac_dbp_ipc-server_start-scheduler_xpc-error" - - case .ipcServerStopSchedulerCalledByApp: return "m_mac_dbp_ipc-server_stop-scheduler_called-by-app" - case .ipcServerStopSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_stop-scheduler_received-by-agent" - case .ipcServerStopSchedulerXPCError: return "m_mac_dbp_ipc-server_stop-scheduler_xpc-error" - - case .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions: return "m_mac_dbp_ipc-server_scan-all-brokers_attempted-to-call-without-login-item-permissions" - case .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory: return "m_mac_dbp_ipc-server_scan-all-brokers_attempted-to-call-in-wrong-directory" - case .ipcServerScanAllBrokersCalledByApp: return "m_mac_dbp_ipc-server_scan-all-brokers_called-by-app" - case .ipcServerScanAllBrokersReceivedByAgent: return "m_mac_dbp_ipc-server_scan-all-brokers_received-by-agent" - case .ipcServerScanAllBrokersXPCError: return "m_mac_dbp_ipc-server_scan-all-brokers_xpc-error" - case .ipcServerScanAllBrokersCompletedOnAgentWithoutError: return "m_mac_dbp_ipc-server_scan-all-brokers_completed-on-agent_without-error" - case .ipcServerScanAllBrokersCompletedOnAgentWithError: return "m_mac_dbp_ipc-server_scan-all-brokers_completed-on-agent_with-error" - case .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_without-error" - case .ipcServerScanAllBrokersCompletionCalledOnAppWithError: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_with-error" - case .ipcServerScanAllBrokersInterruptedOnAgent: return "m_mac_dbp_ipc-server_scan-all-brokers_interrupted-on-agent" - case .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_after-interruption" - - case .ipcServerOptOutAllBrokers: return "m_mac_dbp_ipc-server_opt-out-all-brokers" - case .ipcServerOptOutAllBrokersCompletion: return "m_mac_dbp_ipc-server_opt-out-all-brokers_completion" - case .ipcServerStartScheduledOperations: return "m_mac_dbp_ipc-server_run-queued-operations" - case .ipcServerStartScheduledOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" - case .ipcServerRunAllOperations: return "m_mac_dbp_ipc-server_run-all-operations" + // IPC Server Pixels + case .ipcServerProfileSavedCalledByApp: return "m_mac_dbp_ipc-server_profile-saved_called-by-app" + case .ipcServerProfileSavedReceivedByAgent: return "m_mac_dbp_ipc-server_profile-saved_received-by-agent" + case .ipcServerProfileSavedXPCError: return "m_mac_dbp_ipc-server_profile-saved_xpc-error" + case .ipcServerImmediateScansInterrupted: return "m_mac_dbp_ipc-server_immediate-scans_interrupted" + case .ipcServerImmediateScansFinishedWithoutError: return "m_mac_dbp_ipc-server_immediate-scans_finished_without-error" + case .ipcServerImmediateScansFinishedWithError: return "m_mac_dbp_ipc-server_immediate-scans_finished_with-error" + + case .ipcServerAppLaunchedCalledByApp: return "m_mac_dbp_ipc-server_app-launched_called-by-app" + case .ipcServerAppLaunchedReceivedByAgent: return "m_mac_dbp_ipc-server_app-launched_received-by-agent" + case .ipcServerAppLaunchedXPCError: return "m_mac_dbp_ipc-server_app-launched_xpc-error" + case .ipcServerAppLaunchedScheduledScansBlocked: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_blocked" + case .ipcServerAppLaunchedScheduledScansInterrupted: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_interrupted" + case .ipcServerAppLaunchedScheduledScansFinishedWithoutError: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_finished_without-error" + case .ipcServerAppLaunchedScheduledScansFinishedWithError: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_finished_with-error" // User Notifications case .dataBrokerProtectionNotificationSentFirstScanComplete: @@ -371,9 +340,6 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .webUILoadingFailed(let error): return [Consts.errorCategoryKey: error] case .backgroundAgentStarted, - .backgroundAgentRunOperationsAndStartSchedulerIfPossible, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler, .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, .dataBrokerProtectionNotificationSentFirstScanComplete, .dataBrokerProtectionNotificationOpenedFirstScanComplete, @@ -398,28 +364,19 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .secureVaultInitError, .secureVaultError: return [:] - case .ipcServerStartSchedulerCalledByApp, - .ipcServerStartSchedulerReceivedByAgent, - .ipcServerStartSchedulerXPCError, - .ipcServerStopSchedulerCalledByApp, - .ipcServerStopSchedulerReceivedByAgent, - .ipcServerStopSchedulerXPCError, - .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, - .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, - .ipcServerScanAllBrokersCalledByApp, - .ipcServerScanAllBrokersReceivedByAgent, - .ipcServerScanAllBrokersXPCError, - .ipcServerScanAllBrokersCompletedOnAgentWithoutError, - .ipcServerScanAllBrokersCompletedOnAgentWithError, - .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, - .ipcServerScanAllBrokersCompletionCalledOnAppWithError, - .ipcServerScanAllBrokersInterruptedOnAgent, - .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption, - .ipcServerOptOutAllBrokers, - .ipcServerOptOutAllBrokersCompletion, - .ipcServerStartScheduledOperations, - .ipcServerStartScheduledOperationsCompletion, - .ipcServerRunAllOperations: + case .ipcServerProfileSavedCalledByApp, + .ipcServerProfileSavedReceivedByAgent, + .ipcServerProfileSavedXPCError, + .ipcServerImmediateScansInterrupted, + .ipcServerImmediateScansFinishedWithoutError, + .ipcServerImmediateScansFinishedWithError, + .ipcServerAppLaunchedCalledByApp, + .ipcServerAppLaunchedReceivedByAgent, + .ipcServerAppLaunchedXPCError, + .ipcServerAppLaunchedScheduledScansBlocked, + .ipcServerAppLaunchedScheduledScansInterrupted, + .ipcServerAppLaunchedScheduledScansFinishedWithoutError, + .ipcServerAppLaunchedScheduledScansFinishedWithError: return [Consts.bundleIDParamKey: Bundle.main.bundleIdentifier ?? "nil"] case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isImmediateOperation): return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] @@ -464,26 +421,20 @@ public class DataBrokerProtectionPixelsHandler: EventMapping = .init(Just(())) + public var updatesPublisher: AnyPublisher = .init(Just(())) - var privacyConfig: BrowserServicesKit.PrivacyConfiguration { + public var privacyConfig: BrowserServicesKit.PrivacyConfiguration { guard let privacyConfigurationData = try? PrivacyConfigurationData(data: data) else { fatalError("Could not retrieve privacy configuration data") } @@ -63,11 +63,11 @@ final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { return privacyConfig } - var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) - var toggleProtectionsCounter: ToggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in + public var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) + public var toggleProtectionsCounter: ToggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in }) - func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + public func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { .downloaded } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift similarity index 79% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift index 2914b43f5e..7720725d85 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionProcessorConfiguration.swift +// DataBrokerExecutionConfig.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,13 +18,12 @@ import Foundation -struct DataBrokerProtectionProcessorConfiguration { - // Arbitrary numbers for now +public struct DataBrokerExecutionConfig { let intervalBetweenSameBrokerOperations: TimeInterval = 2 + private let concurrentOperationsDifferentBrokers: Int = 2 // https://app.asana.com/0/481882893211075/1206981742767469/f private let concurrentOperationsOnManualScans: Int = 6 - func concurrentOperationsFor(_ operation: OperationType) -> Int { switch operation { case .all, .optOut: @@ -33,4 +32,8 @@ struct DataBrokerProtectionProcessorConfiguration { return concurrentOperationsOnManualScans } } + + let activitySchedulerTriggerInterval: TimeInterval = 20 * 60 // 20 minutes + let activitySchedulerIntervalTolerance: TimeInterval = 10 * 60 // 10 minutes + let activitySchedulerQOS: QualityOfService = .background } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift new file mode 100644 index 0000000000..1f88d072c2 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -0,0 +1,285 @@ +// +// DataBrokerProtectionAgentManager.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 Common +import BrowserServicesKit +import PixelKit + +// This is to avoid exposing all the dependancies outside of the DBP package +public class DataBrokerProtectionAgentManagerProvider { + // swiftlint:disable:next function_body_length + public static func agentManager() -> DataBrokerProtectionAgentManager { + let pixelHandler = DataBrokerProtectionPixelsHandler() + + let executionConfig = DataBrokerExecutionConfig() + let activityScheduler = DefaultDataBrokerProtectionBackgroundActivityScheduler(config: executionConfig) + + let notificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) + let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned + let ipcServer = DefaultDataBrokerProtectionIPCServer(machServiceName: Bundle.main.bundleIdentifier!) + + let features = ContentScopeFeatureToggles(emailProtection: false, + emailProtectionIncontextSignup: false, + credentialsAutofill: false, + identitiesAutofill: false, + creditCardsAutofill: false, + credentialsSaving: false, + passwordGeneration: false, + inlineIconCredentials: false, + thirdPartyCredentialsProvider: false) + let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, + sessionKey: UUID().uuidString, + featureToggles: features) + + let fakeBroker = DataBrokerDebugFlagFakeBroker() + let dataManager = DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBroker) + + let operationQueue = OperationQueue() + let operationsBuilder = DefaultDataBrokerOperationsCreator() + let mismatchCalculator = DefaultMismatchCalculator(database: dataManager.database, + pixelHandler: pixelHandler) + + var brokerUpdater: DataBrokerProtectionBrokerUpdater? + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + brokerUpdater = DefaultDataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) + } + let queueManager = DefaultDataBrokerProtectionQueueManager(operationQueue: operationQueue, + operationsCreator: operationsBuilder, + mismatchCalculator: mismatchCalculator, + brokerUpdater: brokerUpdater, + pixelHandler: pixelHandler) + + let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), + authenticationRepository: KeychainAuthenticationData()) + let emailService = EmailService(redeemUseCase: redeemUseCase) + let captchaService = CaptchaService(redeemUseCase: redeemUseCase) + let runnerProvider = DataBrokerJobRunnerProvider(privacyConfigManager: privacyConfigurationManager, + contentScopeProperties: contentScopeProperties, + emailService: emailService, + captchaService: captchaService) + let operationDependencies = DefaultDataBrokerOperationDependencies( + database: dataManager.database, + config: executionConfig, + runnerProvider: runnerProvider, + notificationCenter: NotificationCenter.default, + pixelHandler: pixelHandler, + userNotificationService: notificationService) + + return DataBrokerProtectionAgentManager( + userNotificationService: notificationService, + activityScheduler: activityScheduler, + ipcServer: ipcServer, + queueManager: queueManager, + dataManager: dataManager, + operationDependencies: operationDependencies, + pixelHandler: pixelHandler) + } +} + +public final class DataBrokerProtectionAgentManager { + + private let userNotificationService: DataBrokerProtectionUserNotificationService + private var activityScheduler: DataBrokerProtectionBackgroundActivityScheduler + private var ipcServer: DataBrokerProtectionIPCServer + private let queueManager: DataBrokerProtectionQueueManager + private let dataManager: DataBrokerProtectionDataManaging + private let operationDependencies: DataBrokerOperationDependencies + private let pixelHandler: EventMapping + + // Used for debug functions only, so not injected + private lazy var browserWindowManager = BrowserWindowManager() + + private var didStartActivityScheduler = false + + init(userNotificationService: DataBrokerProtectionUserNotificationService, + activityScheduler: DataBrokerProtectionBackgroundActivityScheduler, + ipcServer: DataBrokerProtectionIPCServer, + queueManager: DataBrokerProtectionQueueManager, + dataManager: DataBrokerProtectionDataManaging, + operationDependencies: DataBrokerOperationDependencies, + pixelHandler: EventMapping) { + self.userNotificationService = userNotificationService + self.activityScheduler = activityScheduler + self.ipcServer = ipcServer + self.queueManager = queueManager + self.dataManager = dataManager + self.operationDependencies = operationDependencies + self.pixelHandler = pixelHandler + + self.activityScheduler.delegate = self + self.ipcServer.serverDelegate = self + self.ipcServer.activate() + } + + public func agentFinishedLaunching() { + + do { + // If there's no saved profile we don't need to start the scheduler + // Theoretically this should never happen, if there's no data, the agent shouldn't be running + guard (try dataManager.fetchProfile()) != nil else { + return + } + } catch { + os_log("Error during AgentManager.agentFinishedLaunching when trying to fetchProfile, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + return + } + + activityScheduler.startScheduler() + didStartActivityScheduler = true + queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionBackgroundActivitySchedulerDelegate { + + public func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtection.DataBrokerProtectionBackgroundActivityScheduler) { + queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { + + public func profileSaved() { + let backgroundAgentInitialScanStartTime = Date() + + userNotificationService.requestNotificationPermission() + queueManager.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in + guard let self = self else { return } + + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + switch oneTimeError { + case DataBrokerProtectionQueueError.interrupted: + self.pixelHandler.fire(.ipcServerImmediateScansInterrupted) + os_log("Interrupted during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + default: + self.pixelHandler.fire(.ipcServerImmediateScansFinishedWithError(error: oneTimeError)) + os_log("Error during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + + if errors?.oneTimeError == nil { + self.pixelHandler.fire(.ipcServerImmediateScansFinishedWithoutError) + self.userNotificationService.sendFirstScanCompletedNotification() + } + + if let hasMatches = try? self.dataManager.hasMatches(), + hasMatches { + self.userNotificationService.scheduleCheckInNotificationIfPossible() + } + + fireImmediateScansCompletionPixel(startTime: backgroundAgentInitialScanStartTime) + } + } + + public func appLaunched() { + queueManager.startScheduledOperationsIfPermitted(showWebView: false, + operationDependencies: + operationDependencies) { [weak self] errors in + guard let self = self else { return } + + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + switch oneTimeError { + case DataBrokerProtectionQueueError.interrupted: + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansInterrupted) + os_log("Interrupted during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + case DataBrokerProtectionQueueError.cannotInterrupt: + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansBlocked) + os_log("Cannot interrupt during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted()") + default: + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansFinishedWithError(error: oneTimeError)) + os_log("Error during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + + if errors?.oneTimeError == nil { + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansFinishedWithoutError) + } + } + } + + private func fireImmediateScansCompletionPixel(startTime: Date) { + do { + let profileQueries = try dataManager.profileQueriesCount() + let durationSinceStart = Date().timeIntervalSince(startTime) * 1000 + self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), + profileQueries: profileQueries)) + } catch { + os_log("Initial Scans Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) + } + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentDebugCommands { + public func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } + + public func startImmediateOperations(showWebView: Bool) { + queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: nil) + } + + public func startScheduledOperations(showWebView: Bool) { + queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: nil) + } + + public func runAllOptOuts(showWebView: Bool) { + queueManager.execute(.startOptOutOperations(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: nil)) + } + + public func getDebugMetadata() async -> DataBrokerProtection.DBPBackgroundAgentMetadata? { + + if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { + + return DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", + isAgentRunning: true, + agentSchedulerState: queueManager.debugRunningStatusString, + lastSchedulerSessionStartTimestamp: activityScheduler.lastTriggerTimestamp?.timeIntervalSince1970) + } else { + return DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", + isAgentRunning: true, + agentSchedulerState: queueManager.debugRunningStatusString, + lastSchedulerSessionStartTimestamp: activityScheduler.lastTriggerTimestamp?.timeIntervalSince1970) + } + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAppToAgentInterface { + +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift new file mode 100644 index 0000000000..9467c83b92 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift @@ -0,0 +1,58 @@ +// +// DataBrokerProtectionBackgroundActivityScheduler.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 + +public protocol DataBrokerProtectionBackgroundActivityScheduler { + func startScheduler() + var delegate: DataBrokerProtectionBackgroundActivitySchedulerDelegate? { get set } + + var lastTriggerTimestamp: Date? { get } +} + +public protocol DataBrokerProtectionBackgroundActivitySchedulerDelegate: AnyObject { + func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtectionBackgroundActivityScheduler) +} + +public final class DefaultDataBrokerProtectionBackgroundActivityScheduler: DataBrokerProtectionBackgroundActivityScheduler { + + private let activity: NSBackgroundActivityScheduler + private let schedulerIdentifier = "com.duckduckgo.macos.browser.databroker-protection-scheduler" + + public weak var delegate: DataBrokerProtectionBackgroundActivitySchedulerDelegate? + public private(set) var lastTriggerTimestamp: Date? + + public init(config: DataBrokerExecutionConfig) { + activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) + activity.repeats = true + activity.interval = config.activitySchedulerTriggerInterval + activity.tolerance = config.activitySchedulerIntervalTolerance + activity.qualityOfService = config.activitySchedulerQOS + } + + public func startScheduler() { + activity.schedule { _ in + + self.lastTriggerTimestamp = Date() + os_log("Scheduler running...", log: .dataBrokerProtection) + self.delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index ab21bd6d35..c3e8ce0f88 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -56,6 +56,7 @@ enum DataBrokerProtectionQueueMode { enum DataBrokerProtectionQueueError: Error { case cannotInterrupt + case interrupted } enum DataBrokerProtectionQueueManagerDebugCommand { @@ -79,9 +80,8 @@ protocol DataBrokerProtectionQueueManager { operationDependencies: DataBrokerOperationDependencies, completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) - func stopAllOperations() - func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) + var debugRunningStatusString: String { get } } final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueManager { @@ -95,6 +95,16 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa private var mode = DataBrokerProtectionQueueMode.idle private var operationErrors: [Error] = [] + var debugRunningStatusString: String { + switch mode { + case .idle: + return "idle" + case .immediate(let completion), + .scheduled(let completion): + return "running" + } + } + init(operationQueue: DataBrokerProtectionOperationQueue, operationsCreator: DataBrokerOperationsCreator, mismatchCalculator: MismatchCalculator, @@ -133,10 +143,6 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa completion: completion) } - func stopAllOperations() { - cancelCurrentModeAndResetIfNeeded() - } - func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) { guard case .startOptOutOperations(let showWebView, let operationDependencies, @@ -183,7 +189,8 @@ private extension DefaultDataBrokerProtectionQueueManager { switch mode { case .immediate(let completion), .scheduled(let completion): operationQueue.cancelAllOperations() - completion?(errorCollectionForCurrentOperations()) + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: DataBrokerProtectionQueueError.interrupted, operationErrors: operationErrorsForCurrentOperations()) + completion?(errorCollection) resetModeAndClearErrors() default: break @@ -227,14 +234,14 @@ private extension DefaultDataBrokerProtectionQueueManager { } operationQueue.addBarrierBlock { [weak self] in - let errorCollection = self?.errorCollectionForCurrentOperations() + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: nil, operationErrors: self?.operationErrorsForCurrentOperations()) completion?(errorCollection) self?.resetModeAndClearErrors() } } - func errorCollectionForCurrentOperations() -> DataBrokerProtectionAgentErrorCollection? { - return operationErrors.count != 0 ? DataBrokerProtectionAgentErrorCollection(operationErrors: operationErrors) : nil + func operationErrorsForCurrentOperations() -> [Error]? { + return operationErrors.count != 0 ? operationErrors : nil } func firePixels(operationDependencies: DataBrokerOperationDependencies) { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift deleted file mode 100644 index aa8fc9e004..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ /dev/null @@ -1,296 +0,0 @@ -// -// DataBrokerProtectionScheduler.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 Common -import BrowserServicesKit -import Combine - -public enum DataBrokerProtectionSchedulerStatus: Codable { - case stopped - case idle - case running -} - -public final class DefaultDataBrokerProtectionScheduler { - - private enum SchedulerCycle { - // Arbitrary numbers for now - - static let interval: TimeInterval = 40 * 60 // 40 minutes - static let tolerance: TimeInterval = 20 * 60 // 20 minutes - } - - private enum DataBrokerProtectionCurrentOperation { - case idle - case queued - case manualScan - case optOutAll - case all - } - - private let privacyConfigManager: PrivacyConfigurationManaging - private let contentScopeProperties: ContentScopeProperties - private let dataManager: DataBrokerProtectionDataManager - private let activity: NSBackgroundActivityScheduler - private let pixelHandler: EventMapping - private let schedulerIdentifier = "com.duckduckgo.macos.browser.databroker-protection-scheduler" - private let notificationCenter: NotificationCenter - private let emailService: EmailServiceProtocol - private let captchaService: CaptchaServiceProtocol - private let userNotificationService: DataBrokerProtectionUserNotificationService - private var currentOperation: DataBrokerProtectionCurrentOperation = .idle - - /// Ensures that only one scheduler operation is executed at the same time. - /// - private let schedulerDispatchQueue = DispatchQueue(label: "schedulerDispatchQueue", qos: .background) - - @Published public var status: DataBrokerProtectionSchedulerStatus = .stopped - - public var statusPublisher: Published.Publisher { $status } - - public var lastSchedulerSessionStartTimestamp: Date? - - private lazy var queueManager: DataBrokerProtectionQueueManager = { - let operationQueue = OperationQueue() - let operationsBuilder = DefaultDataBrokerOperationsCreator() - let mismatchCalculator = DefaultMismatchCalculator(database: dataManager.database, - pixelHandler: pixelHandler) - - var brokerUpdater: DataBrokerProtectionBrokerUpdater? - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { - brokerUpdater = DefaultDataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) - } - - return DefaultDataBrokerProtectionQueueManager(operationQueue: operationQueue, - operationsCreator: operationsBuilder, - mismatchCalculator: mismatchCalculator, - brokerUpdater: brokerUpdater, - pixelHandler: pixelHandler) - }() - - private lazy var operationDependencies: DataBrokerOperationDependencies = { - let runnerProvider = DataBrokerJobRunnerProvider(privacyConfigManager: privacyConfigManager, - contentScopeProperties: contentScopeProperties, - emailService: emailService, - captchaService: captchaService) - - return DefaultDataBrokerOperationDependencies(database: dataManager.database, - config: DataBrokerProtectionProcessorConfiguration(), - runnerProvider: runnerProvider, - notificationCenter: notificationCenter, - pixelHandler: pixelHandler, userNotificationService: userNotificationService) - }() - - public init(privacyConfigManager: PrivacyConfigurationManaging, - contentScopeProperties: ContentScopeProperties, - dataManager: DataBrokerProtectionDataManager, - notificationCenter: NotificationCenter = NotificationCenter.default, - pixelHandler: EventMapping, - redeemUseCase: DataBrokerProtectionRedeemUseCase, - userNotificationService: DataBrokerProtectionUserNotificationService - ) { - activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) - activity.repeats = true - activity.interval = SchedulerCycle.interval - activity.tolerance = SchedulerCycle.tolerance - activity.qualityOfService = QualityOfService.default - - self.dataManager = dataManager - self.privacyConfigManager = privacyConfigManager - self.contentScopeProperties = contentScopeProperties - self.pixelHandler = pixelHandler - self.notificationCenter = notificationCenter - self.userNotificationService = userNotificationService - - self.emailService = EmailService(redeemUseCase: redeemUseCase) - self.captchaService = CaptchaService(redeemUseCase: redeemUseCase) - } - - public func startScheduler(showWebView: Bool = false) { - guard status == .stopped else { - os_log("Trying to start scheduler when it's already running, returning...", log: .dataBrokerProtection) - return - } - - status = .idle - activity.schedule { completion in - guard self.status != .stopped else { - os_log("Activity started when scheduler was already running, returning...", log: .dataBrokerProtection) - completion(.finished) - return - } - - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - completion(.finished) - return - } - self.lastSchedulerSessionStartTimestamp = Date() - self.status = .running - os_log("Scheduler running...", log: .dataBrokerProtection) - self.currentOperation = .queued - self.queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, operationDependencies: self.operationDependencies) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during startScheduler in dataBrokerProcessor.startScheduledOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduler")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during startScheduler in dataBrokerProcessor.startScheduledOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.status = .idle - self?.currentOperation = .idle - completion(.finished) - } - } - } - - public func stopScheduler() { - os_log("Stopping scheduler...", log: .dataBrokerProtection) - activity.invalidate() - status = .stopped - queueManager.stopAllOperations() - } - - public func startScheduledOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Running queued operations...", log: .dataBrokerProtection) - self.currentOperation = .queued - queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, - operationDependencies: operationDependencies, - completion: { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.startScheduledOperations in dataBrokerProcessor.startScheduledOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduledOperations")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startScheduledOperations in dataBrokerProcessor.startScheduledOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - completion?(errors) - self?.currentOperation = .idle - }) - - } - - public func startImmediateOperations(showWebView: Bool = false, - startTime: Date, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { - pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) - let backgroundAgentManualScanStartTime = Date() - stopScheduler() - - userNotificationService.requestNotificationPermission() - self.currentOperation = .manualScan - os_log("Scanning all brokers...", log: .dataBrokerProtection) - queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, - operationDependencies: operationDependencies) { [weak self] errors in - guard let self = self else { return } - - self.startScheduler(showWebView: showWebView) - - if errors?.oneTimeError == nil { - self.userNotificationService.sendFirstScanCompletedNotification() - } - - if let hasMatches = try? self.dataManager.hasMatches(), - hasMatches { - self.userNotificationService.scheduleCheckInNotificationIfPossible() - } - - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - switch oneTimeError { - case DataBrokerProtectionAgentInterfaceError.operationsInterrupted: - os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - default: - os_log("Error during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startImmediateOperations")) - } - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self.currentOperation = .idle - fireManualScanCompletionPixel(startTime: backgroundAgentManualScanStartTime) - completion?(errors) - } - } - - private func fireManualScanCompletionPixel(startTime: Date) { - do { - let profileQueries = try dataManager.profileQueriesCount() - let durationSinceStart = Date().timeIntervalSince(startTime) * 1000 - self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), - profileQueries: profileQueries)) - } catch { - os_log("Manual Scan Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) - } - } - - public func optOutAllBrokers(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Opting out all brokers...", log: .dataBrokerProtection) - self.currentOperation = .optOutAll - queueManager.execute(.startOptOutOperations(showWebView: showWebView, operationDependencies: operationDependencies) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.optOutAllBrokers")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.currentOperation = .idle - completion?(errors) - }) - } -} - -public extension DataBrokerProtectionSchedulerStatus { - var toString: String { - switch self { - case .idle: - return "idle" - case .running: - return "running" - case .stopped: - return "stopped" - } - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index 9a240074b3..95007d8aab 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -36,7 +36,7 @@ final public class DataBrokerProtectionViewController: NSViewController { private let openURLHandler: (URL?) -> Void private var reloadObserver: NSObjectProtocol? - public init(agentInterface: DataBrokerProtectionAgentInterface, + public init(agentInterface: DataBrokerProtectionAppToAgentInterface, dataManager: DataBrokerProtectionDataManaging, privacyConfig: PrivacyConfigurationManaging? = nil, prefs: ContentScopeProperties? = nil, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift similarity index 90% rename from LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift rename to LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift index f8e2be4be2..8c7ae5c374 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift @@ -20,9 +20,9 @@ import XCTest import Foundation @testable import DataBrokerProtection -final class DataBrokerProtectionProcessorConfigurationTests: XCTestCase { +final class DataBrokerExecutionConfigTests: XCTestCase { - private let sut = DataBrokerProtectionProcessorConfiguration() + private let sut = DataBrokerExecutionConfig() func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { let value = sut.concurrentOperationsFor(.scan) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift index 42060fdb9b..a19e16863a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -25,24 +25,24 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { // Dependencies private var mockDatabase: MockDatabase! - private var mockSchedulerConfig = DataBrokerProtectionProcessorConfiguration() + private var mockSchedulerConfig = DataBrokerExecutionConfig() private var mockRunnerProvider: MockRunnerProvider! private var mockPixelHandler: MockPixelHandler! - private var mockUserNotification: MockUserNotification! + private var mockUserNotificationService: MockUserNotificationService! var mockDependencies: DefaultDataBrokerOperationDependencies! override func setUpWithError() throws { mockDatabase = MockDatabase() mockRunnerProvider = MockRunnerProvider() mockPixelHandler = MockPixelHandler() - mockUserNotification = MockUserNotification() + mockUserNotificationService = MockUserNotificationService() mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, config: mockSchedulerConfig, runnerProvider: mockRunnerProvider, notificationCenter: .default, pixelHandler: mockPixelHandler, - userNotificationService: mockUserNotification) + userNotificationService: mockUserNotificationService) } func testWhenBuildOperations_andBrokerQueryDataHasDuplicateBrokers_thenDuplicatesAreIgnored() throws { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 7468cfdb9d..bd3aeafb71 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -28,11 +28,11 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let sut = DataBrokerProfileQueryOperationManager() let mockWebOperationRunner = MockWebJobRunner() let mockDatabase = MockDatabase() - let mockUserNotification = MockUserNotification() + let mockUserNotificationService = MockUserNotificationService() override func tearDown() { mockWebOperationRunner.clear() - mockUserNotification.reset() + mockUserNotificationService.reset() } // MARK: - Notification tests @@ -74,11 +74,11 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: mockUserNotification, + userNotificationService: mockUserNotificationService, shouldRunNextStep: { true } ) - XCTAssertTrue(mockUserNotification.allInfoRemovedWasSent) - XCTAssertFalse(mockUserNotification.firstRemovedNotificationWasSent) + XCTAssertTrue(mockUserNotificationService.allInfoRemovedWasSent) + XCTAssertFalse(mockUserNotificationService.firstRemovedNotificationWasSent) } catch { XCTFail("Should not throw") } @@ -125,11 +125,11 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: mockUserNotification, + userNotificationService: mockUserNotificationService, shouldRunNextStep: { true } ) - XCTAssertFalse(mockUserNotification.allInfoRemovedWasSent) - XCTAssertTrue(mockUserNotification.firstRemovedNotificationWasSent) + XCTAssertFalse(mockUserNotificationService.allInfoRemovedWasSent) + XCTAssertTrue(mockUserNotificationService.firstRemovedNotificationWasSent) } catch { XCTFail("Should not throw") } @@ -176,11 +176,11 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: mockUserNotification, + userNotificationService: mockUserNotificationService, shouldRunNextStep: { true } ) - XCTAssertFalse(mockUserNotification.allInfoRemovedWasSent) - XCTAssertFalse(mockUserNotification.firstRemovedNotificationWasSent) + XCTAssertFalse(mockUserNotificationService.allInfoRemovedWasSent) + XCTAssertFalse(mockUserNotificationService.firstRemovedNotificationWasSent) } catch { XCTFail("Should not throw") } @@ -200,7 +200,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -222,7 +222,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id for broker") @@ -243,7 +243,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertEqual(mockDatabase.eventsAdded.first?.type, .scanStarted) @@ -264,7 +264,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .noMatchFound })) @@ -288,7 +288,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasUpdateRemoveDateCalled) @@ -314,7 +314,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasUpdateRemoveDateCalled) @@ -338,7 +338,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasUpdateRemoveDateCalled) @@ -362,7 +362,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasSaveOptOutOperationCalled) @@ -385,7 +385,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutConfirmed })) @@ -410,7 +410,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutConfirmed })) @@ -436,7 +436,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Should throw!") @@ -465,7 +465,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -489,7 +489,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -513,7 +513,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -537,7 +537,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasDatabaseCalled) @@ -561,7 +561,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasDatabaseCalled) @@ -585,7 +585,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutStarted })) @@ -608,7 +608,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutRequested })) @@ -632,7 +632,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Should throw!") @@ -658,7 +658,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) @@ -691,7 +691,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) @@ -730,7 +730,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) if let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last { @@ -769,7 +769,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("The code above should throw") @@ -1059,43 +1059,6 @@ extension ExtractedProfile { } } -final class MockUserNotification: DataBrokerProtectionUserNotificationService { - - var requestPermissionWasAsked = false - var firstScanNotificationWasSent = false - var firstRemovedNotificationWasSent = false - var checkInNotificationWasScheduled = false - var allInfoRemovedWasSent = false - - func requestNotificationPermission() { - requestPermissionWasAsked = true - } - - func sendFirstScanCompletedNotification() { - firstScanNotificationWasSent = true - } - - func sendFirstRemovedNotificationIfPossible() { - firstRemovedNotificationWasSent = true - } - - func sendAllInfoRemovedNotificationIfPossible() { - allInfoRemovedWasSent = true - } - - func scheduleCheckInNotificationIfPossible() { - checkInNotificationWasScheduled = true - } - - func reset() { - requestPermissionWasAsked = false - firstScanNotificationWasSent = false - firstRemovedNotificationWasSent = false - checkInNotificationWasScheduled = false - allInfoRemovedWasSent = false - } -} - extension AttemptInformation { static var mock: AttemptInformation { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift new file mode 100644 index 0000000000..f4d00a5d5c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift @@ -0,0 +1,306 @@ +// +// DataBrokerProtectionAgentManagerTests.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 DataBrokerProtection + +final class DataBrokerProtectionAgentManagerTests: XCTestCase { + + private var sut: DataBrokerProtectionAgentManager! + + private var mockActivityScheduler: MockDataBrokerProtectionBackgroundActivityScheduler! + private var mockNotificationService: MockUserNotificationService! + private var mockQueueManager: MockDataBrokerProtectionOperationQueueManager! + private var mockDataManager: MockDataBrokerProtectionDataManager! + private var mockIPCServer: MockIPCServer! + private var mockPixelHandler: MockPixelHandler! + private var mockDependencies: DefaultDataBrokerOperationDependencies! + private var mockProfile: DataBrokerProtectionProfile! + + override func setUpWithError() throws { + + mockPixelHandler = MockPixelHandler() + mockActivityScheduler = MockDataBrokerProtectionBackgroundActivityScheduler() + mockNotificationService = MockUserNotificationService() + + let mockDatabase = MockDatabase() + let mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) + mockQueueManager = MockDataBrokerProtectionOperationQueueManager( + operationQueue: MockDataBrokerProtectionOperationQueue(), + operationsCreator: MockDataBrokerOperationsCreator(), + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: MockDataBrokerProtectionBrokerUpdater(), + pixelHandler: mockPixelHandler) + + mockIPCServer = MockIPCServer(machServiceName: "") + + let fakeBroker = DataBrokerDebugFlagFakeBroker() + mockDataManager = MockDataBrokerProtectionDataManager(pixelHandler: mockPixelHandler, fakeBrokerFlag: fakeBroker) + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + config: DataBrokerExecutionConfig(), + runnerProvider: MockRunnerProvider(), + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockNotificationService) + + mockProfile = DataBrokerProtectionProfile( + names: [], + addresses: [], + phones: [], + birthYear: 1992) + } + + func testWhenAgentStart_andProfileExists_thenActivityIsScheduled_andSheduledOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = mockProfile + + var schedulerStarted = false + mockActivityScheduler.startSchedulerCompletion = { + schedulerStarted = true + } + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.agentFinishedLaunching() + + // Then + XCTAssertTrue(schedulerStarted) + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenAgentStart_andProfileDoesNotExist_thenActivityIsNotScheduled_andSheduledOpereationsNotRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = nil + + var schedulerStarted = false + mockActivityScheduler.startSchedulerCompletion = { + schedulerStarted = true + } + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.agentFinishedLaunching() + + // Then + XCTAssertFalse(schedulerStarted) + XCTAssertFalse(startScheduledScansCalled) + } + + func testWhenActivitySchedulerTriggers_thenSheduledOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = mockProfile + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + mockActivityScheduler.triggerDelegateCall() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenProfileSaved_thenImmediateOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = mockProfile + + var startImmediateScansCalled = false + mockQueueManager.startImmediateOperationsIfPermittedCalledCompletion = { _ in + startImmediateScansCalled = true + } + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(startImmediateScansCalled) + } + + func testWhenProfileSaved_thenUserNotificationPermissionAsked() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(mockNotificationService.requestPermissionWasAsked) + } + + func testWhenProfileSaved_andScansCompleted_andNoScanError_thenUserNotificationSent() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(mockNotificationService.firstScanNotificationWasSent) + } + + func testWhenProfileSaved_andScansCompleted_andScanError_thenUserNotificationNotSent() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + mockQueueManager.startImmediateOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) + + // When + sut.profileSaved() + + // Then + XCTAssertFalse(mockNotificationService.firstScanNotificationWasSent) + } + + func testWhenProfileSaved_andScansCompleted_andHasMatches_thenCheckInNotificationScheduled() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + mockDataManager.shouldReturnHasMatches = true + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(mockNotificationService.checkInNotificationWasScheduled) + } + + func testWhenProfileSaved_andScansCompleted_andHasNoMatches_thenCheckInNotificationNotScheduled() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + mockDataManager.shouldReturnHasMatches = false + + // When + sut.profileSaved() + + // Then + XCTAssertFalse(mockNotificationService.checkInNotificationWasScheduled) + } + + func testWhenAppLaunched_thenSheduledOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.appLaunched() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift index eb11ce957c..e69d984c64 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift @@ -29,9 +29,9 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { private var mockPixelHandler: MockPixelHandler! private var mockMismatchCalculator: MockMismatchCalculator! private var mockUpdater: MockDataBrokerProtectionBrokerUpdater! - private var mockSchedulerConfig = DataBrokerProtectionProcessorConfiguration() + private var mockSchedulerConfig = DataBrokerExecutionConfig() private var mockRunnerProvider: MockRunnerProvider! - private var mockUserNotification: MockUserNotification! + private var mockUserNotification: MockUserNotificationService! private var mockOperationErrorDelegate: MockDataBrokerOperationErrorDelegate! private var mockDependencies: DefaultDataBrokerOperationDependencies! @@ -43,10 +43,10 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) mockUpdater = MockDataBrokerProtectionBrokerUpdater() mockRunnerProvider = MockRunnerProvider() - mockUserNotification = MockUserNotification() + mockUserNotification = MockUserNotificationService() mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, - config: DataBrokerProtectionProcessorConfiguration(), + config: DataBrokerExecutionConfig(), runnerProvider: mockRunnerProvider, notificationCenter: .default, pixelHandler: mockPixelHandler, @@ -65,7 +65,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] let expectation = expectation(description: "Expected errors to be returned in completion") var errorCollection: DataBrokerProtectionAgentErrorCollection! - let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.scan) + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.scan) // When sut.startImmediateOperationsIfPermitted(showWebView: false, @@ -95,7 +95,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] let expectation = expectation(description: "Expected errors to be returned in completion") var errorCollection: DataBrokerProtectionAgentErrorCollection! - let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.all) + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.all) // When sut.startScheduledOperationsIfPermitted(showWebView: false, @@ -144,6 +144,8 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { // Then XCTAssert(errorCollection.operationErrors?.count == 2) + let error = errorCollection.oneTimeError as? DataBrokerProtectionQueueError + XCTAssertEqual(error, .interrupted) XCTAssert(mockQueue.didCallCancelCount == 1) XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) @@ -180,6 +182,8 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { // Then XCTAssert(errorCollection.operationErrors?.count == 2) + let error = errorCollection.oneTimeError as? DataBrokerProtectionQueueError + XCTAssertEqual(error, .interrupted) XCTAssert(mockQueue.didCallCancelCount == 1) XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) @@ -294,40 +298,6 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssertNotNil(errorCollection.oneTimeError) } - func testWhenOperationsAreRunning_andStopAllIsCalled_thenAllAreCancelled_andCompletionIsCalledWithErrors() async throws { - // Given - sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, - operationsCreator: mockOperationsCreator, - mismatchCalculator: mockMismatchCalculator, - brokerUpdater: mockUpdater, - pixelHandler: mockPixelHandler) - let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, - operationType: .scan, - errorDelegate: sut, - shouldError: true) } - let mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, - operationType: .scan, - errorDelegate: sut) } - mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations - let expectation = expectation(description: "Expected completion to be called") - var errorCollection: DataBrokerProtectionAgentErrorCollection! - - // When - sut.startImmediateOperationsIfPermitted(showWebView: false, - operationDependencies: mockDependencies) { errors in - errorCollection = errors - expectation.fulfill() - } - - mockQueue.completeOperationsUpTo(index: 2) - - sut.stopAllOperations() - - // Then - await fulfillment(of: [expectation], timeout: 3) - XCTAssert(errorCollection.operationErrors?.count == 2) - } - func testWhenCallDebugOptOutCommand_thenOptOutOperationsAreCreated() throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, @@ -335,7 +305,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.optOut) + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.optOut) XCTAssert(mockOperationsCreator.createdType == .scan) // When diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index a80b15eb19..b8f1a4cb3a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -1026,6 +1026,166 @@ extension DataBroker { } } +final class MockDataBrokerProtectionOperationQueueManager: DataBrokerProtectionQueueManager { + var debugRunningStatusString: String { return "" } + + var startImmediateOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startScheduledOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + + var startImmediateOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startScheduledOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + + init(operationQueue: DataBrokerProtection.DataBrokerProtectionOperationQueue, operationsCreator: DataBrokerProtection.DataBrokerOperationsCreator, mismatchCalculator: DataBrokerProtection.MismatchCalculator, brokerUpdater: DataBrokerProtection.DataBrokerProtectionBrokerUpdater?, pixelHandler: Common.EventMapping) { + + } + + func startImmediateOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startImmediateOperationsIfPermittedCompletionError) + startImmediateOperationsIfPermittedCalledCompletion?(startImmediateOperationsIfPermittedCompletionError) + } + + func startScheduledOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startScheduledOperationsIfPermittedCompletionError) + startScheduledOperationsIfPermittedCalledCompletion?(startScheduledOperationsIfPermittedCompletionError) + } + + func execute(_ command: DataBrokerProtection.DataBrokerProtectionQueueManagerDebugCommand) { + } +} + +final class MockUserNotificationService: DataBrokerProtectionUserNotificationService { + + var requestPermissionWasAsked = false + var firstScanNotificationWasSent = false + var firstRemovedNotificationWasSent = false + var checkInNotificationWasScheduled = false + var allInfoRemovedWasSent = false + + func requestNotificationPermission() { + requestPermissionWasAsked = true + } + + func sendFirstScanCompletedNotification() { + firstScanNotificationWasSent = true + } + + func sendFirstRemovedNotificationIfPossible() { + firstRemovedNotificationWasSent = true + } + + func sendAllInfoRemovedNotificationIfPossible() { + allInfoRemovedWasSent = true + } + + func scheduleCheckInNotificationIfPossible() { + checkInNotificationWasScheduled = true + } + + func reset() { + requestPermissionWasAsked = false + firstScanNotificationWasSent = false + firstRemovedNotificationWasSent = false + checkInNotificationWasScheduled = false + allInfoRemovedWasSent = false + } +} + +final class MockDataBrokerProtectionBackgroundActivityScheduler: DataBrokerProtectionBackgroundActivityScheduler { + + var delegate: DataBrokerProtection.DataBrokerProtectionBackgroundActivitySchedulerDelegate? + var lastTriggerTimestamp: Date? + + var startSchedulerCompletion: (() -> Void)? + + func startScheduler() { + startSchedulerCompletion?() + } + + func triggerDelegateCall() { + delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) + } +} + +final class MockDataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { + + var profileToReturn: DataBrokerProtectionProfile? + var shouldReturnHasMatches = false + + var cache: DataBrokerProtection.InMemoryDataCache + var delegate: DataBrokerProtection.DataBrokerProtectionDataManagerDelegate? + + init(pixelHandler: Common.EventMapping, fakeBrokerFlag: DataBrokerProtection.DataBrokerDebugFlag) { + cache = InMemoryDataCache() + } + + func saveProfile(_ profile: DataBrokerProtection.DataBrokerProtectionProfile) async throws { + } + + func fetchProfile() throws -> DataBrokerProtection.DataBrokerProtectionProfile? { + return profileToReturn + } + + func prepareProfileCache() throws { + } + + func fetchBrokerProfileQueryData(ignoresCache: Bool) throws -> [DataBrokerProtection.BrokerProfileQueryData] { + return [] + } + + func prepareBrokerProfileQueryDataCache() throws { + } + + func hasMatches() throws -> Bool { + return shouldReturnHasMatches + } + + func profileQueriesCount() throws -> Int { + return 0 + } +} + +final class MockIPCServer: DataBrokerProtectionIPCServer { + + var serverDelegate: DataBrokerProtection.DataBrokerProtectionAppToAgentInterface? + + init(machServiceName: String) { + } + + func activate() { + } + + func register() { + } + + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + serverDelegate?.profileSaved() + } + + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + serverDelegate?.appLaunched() + } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } + + func startImmediateOperations(showWebView: Bool) { + serverDelegate?.startImmediateOperations(showWebView: showWebView) + } + + func startScheduledOperations(showWebView: Bool) { + serverDelegate?.startScheduledOperations(showWebView: showWebView) + } + + func runAllOptOuts(showWebView: Bool) { + serverDelegate?.runAllOptOuts(showWebView: showWebView) + } + + func getDebugMetadata(completion: @escaping (DataBrokerProtection.DBPBackgroundAgentMetadata?) -> Void) { + serverDelegate?.profileSaved() + } +} + final class MockDataBrokerProtectionOperationQueue: DataBrokerProtectionOperationQueue { var maxConcurrentOperationCount = 1 @@ -1148,11 +1308,11 @@ final class MockDataBrokerOperationErrorDelegate: DataBrokerOperationErrorDelega extension DefaultDataBrokerOperationDependencies { static var mock: DefaultDataBrokerOperationDependencies { DefaultDataBrokerOperationDependencies(database: MockDatabase(), - config: DataBrokerProtectionProcessorConfiguration(), + config: DataBrokerExecutionConfig(), runnerProvider: MockRunnerProvider(), notificationCenter: .default, pixelHandler: MockPixelHandler(), - userNotificationService: MockUserNotification()) + userNotificationService: MockUserNotificationService()) } } From f4269b5fa9014241ad344399f2e61483e7a91613 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Fri, 17 May 2024 17:40:35 +0100 Subject: [PATCH 05/42] Fix swiftlint warning --- .../Scheduler/DataBrokerProtectionQueueManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index c3e8ce0f88..48b6113346 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -99,8 +99,8 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa switch mode { case .idle: return "idle" - case .immediate(let completion), - .scheduled(let completion): + case .immediate(_), + .scheduled(_): return "running" } } From b8c4e4f2d422d61fba9e151371c612f32082d396 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Fri, 17 May 2024 17:40:57 +0100 Subject: [PATCH 06/42] Fix new swift lint warning --- .../Scheduler/DataBrokerProtectionQueueManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index 48b6113346..3567279d89 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -99,8 +99,8 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa switch mode { case .idle: return "idle" - case .immediate(_), - .scheduled(_): + case .immediate, + .scheduled: return "running" } } From 03978c2e24109cb22daeda262bb27a2b4cdc8b49 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Mon, 20 May 2024 14:46:20 +0100 Subject: [PATCH 07/42] Fix activity scheduler not calling completion --- .../DataBrokerProtectionBackgroundActivityScheduler.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift index 9467c83b92..e1d7694116 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift @@ -48,11 +48,12 @@ public final class DefaultDataBrokerProtectionBackgroundActivityScheduler: DataB } public func startScheduler() { - activity.schedule { _ in + activity.schedule { completion in self.lastTriggerTimestamp = Date() os_log("Scheduler running...", log: .dataBrokerProtection) self.delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) + completion(.finished) } } } From 4c801c7eb785595fb9fcd60c881ccebb56d984fa Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Mon, 20 May 2024 15:16:45 +0100 Subject: [PATCH 08/42] Make activity only call completion once all work has actually finished --- .../Scheduler/DataBrokerProtectionAgentManager.swift | 6 ++++-- .../DataBrokerProtectionBackgroundActivityScheduler.swift | 8 +++++--- .../Tests/DataBrokerProtectionTests/Mocks.swift | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index e8a46900fe..fae04e7e17 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -146,8 +146,10 @@ public final class DataBrokerProtectionAgentManager { extension DataBrokerProtectionAgentManager: DataBrokerProtectionBackgroundActivitySchedulerDelegate { - public func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtection.DataBrokerProtectionBackgroundActivityScheduler) { - queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + public func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtection.DataBrokerProtectionBackgroundActivityScheduler, completion: (() -> Void)?) { + queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { _ in + completion?() + } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift index e1d7694116..8df894d8c1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift @@ -28,7 +28,7 @@ public protocol DataBrokerProtectionBackgroundActivityScheduler { } public protocol DataBrokerProtectionBackgroundActivitySchedulerDelegate: AnyObject { - func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtectionBackgroundActivityScheduler) + func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtectionBackgroundActivityScheduler, completion: (() -> Void)?) } public final class DefaultDataBrokerProtectionBackgroundActivityScheduler: DataBrokerProtectionBackgroundActivityScheduler { @@ -52,8 +52,10 @@ public final class DefaultDataBrokerProtectionBackgroundActivityScheduler: DataB self.lastTriggerTimestamp = Date() os_log("Scheduler running...", log: .dataBrokerProtection) - self.delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) - completion(.finished) + self.delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) { + os_log("Scheduler finished...", log: .dataBrokerProtection) + completion(.finished) + } } } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 39ab659728..c4b2804e3f 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -1102,7 +1102,7 @@ final class MockDataBrokerProtectionBackgroundActivityScheduler: DataBrokerProte } func triggerDelegateCall() { - delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) + delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self, completion: nil) } } From f645b5907cbd32848d302db1d725927b040028f9 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Mon, 20 May 2024 17:12:27 +0100 Subject: [PATCH 09/42] Fix lint error --- .../DataBrokerExecutionConfigTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift index 8c7ae5c374..cb54af211b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionProcessorConfigurationTests.swift +// DataBrokerExecutionConfigTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // From 8dd682c91deef8949f4a75c98091b99d766b5b8d Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 22 May 2024 21:16:43 +0100 Subject: [PATCH 10/42] Merge pir stabilization feature branch into main (#2789) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207338264132623/f Tech Design URL: https://app.asana.com/0/481882893211075/1207174884557414/f CC: **Description**: This PR has the effect of mergeing four different PRs into main: https://github.com/duckduckgo/macos-browser/pull/2777 https://github.com/duckduckgo/macos-browser/pull/2758 https://github.com/duckduckgo/macos-browser/pull/2757 https://github.com/duckduckgo/macos-browser/pull/2743 This covers significant changes to the XPC interface and how the main app uses it, the background manager, the scheduler, and the processor. See individual PRs for details. There's no code changes here that haven't been reviewed separately as part of those PRs, so it's up to you how you want to review it code wise. The more important step at this stage is manual testing. **Steps to test this PR**: 1. Because this is a fairly significant set of changes, we need to be as thorough as we can in testing DBP generally works and really try to break it. Because of this, I'm keen that anyone testing things of their own ways to break it before trying the steps below. 2. Edit/add profile data, and before scans finish, edit it again. Scans should start afresh, and you should see an interrupted pixel fire. Check that you don't get a scans completed notification 3. Edit/add profile data, and before scans finish, close the app, and reopen it. The same set of manual scans should still continue, and a blocked pixel should fire. 4. With profile data already existing but initial scans having finished, close the app and reopen it. Scheduled scans should run. 5. With profile data already existing, and with the app closed, launch the background agent, scheduled scans should run 6. Edit/add profile data, put the laptop to sleep/lock it/restart it, check that scans continue as expected. 7. Check that the background agent activity lines up with the UI, I.e. the progress bar and when we sent the scans completed notification is accurate --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Pete Smith --- DuckDuckGo.xcodeproj/project.pbxproj | 36 +- DuckDuckGo/DBP/DBPHomeViewController.swift | 2 +- .../DBP/DataBrokerProtectionAppEvents.swift | 5 + .../DBP/DataBrokerProtectionDebugMenu.swift | 50 +- .../DataBrokerProtectionFeatureDisabler.swift | 10 +- ...taBrokerProtectionLoginItemInterface.swift | 119 +++++ ...taBrokerProtectionLoginItemScheduler.swift | 93 ---- .../DBP/DataBrokerProtectionManager.swift | 11 +- .../DataBrokerProtectionPixelsHandler.swift | 38 +- ...ataBrokerProtectionBackgroundManager.swift | 116 ----- ...kDuckGoDBPBackgroundAgentAppDelegate.swift | 8 +- .../IPCServiceManager.swift | 129 ----- .../DataBrokerProtectionDataManager.swift | 5 +- .../DataBrokerProtectionDatabase.swift | 20 +- .../DataBrokerDatabaseBrowserViewModel.swift | 8 +- .../DataBrokerRunCustomJSONViewModel.swift | 109 +---- ...ScanOperation.swift => DebugScanJob.swift} | 4 +- .../DataBrokerForceOptOutViewModel.swift | 14 +- ...aBrokerProtectionAppToAgentInterface.swift | 77 +++ .../IPC/DataBrokerProtectionIPCClient.swift | 144 ++---- .../DataBrokerProtectionIPCScheduler.swift | 74 --- .../IPC/DataBrokerProtectionIPCServer.swift | 142 ++---- ...perationData.swift => BrokerJobData.swift} | 8 +- .../Model/BrokerProfileQueryData.swift | 20 +- .../Model/DBPUIViewModel.swift | 18 +- .../Operations/DataBrokerJob.swift | 312 ++++++++++++ ...Runner.swift => DataBrokerJobRunner.swift} | 12 +- ...wift => DataBrokerJobRunnerProvider.swift} | 12 +- .../Operations/DataBrokerOperation.swift | 385 ++++++--------- .../DataBrokerOperationsCollection.swift | 220 --------- ...taBrokerProfileQueryOperationManager.swift | 36 +- .../DataBrokerProtectionBrokerUpdater.swift | 12 +- .../OperationPreferredDateUpdater.swift | 50 +- ...{OptOutOperation.swift => OptOutJob.swift} | 4 +- ...UseCase.swift => MismatchCalculator.swift} | 13 +- .../{ScanOperation.swift => ScanJob.swift} | 4 +- .../DataBrokerProtectionEventPixels.swift | 4 +- .../Pixels/DataBrokerProtectionPixels.swift | 185 +++----- ...kerProtectionStageDurationCalculator.swift | 14 +- .../Scheduler}/BrowserWindowManager.swift | 0 .../Scheduler}/DBPMocks.swift | 14 +- ....swift => DataBrokerExecutionConfig.swift} | 13 +- .../DataBrokerOperationsCreator.swift | 59 +++ .../DataBrokerProtectionAgentManager.swift | 285 +++++++++++ ...rotectionBackgroundActivityScheduler.swift | 61 +++ .../DataBrokerProtectionNoOpScheduler.swift | 43 -- .../DataBrokerProtectionProcessor.swift | 209 -------- .../DataBrokerProtectionQueueManager.swift | 269 +++++++++++ .../DataBrokerProtectionScheduler.swift | 393 --------------- .../DataBrokerProtectionSecureVault.swift | 22 +- .../Storage/Mappers.swift | 4 +- .../DataBrokerProtectionViewController.swift | 6 +- .../DataBrokerProtection/UI/UIMapper.swift | 44 +- ...t => DataBrokerExecutionConfigTests.swift} | 8 +- .../DataBrokerOperationActionTests.swift | 38 +- .../DataBrokerOperationsCreatorTests.swift | 82 ++++ ...kerProfileQueryOperationManagerTests.swift | 275 +++++------ ...ataBrokerProtectionAgentManagerTests.swift | 306 ++++++++++++ ...DataBrokerProtectionEventPixelsTests.swift | 38 +- .../DataBrokerProtectionProfileTests.swift | 6 +- ...ataBrokerProtectionQueueManagerTests.swift | 320 +++++++++++++ .../DataBrokerProtectionQueueModeTests.swift | 122 +++++ .../DataBrokerProtectionUpdaterTests.swift | 16 +- .../MismatchCalculatorUseCaseTests.swift | 14 +- .../DataBrokerProtectionTests/Mocks.swift | 447 +++++++++++++++++- 65 files changed, 3151 insertions(+), 2466 deletions(-) create mode 100644 DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift delete mode 100644 DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift delete mode 100644 DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift delete mode 100644 DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/{DebugScanOperation.swift => DebugScanJob.swift} (98%) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/{BrokerOperationData.swift => BrokerJobData.swift} (93%) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{DataBrokerOperationRunner.swift => DataBrokerJobRunner.swift} (95%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{DataBrokerOperationRunnerProvider.swift => DataBrokerJobRunnerProvider.swift} (80%) delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{OptOutOperation.swift => OptOutJob.swift} (98%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/{MismatchCalculatorUseCase.swift => MismatchCalculator.swift} (85%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/{ScanOperation.swift => ScanJob.swift} (98%) rename {DuckDuckGoDBPBackgroundAgent => LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler}/BrowserWindowManager.swift (100%) rename {DuckDuckGoDBPBackgroundAgent => LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler}/DBPMocks.swift (81%) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/{DataBrokerProtectionProcessorConfiguration.swift => DataBrokerExecutionConfig.swift} (78%) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift rename LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/{DataBrokerProtectionProcessorConfigurationTests.swift => DataBrokerExecutionConfigTests.swift} (83%) create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ebf54146aa..6caadc88f1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -169,12 +169,11 @@ 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 3158B14D2B0BF74D00AF130C /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */; }; + 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.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 */; }; 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */; }; - 315A023D2B64216B00BFA577 /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 315A023F2B6421AE00BFA577 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 315A023E2B6421AE00BFA577 /* Networking */; }; 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */; }; 3168506D2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; @@ -222,7 +221,7 @@ 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; - 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */; }; + 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */; }; 31EF1E832B63FFCA00E6DB17 /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; }; 31EF1E842B63FFD100E6DB17 /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */; }; @@ -1494,14 +1493,12 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; - 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; - 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; @@ -1576,7 +1573,6 @@ 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; - 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; @@ -1725,10 +1721,6 @@ 9D9AE9202AAA3B450026E7DC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */; }; 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9172AAA3B450026E7DC /* UserText.swift */; }; 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9172AAA3B450026E7DC /* UserText.swift */; }; - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */; }; - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */; }; - 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; - 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */; }; @@ -3338,7 +3330,6 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; - 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; @@ -3357,7 +3348,7 @@ 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; 7B5291882A1697680022E406 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeveloperID.xcconfig; sourceTree = ""; }; - 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemScheduler.swift; sourceTree = ""; }; + 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemInterface.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 = ""; }; @@ -3383,7 +3374,6 @@ 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; - 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPCServiceManager.swift; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; @@ -3508,8 +3498,6 @@ 9D9AE9182AAA3B450026E7DC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D9AE9192AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoDBPBackgroundAgentAppStore.entitlements; sourceTree = ""; }; 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoDBPBackgroundAgent.entitlements; sourceTree = ""; }; - 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; - 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F0660722BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandlerTests.swift; sourceTree = ""; }; 9F0660762BECC81800B8EEF1 /* PixelCapturedParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelCapturedParameters.swift; sourceTree = ""; }; @@ -4598,7 +4586,7 @@ 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, - 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */, + 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */, 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */, 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, @@ -6334,10 +6322,6 @@ isa = PBXGroup; children = ( 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, - 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, - 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, - 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, - 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -10149,7 +10133,7 @@ 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, B6ABD0CB2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, - 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, + 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, B6B140892ABDBCC1004F8E85 /* HoverTrackingArea.swift in Sources */, 3706FC36293F65D500E42796 /* LongPressButton.swift in Sources */, 3706FC37293F65D500E42796 /* CoreDataStore.swift in Sources */, @@ -10862,12 +10846,8 @@ buildActionMask = 2147483647; files = ( 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, - 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, - 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */, 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */, ); @@ -10878,12 +10858,8 @@ buildActionMask = 2147483647; files = ( 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, - 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, - 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, - 315A023D2B64216B00BFA577 /* IPCServiceManager.swift in Sources */, 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */, ); @@ -11078,8 +11054,8 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, + 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index d6ececa0bb..cbcbb3dce4 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -61,7 +61,7 @@ final class DBPHomeViewController: NSViewController { featureToggles: features) return DataBrokerProtectionViewController( - scheduler: dataBrokerProtectionManager.scheduler, + agentInterface: dataBrokerProtectionManager.loginItemInterface, dataManager: dataBrokerProtectionManager.dataManager, privacyConfig: privacyConfigurationManager, prefs: prefs, diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 07a579367f..a86c213dc4 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -34,6 +34,7 @@ struct DataBrokerProtectionAppEvents { func applicationDidFinishLaunching() { let loginItemsManager = LoginItemsManager() let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() + let loginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } @@ -52,6 +53,10 @@ struct DataBrokerProtectionAppEvents { if let profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), profileQueriesCount > 0 { restartBackgroundAgent(loginItemsManager: loginItemsManager) + + // Wait to make sure the agent has had time to restart before attempting to call a method on it + try await Task.sleep(nanoseconds: 1_000_000_000) + loginItemInterface.appLaunched() } else { featureVisibility.disableAndDeleteForWaitlistUsers() } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index a552bf4bc6..35104d98e3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -105,7 +105,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Operations") { NSMenuItem(title: "Hidden WebView") { menuItem(withTitle: "Run queued operations", - action: #selector(DataBrokerProtectionDebugMenu.runQueuedOperations(_:)), + action: #selector(DataBrokerProtectionDebugMenu.startScheduledOperations(_:)), representedObject: false) menuItem(withTitle: "Run scan operations", @@ -119,7 +119,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Visible WebView") { menuItem(withTitle: "Run queued operations", - action: #selector(DataBrokerProtectionDebugMenu.runQueuedOperations(_:)), + action: #selector(DataBrokerProtectionDebugMenu.startScheduledOperations(_:)), representedObject: true) menuItem(withTitle: "Run scan operations", @@ -204,61 +204,25 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } } - @objc private func runQueuedOperations(_ sender: NSMenuItem) { + @objc private func startScheduledOperations(_ sender: NSMenuItem) { os_log("Running queued operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.runQueuedOperations(showWebView: showWebView) { errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Queued operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Queued operations finished, operation errors count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } else { - os_log("Queued operations finished", log: .dataBrokerProtection) - } - } + DataBrokerProtectionManager.shared.loginItemInterface.startScheduledOperations(showWebView: showWebView) } @objc private func runScanOperations(_ sender: NSMenuItem) { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.startManualScan(showWebView: showWebView, startTime: Date()) { errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("scan operations finished, operation errors count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } else { - os_log("Scan operations finished", log: .dataBrokerProtection) - } - } + DataBrokerProtectionManager.shared.loginItemInterface.startImmediateOperations(showWebView: showWebView) } @objc private func runOptoutOperations(_ sender: NSMenuItem) { os_log("Running Optout operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.optOutAllBrokers(showWebView: showWebView) { errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Optout operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Optout operations finished, operation errors count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } else { - os_log("Optout operations finished", log: .dataBrokerProtection) - } - } + DataBrokerProtectionManager.shared.loginItemInterface.runAllOptOuts(showWebView: showWebView) } @objc private func backgroundAgentRestart() { @@ -332,7 +296,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func forceBrokerJSONFilesUpdate() { - if let updater = DataBrokerProtectionBrokerUpdater.provide() { + if let updater = DefaultDataBrokerProtectionBrokerUpdater.provideForDebug() { updater.updateBrokers() } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index 83489b21bb..18a1c89b86 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -31,23 +31,21 @@ protocol DataBrokerProtectionFeatureDisabling { } struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling { - private let scheduler: DataBrokerProtectionLoginItemScheduler + private let loginItemInterface: DataBrokerProtectionLoginItemInterface private let dataManager: InMemoryDataCacheDelegate - init(scheduler: DataBrokerProtectionLoginItemScheduler = DataBrokerProtectionManager.shared.scheduler, + init(loginItemInterface: DataBrokerProtectionLoginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface, dataManager: InMemoryDataCacheDelegate = DataBrokerProtectionManager.shared.dataManager) { self.dataManager = dataManager - self.scheduler = scheduler + self.loginItemInterface = loginItemInterface } func disableAndDelete() { if !DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist { - scheduler.stopScheduler() - - scheduler.disableLoginItem() do { try dataManager.removeAllData() + // the dataManagers delegate handles login item disabling } catch { os_log("DataBrokerProtectionFeatureDisabler error: disableAndDelete, error: %{public}@", log: .error, error.localizedDescription) } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift new file mode 100644 index 0000000000..c4e86483c9 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift @@ -0,0 +1,119 @@ +// +// DataBrokerProtectionLoginItemInterface.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. +// + +#if DBP + +import Foundation +import DataBrokerProtection +import Common + +protocol DataBrokerProtectionLoginItemInterface: DataBrokerProtectionAppToAgentInterface { + func dataDeleted() +} + +/// Launches a login item and then communicates with it through IPC +/// +final class DefaultDataBrokerProtectionLoginItemInterface { + private let ipcClient: DataBrokerProtectionIPCClient + private let loginItemsManager: LoginItemsManager + private let pixelHandler: EventMapping + + init(ipcClient: DataBrokerProtectionIPCClient, + loginItemsManager: LoginItemsManager = .init(), + pixelHandler: EventMapping) { + self.ipcClient = ipcClient + self.loginItemsManager = loginItemsManager + self.pixelHandler = pixelHandler + } +} + +extension DefaultDataBrokerProtectionLoginItemInterface: DataBrokerProtectionLoginItemInterface { + + // MARK: - Login Item Management + + private func disableLoginItem() { + DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerDisableLoginItemDaily, frequency: .daily) + loginItemsManager.disableLoginItems([.dbpBackgroundAgent]) + } + + private func enableLoginItem() { + DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerEnableLoginItemDaily, frequency: .daily) + loginItemsManager.enableLoginItems([.dbpBackgroundAgent], log: .dbp) + } + + // MARK: - DataBrokerProtectionLoginItemInterface + + func dataDeleted() { + disableLoginItem() + } + + // MARK: - DataBrokerProtectionAppToAgentInterface + // MARK: - DataBrokerProtectionAgentAppEvents + + func profileSaved() { + enableLoginItem() + + Task { + // Wait to make sure the agent has had time to launch + try await Task.sleep(nanoseconds: 1_000_000_000) + pixelHandler.fire(.ipcServerProfileSavedCalledByApp) + ipcClient.profileSaved { error in + if let error = error { + self.pixelHandler.fire(.ipcServerProfileSavedXPCError(error: error)) + } else { + self.pixelHandler.fire(.ipcServerProfileSavedReceivedByAgent) + } + } + } + } + + func appLaunched() { + pixelHandler.fire(.ipcServerAppLaunchedCalledByApp) + ipcClient.appLaunched { error in + if let error = error { + self.pixelHandler.fire(.ipcServerAppLaunchedXPCError(error: error)) + } else { + self.pixelHandler.fire(.ipcServerAppLaunchedReceivedByAgent) + } + } + } + + // MARK: - DataBrokerProtectionAgentDebugCommands + + func openBrowser(domain: String) { + ipcClient.openBrowser(domain: domain) + } + + func startImmediateOperations(showWebView: Bool) { + ipcClient.startImmediateOperations(showWebView: showWebView) + } + + func startScheduledOperations(showWebView: Bool) { + ipcClient.startScheduledOperations(showWebView: showWebView) + } + + func runAllOptOuts(showWebView: Bool) { + ipcClient.runAllOptOuts(showWebView: showWebView) + } + + func getDebugMetadata() async -> DataBrokerProtection.DBPBackgroundAgentMetadata? { + return await ipcClient.getDebugMetadata() + } +} + +#endif diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift deleted file mode 100644 index 50b75e9fac..0000000000 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// DataBrokerProtectionLoginItemScheduler.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. -// - -#if DBP - -import Foundation -import DataBrokerProtection -import Common - -/// A scheduler that launches a login item and the communicates with it through an IPC scheduler. -/// -final class DataBrokerProtectionLoginItemScheduler { - private let ipcScheduler: DataBrokerProtectionIPCScheduler - private let loginItemsManager: LoginItemsManager - - init(ipcScheduler: DataBrokerProtectionIPCScheduler, loginItemsManager: LoginItemsManager = .init()) { - self.ipcScheduler = ipcScheduler - self.loginItemsManager = loginItemsManager - } - - // MARK: - Login Item Management - - func disableLoginItem() { - DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerDisableLoginItemDaily, frequency: .daily) - loginItemsManager.disableLoginItems([.dbpBackgroundAgent]) - } - - func enableLoginItem() { - DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerEnableLoginItemDaily, frequency: .daily) - loginItemsManager.enableLoginItems([.dbpBackgroundAgent], log: .dbp) - } -} - -extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler { - var status: DataBrokerProtection.DataBrokerProtectionSchedulerStatus { - ipcScheduler.status - } - - var statusPublisher: Published.Publisher { - ipcScheduler.statusPublisher - } - - func startManualScan(showWebView: Bool, - startTime: Date, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - enableLoginItem() - ipcScheduler.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) - } - - func startScheduler(showWebView: Bool) { - enableLoginItem() - ipcScheduler.startScheduler(showWebView: showWebView) - } - - func stopScheduler() { - ipcScheduler.stopScheduler() - } - - func optOutAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - ipcScheduler.optOutAllBrokers(showWebView: showWebView, completion: completion) - } - - func runAllOperations(showWebView: Bool) { - ipcScheduler.runAllOperations(showWebView: showWebView) - } - - func runQueuedOperations(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - ipcScheduler.runQueuedOperations(showWebView: showWebView, completion: completion) - } - - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - ipcScheduler.getDebugMetadata(completion: completion) - } -} - -#endif diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index 9ecde48a3e..111892a5fe 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -46,11 +46,8 @@ public final class DataBrokerProtectionManager { loginItemStatusChecker: loginItemStatusChecker) }() - lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { - - let ipcScheduler = DataBrokerProtectionIPCScheduler(ipcClient: ipcClient) - - return DataBrokerProtectionLoginItemScheduler(ipcScheduler: ipcScheduler) + lazy var loginItemInterface: DataBrokerProtectionLoginItemInterface = { + return DefaultDataBrokerProtectionLoginItemInterface(ipcClient: ipcClient, pixelHandler: pixelHandler) }() private init() { @@ -70,14 +67,14 @@ public final class DataBrokerProtectionManager { extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { public func dataBrokerProtectionDataManagerDidUpdateData() { - scheduler.startScheduler() + loginItemInterface.profileSaved() let dbpDateStore = DefaultWaitlistActivationDateStore(source: .dbp) dbpDateStore.setActivationDateIfNecessary() } public func dataBrokerProtectionDataManagerDidDeleteData() { - scheduler.stopScheduler() + loginItemInterface.dataDeleted() } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 2f91fe97a0..494d141be5 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -33,26 +33,20 @@ public class DataBrokerProtectionPixelsHandler: EventMapping = DataBrokerProtectionPixelsHandler() - - private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() - private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() - private let authenticationManager: DataBrokerProtectionAuthenticationManaging - private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() - - private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) - - lazy var dataManager: DataBrokerProtectionDataManager = { - DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) - }() - - lazy var scheduler: DataBrokerProtectionScheduler = { - let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned - let features = ContentScopeFeatureToggles(emailProtection: false, - emailProtectionIncontextSignup: false, - credentialsAutofill: false, - identitiesAutofill: false, - creditCardsAutofill: false, - credentialsSaving: false, - passwordGeneration: false, - inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) - - let sessionKey = UUID().uuidString - let prefs = ContentScopeProperties(gpcEnabled: false, - sessionKey: sessionKey, - featureToggles: features) - - let pixelHandler = DataBrokerProtectionPixelsHandler() - - let userNotificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) - - return DefaultDataBrokerProtectionScheduler(privacyConfigManager: privacyConfigurationManager, - contentScopeProperties: prefs, - dataManager: dataManager, - notificationCenter: NotificationCenter.default, - pixelHandler: pixelHandler, - authenticationManager: authenticationManager, - userNotificationService: userNotificationService) - }() - - private init() { - let redeemUseCase = RedeemUseCase(authenticationService: authenticationService, - authenticationRepository: authenticationRepository) - self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase) - - _ = ipcServiceManager - } - - public func runOperationsAndStartSchedulerIfPossible() { - pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossible) - - do { - // If there's no saved profile we don't need to start the scheduler - guard (try dataManager.fetchProfile()) != nil else { - pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile) - return - } - } catch { - pixelHandler.fire(.generalError(error: error, - functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) - return - } - - scheduler.runQueuedOperations(showWebView: false) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), error: %{public}@", - log: .dataBrokerProtection, - oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, - functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - return - } - - self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler) - self?.scheduler.startScheduler() - } - } -} diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 0a3e777138..7e2bcebbef 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -80,13 +80,17 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele private let settings = DataBrokerProtectionSettings() private var cancellables = Set() private var statusBarMenu: StatusBarMenu? + private var manager: DataBrokerProtectionAgentManager? @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) - let manager = DataBrokerProtectionBackgroundManager.shared - manager.runOperationsAndStartSchedulerIfPossible() + let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), + authenticationRepository: KeychainAuthenticationData()) + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase) + manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager) + manager?.agentFinishedLaunching() setupStatusBarMenu() } diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift deleted file mode 100644 index 8c39f28e07..0000000000 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// IPCServiceManager.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 Combine -import Common -import DataBrokerProtection -import Foundation -import PixelKit - -/// Manages the IPC service for the Agent app -/// -/// This class will handle all interactions between IPC requests and the classes those requests -/// demand interaction with. -/// -final class IPCServiceManager { - private var browserWindowManager: BrowserWindowManager - private let ipcServer: DataBrokerProtectionIPCServer - private let scheduler: DataBrokerProtectionScheduler - private let pixelHandler: EventMapping - private var cancellables = Set() - - init(ipcServer: DataBrokerProtectionIPCServer = .init(machServiceName: Bundle.main.bundleIdentifier!), - scheduler: DataBrokerProtectionScheduler, - pixelHandler: EventMapping) { - - self.ipcServer = ipcServer - self.scheduler = scheduler - self.pixelHandler = pixelHandler - - browserWindowManager = BrowserWindowManager() - - ipcServer.serverDelegate = self - ipcServer.activate() - } - - private func subscribeToSchedulerStatusChanges() { - scheduler.statusPublisher - .subscribe(on: DispatchQueue.main) - .sink { [weak self] status in - self?.ipcServer.schedulerStatusChanges(status) - } - .store(in: &cancellables) - } -} - -extension IPCServiceManager: IPCServerInterface { - - func register() { - // When a new client registers, send the last known status - ipcServer.schedulerStatusChanges(scheduler.status) - } - - func startScheduler(showWebView: Bool) { - pixelHandler.fire(.ipcServerStartSchedulerReceivedByAgent) - scheduler.startScheduler(showWebView: showWebView) - } - - func stopScheduler() { - pixelHandler.fire(.ipcServerStopSchedulerReceivedByAgent) - scheduler.stopScheduler() - } - - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerOptOutAllBrokers) - scheduler.optOutAllBrokers(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerOptOutAllBrokersCompletion(error: errors?.oneTimeError)) - completion(errors) - } - } - - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerScanAllBrokersReceivedByAgent) - scheduler.startManualScan(showWebView: showWebView, startTime: startTime) { errors in - if let error = errors?.oneTimeError { - switch error { - case DataBrokerProtectionSchedulerError.operationsInterrupted: - self.pixelHandler.fire(.ipcServerScanAllBrokersInterruptedOnAgent) - default: - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletedOnAgentWithError(error: error)) - } - } else { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletedOnAgentWithoutError) - } - completion(errors) - } - } - - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - pixelHandler.fire(.ipcServerRunQueuedOperations) - scheduler.runQueuedOperations(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) - completion(errors) - } - } - - func runAllOperations(showWebView: Bool) { - pixelHandler.fire(.ipcServerRunAllOperations) - scheduler.runAllOperations(showWebView: showWebView) - } - - func openBrowser(domain: String) { - Task { @MainActor in - browserWindowManager.show(domain: domain) - } - } - - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - scheduler.getDebugMetadata(completion: completion) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index b006af7433..5a33e63831 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -294,7 +294,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { } func startScanAndOptOut() -> Bool { - return scanDelegate?.startScan(startDate: Date()) ?? false + // This is now unusused as we decided the web UI shouldn't issue commands directly + // The background agent itself instead decides to start scans based on events + // This should be removed once we can remove it from the web side + return true } func getInitialScanState() async -> DBPUIInitialScanState { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index c1cbafd4a8..8b293965af 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -27,7 +27,7 @@ protocol DataBrokerProtectionRepository { func fetchChildBrokers(for parentBroker: String) throws -> [DataBroker] - func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws + func saveOptOutJob(optOut: OptOutJobData, extractedProfile: ExtractedProfile) throws func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) throws -> BrokerProfileQueryData? func fetchAllBrokerProfileQueryData() throws -> [BrokerProfileQueryData] @@ -133,20 +133,20 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) guard let broker = try vault.fetchBroker(with: brokerId), let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), - let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { + let scanJob = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { let error = DataBrokerProtectionError.dataNotInDatabase os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.brokerProfileQueryData for brokerId and profileQueryId")) throw error } - let optOutOperations = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) + let optOutJobs = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) return BrokerProfileQueryData( dataBroker: broker, profileQuery: profileQuery, - scanOperationData: scanOperation, - optOutOperationsData: optOutOperations + scanJobData: scanJob, + optOutJobData: optOutJobs ) } catch { os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) @@ -258,14 +258,14 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { for broker in brokers { for profileQuery in profileQueries { if let brokerId = broker.id, let profileQueryId = profileQuery.id { - guard let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { continue } - let optOutOperations = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) + guard let scanJob = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { continue } + let optOutJobs = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) let brokerProfileQueryData = BrokerProfileQueryData( dataBroker: broker, profileQuery: profileQuery, - scanOperationData: scanOperation, - optOutOperationsData: optOutOperations + scanJobData: scanJob, + optOutJobData: optOutJobs ) brokerProfileQueryDataList.append(brokerProfileQueryData) @@ -281,7 +281,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { } } - func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { + func saveOptOutJob(optOut: OptOutJobData, extractedProfile: ExtractedProfile) throws { do { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift index a3bf0d1547..36eaf1b8ac 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift @@ -57,15 +57,15 @@ final class DataBrokerDatabaseBrowserViewModel: ObservableObject { let dataBrokers = Array(Set(profileBrokers)).sorted { $0.id ?? 0 < $1.id ?? 0 } let profileQuery = Array(Set(data.map { $0.profileQuery })) - let scanOperations = data.map { $0.scanOperationData } - let optOutOperations = data.flatMap { $0.optOutOperationsData } + let scanJobs = data.map { $0.scanJobData } + let optOutJobs = data.flatMap { $0.optOutJobData } let extractedProfiles = data.flatMap { $0.extractedProfiles } let events = data.flatMap { $0.events } let brokersTable = createTable(using: dataBrokers, tableName: "DataBrokers") let profileQueriesTable = createTable(using: profileQuery, tableName: "ProfileQuery") - let scansTable = createTable(using: scanOperations, tableName: "ScanOperation") - let optOutsTable = createTable(using: optOutOperations, tableName: "OptOutOperation") + let scansTable = createTable(using: scanJobs, tableName: "ScanOperation") + let optOutsTable = createTable(using: optOutJobs, tableName: "OptOutOperation") let extractedProfilesTable = createTable(using: extractedProfiles, tableName: "ExtractedProfile") let eventsTable = createTable(using: events.sorted(by: { $0.date < $1.date }), tableName: "Events") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 75c2f1ab78..50e1b3a5a5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -143,7 +143,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { let brokers: [DataBroker] - private let runnerProvider: OperationRunnerProvider + private let runnerProvider: JobRunnerProvider private let privacyConfigManager: PrivacyConfigurationManaging private let fakePixelHandler: EventMapping = EventMapping { event, _, _, _ in print(event) @@ -170,7 +170,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { sessionKey: sessionKey, featureToggles: features) - self.runnerProvider = DataBrokerOperationRunnerProvider( + self.runnerProvider = DataBrokerJobRunnerProvider( privacyConfigManager: privacyConfigurationManager, contentScopeProperties: contentScopeProperties, emailService: EmailService(authenticationManager: authenticationManager), @@ -192,17 +192,17 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { try await withThrowingTaskGroup(of: DebugScanReturnValue.self) { group in for queryData in brokerProfileQueryData { - let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, - prefs: self.contentScopeProperties, - query: queryData, - emailService: EmailService(authenticationManager: self.authenticationManager), - captchaService: CaptchaService(authenticationManager: self.authenticationManager)) { + let debugScanJob = DebugScanJob(privacyConfig: self.privacyConfigManager, + prefs: self.contentScopeProperties, + query: queryData, + emailService: EmailService(authenticationManager: self.authenticationManager), + captchaService: CaptchaService(authenticationManager: self.authenticationManager)) { true } group.addTask { do { - return try await debugScanOperation.run(inputValue: (), showWebView: false) + return try await debugScanJob.run(inputValue: (), showWebView: false) } catch { return DebugScanReturnValue(brokerURL: "ERROR - with broker: \(queryData.dataBroker.name)", extractedProfiles: [ExtractedProfile](), brokerProfileQueryData: queryData) } @@ -348,7 +348,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { let dataBroker = try decoder.decode(DataBroker.self, from: data) self.selectedDataBroker = dataBroker let brokerProfileQueryData = createBrokerProfileQueryData(for: dataBroker) - let runner = runnerProvider.getOperationRunner() + let runner = runnerProvider.getJobRunner() let group = DispatchGroup() for query in brokerProfileQueryData { @@ -384,11 +384,11 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } func runOptOut(scanResult: ScanResult) { - let runner = runnerProvider.getOperationRunner() + let runner = runnerProvider.getJobRunner() let brokerProfileQueryData = BrokerProfileQueryData( dataBroker: scanResult.dataBroker, profileQuery: scanResult.profileQuery, - scanOperationData: ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent]()) + scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent]()) ) Task { do { @@ -420,9 +420,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { var profileQueryIndex: Int64 = 1 for profileQuery in profileQueries { - let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) + let fakeScanJobData = ScanJobData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) brokerProfileQueryData.append( - .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData) + .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanJobData: fakeScanJobData) ) profileQueryIndex += 1 @@ -444,10 +444,10 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { var profileQueryIndex: Int64 = 1 for profileQuery in profileQueries { - let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) + let fakeScanJobData = ScanJobData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]()) for broker in brokers { brokerProfileQueryData.append( - .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData) + .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanJobData: fakeScanJobData) ) } @@ -489,7 +489,7 @@ final class FakeSleepObserver: SleepObserver { final class FakeStageDurationCalculator: StageDurationCalculator { var attemptId: UUID = UUID() - var isManualScan: Bool = false + var isImmediateOperation: Bool = false func durationSinceLastStage() -> Double { 0.0 @@ -554,83 +554,6 @@ final class FakeStageDurationCalculator: StageDurationCalculator { } } -/* - I wasn't able to import this mock from the background agent project, so I had to re-use it here. - */ -private final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { - - var data: Data { - let configString = """ - { - "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1693838894358, - "features": { - "brokerProtection": { - "state": "enabled", - "exceptions": [], - "settings": {} - } - }, - "unprotectedTemporary": [] - } - """ - let data = configString.data(using: .utf8) - return data! - } - - var currentConfig: Data { - data - } - - var updatesPublisher: AnyPublisher = .init(Just(())) - - var privacyConfig: BrowserServicesKit.PrivacyConfiguration { - guard let privacyConfigurationData = try? PrivacyConfigurationData(data: data) else { - fatalError("Could not retrieve privacy configuration data") - } - let privacyConfig = privacyConfiguration(withData: privacyConfigurationData, - internalUserDecider: internalUserDecider, - toggleProtectionsCounter: toggleProtectionsCounter) - return privacyConfig - } - - var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) - - var toggleProtectionsCounter: ToggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in - }) - - func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { - .downloaded - } -} - -func privacyConfiguration(withData data: PrivacyConfigurationData, - internalUserDecider: InternalUserDecider, - toggleProtectionsCounter: ToggleProtectionsCounter) -> PrivacyConfiguration { - let domain = MockDomainsProtectionStore() - return AppPrivacyConfiguration(data: data, - identifier: UUID().uuidString, - localProtection: domain, - internalUserDecider: internalUserDecider, - toggleProtectionsCounter: toggleProtectionsCounter) -} - -final class MockDomainsProtectionStore: DomainsProtectionStore { - var unprotectedDomains = Set() - - func disableProtection(forDomain domain: String) { - unprotectedDomains.insert(domain) - } - - func enableProtection(forDomain domain: String) { - unprotectedDomains.remove(domain) - } -} - -final class InternalUserDeciderStoreMock: InternalUserStoring { - var isInternalUser: Bool = false -} - extension DataBroker { func toJSONString() -> String { do { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift similarity index 98% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift index 364be47c18..e8900027d3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift @@ -1,5 +1,5 @@ // -// DebugScanOperation.swift +// DebugScanJob.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -48,7 +48,7 @@ struct EmptyCookieHandler: CookieHandler { } } -final class DebugScanOperation: DataBrokerOperation { +final class DebugScanJob: DataBrokerJob { typealias ReturnValue = DebugScanReturnValue typealias InputValue = Void diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift index 322af7070d..25219b64e8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift @@ -37,13 +37,13 @@ final class DataBrokerForceOptOutViewModel: ObservableObject { } self.optOutData = brokerProfileData .flatMap { profileData in - profileData.optOutOperationsData.map { ($0, profileData.dataBroker.name) } + profileData.optOutJobData.map { ($0, profileData.dataBroker.name) } } .filter { operationData, _ in operationData.extractedProfile.removedDate == nil } .map { operationData, brokerName in - OptOutViewData(optOutOperationData: operationData, brokerName: brokerName) + OptOutViewData(optOutJobData: operationData, brokerName: brokerName) }.sorted(by: { $0.brokerName < $1.brokerName }) } } @@ -57,16 +57,16 @@ final class DataBrokerForceOptOutViewModel: ObservableObject { struct OptOutViewData: Identifiable { let id: UUID - let optOutOperationData: OptOutOperationData + let optOutJobData: OptOutJobData let profileName: String let brokerName: String let extractedProfileID: Int64? - internal init(optOutOperationData: OptOutOperationData, brokerName: String) { - self.optOutOperationData = optOutOperationData - self.extractedProfileID = optOutOperationData.extractedProfile.id + internal init(optOutJobData: OptOutJobData, brokerName: String) { + self.optOutJobData = optOutJobData + self.extractedProfileID = optOutJobData.extractedProfile.id self.brokerName = brokerName - self.profileName = "\(extractedProfileID ?? 0) \(optOutOperationData.extractedProfile.fullName ?? "No Name")" + self.profileName = "\(extractedProfileID ?? 0) \(optOutJobData.extractedProfile.fullName ?? "No Name")" self.id = UUID() } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift new file mode 100644 index 0000000000..72d630b0fc --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAppToAgentInterface.swift @@ -0,0 +1,77 @@ +// +// DataBrokerProtectionAppToAgentInterface.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public enum DataBrokerProtectionAppToAgentInterfaceError: Error { + case loginItemDoesNotHaveNecessaryPermissions + case appInWrongDirectory +} + +@objc +public class DataBrokerProtectionAgentErrorCollection: NSObject, NSSecureCoding { + /* + This needs to be an NSObject (rather than a struct) so it can be represented in Objective C + and confrom to NSSecureCoding for the IPC layer. + */ + + private enum NSSecureCodingKeys { + static let oneTimeError = "oneTimeError" + static let operationErrors = "operationErrors" + } + + public let oneTimeError: Error? + public let operationErrors: [Error]? + + public init(oneTimeError: Error? = nil, operationErrors: [Error]? = nil) { + self.oneTimeError = oneTimeError + self.operationErrors = operationErrors + super.init() + } + + // MARK: - NSSecureCoding + + public static let supportsSecureCoding = true + + public func encode(with coder: NSCoder) { + coder.encode(oneTimeError, forKey: NSSecureCodingKeys.oneTimeError) + coder.encode(operationErrors, forKey: NSSecureCodingKeys.operationErrors) + } + + public required init?(coder: NSCoder) { + oneTimeError = coder.decodeObject(of: NSError.self, forKey: NSSecureCodingKeys.oneTimeError) + operationErrors = coder.decodeArrayOfObjects(ofClass: NSError.self, forKey: NSSecureCodingKeys.operationErrors) + } +} + +public protocol DataBrokerProtectionAgentAppEvents { + func profileSaved() + func appLaunched() +} + +public protocol DataBrokerProtectionAgentDebugCommands { + func openBrowser(domain: String) + func startImmediateOperations(showWebView: Bool) + func startScheduledOperations(showWebView: Bool) + func runAllOptOuts(showWebView: Bool) + func getDebugMetadata() async -> DBPBackgroundAgentMetadata? +} + +public protocol DataBrokerProtectionAppToAgentInterface: AnyObject, DataBrokerProtectionAgentAppEvents, DataBrokerProtectionAgentDebugCommands { + +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 1f1d2451b2..22ba75d0af 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -24,7 +24,6 @@ import XPCHelper /// This protocol describes the server-side IPC interface for controlling the tunnel /// public protocol IPCClientInterface: AnyObject { - func schedulerStatusChanges(_ status: DataBrokerProtectionSchedulerStatus) } public protocol DBPLoginItemStatusChecker { @@ -35,7 +34,6 @@ public protocol DBPLoginItemStatusChecker { /// This is the XPC interface with parameters that can be packed properly @objc protocol XPCClientInterface: NSObjectProtocol { - func schedulerStatusChanged(_ payload: Data) } public final class DataBrokerProtectionIPCClient: NSObject { @@ -47,15 +45,6 @@ public final class DataBrokerProtectionIPCClient: NSObject { let xpc: XPCClient - // MARK: - Scheduler Status - - @Published - private(set) public var schedulerStatus: DataBrokerProtectionSchedulerStatus = .idle - - public var schedulerStatusPublisher: Published.Publisher { - $schedulerStatus - } - // MARK: - Initializers public init(machServiceName: String, pixelHandler: EventMapping, loginItemStatusChecker: DBPLoginItemStatusChecker) { @@ -100,137 +89,76 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func startScheduler(showWebView: Bool) { - self.pixelHandler.fire(.ipcServerStartSchedulerCalledByApp) - xpc.execute(call: { server in - server.startScheduler(showWebView: showWebView) - }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerStartSchedulerXPCError(error: error)) - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! - }) - } + // MARK: - DataBrokerProtectionAgentAppEvents - public func stopScheduler() { - self.pixelHandler.fire(.ipcServerStopSchedulerCalledByApp) + public func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.stopScheduler() - }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerStopSchedulerXPCError(error: error)) - // Intentional no-op as there's no completion block - // If you add a completion block, please remember to call it here too! - }) + server.profileSaved(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) + }, xpcReplyErrorHandler: xpcMessageReceivedCompletion) } - public func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerOptOutAllBrokers) + public func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { xpc.execute(call: { server in - server.optOutAllBrokers(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) - completion(errors) - } - }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) - }) + server.appLaunched(xpcMessageReceivedCompletion: xpcMessageReceivedCompletion) + }, xpcReplyErrorHandler: xpcMessageReceivedCompletion) } - public func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerScanAllBrokersCalledByApp) - - guard loginItemStatusChecker.doesHaveNecessaryPermissions() else { - self.pixelHandler.fire(.ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions) - let errors = DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.loginItemDoesNotHaveNecessaryPermissions) - completion(errors) - return - } - - guard loginItemStatusChecker.isInCorrectDirectory() else { - self.pixelHandler.fire(.ipcServerScanAllBrokersAttemptedToCallInWrongDirectory) - let errors = DataBrokerProtectionSchedulerErrorCollection(oneTimeError: DataBrokerProtectionSchedulerError.appInWrongDirectory) - completion(errors) - return - } + // MARK: - DataBrokerProtectionAgentDebugCommands + public func openBrowser(domain: String) { xpc.execute(call: { server in - server.startManualScan(showWebView: showWebView, startTime: startTime) { errors in - if let error = errors?.oneTimeError { - let nsError = error as NSError - let interruptedError = DataBrokerProtectionSchedulerError.operationsInterrupted as NSError - if nsError.domain == interruptedError.domain, - nsError.code == interruptedError.code { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption) - } else { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: error)) - } - } else { - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletionCalledOnAppWithoutError) - } - completion(errors) - } + server.openBrowser(domain: domain) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerScanAllBrokersXPCError(error: error)) - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! }) } - public func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - self.pixelHandler.fire(.ipcServerRunQueuedOperations) + public func startImmediateOperations(showWebView: Bool) { xpc.execute(call: { server in - server.runQueuedOperations(showWebView: showWebView) { errors in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) - completion(errors) - } + server.startImmediateOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) - }) - } - - public func runAllOperations(showWebView: Bool) { - self.pixelHandler.fire(.ipcServerRunAllOperations) - xpc.execute(call: { server in - server.runAllOperations(showWebView: showWebView) - }, xpcReplyErrorHandler: { _ in + os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) } - public func openBrowser(domain: String) { - self.pixelHandler.fire(.ipcServerRunAllOperations) + public func startScheduledOperations(showWebView: Bool) { xpc.execute(call: { server in - server.openBrowser(domain: domain) + server.startScheduledOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! - }) - } + }) } - public func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { + public func runAllOptOuts(showWebView: Bool) { xpc.execute(call: { server in - server.getDebugMetadata(completion: completion) + server.runAllOptOuts(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") - completion(nil) + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! }) } + + public func getDebugMetadata() async -> DBPBackgroundAgentMetadata? { + await withCheckedContinuation { continuation in + xpc.execute(call: { server in + server.getDebugMetadata { metaData in + continuation.resume(returning: metaData) + } + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + continuation.resume(returning: nil) + }) + } + } } // MARK: - Incoming communication from the server extension DataBrokerProtectionIPCClient: XPCClientInterface { - func schedulerStatusChanged(_ payload: Data) { - guard let status = try? JSONDecoder().decode(DataBrokerProtectionSchedulerStatus.self, from: payload) else { - - return - } - - schedulerStatus = status - } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift deleted file mode 100644 index ea73c3e1a0..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// DataBrokerProtectionIPCScheduler.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 Common - -/// A scheduler that works through IPC to request the scheduling to a different process -/// -public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionScheduler { - private let ipcClient: DataBrokerProtectionIPCClient - - public init(ipcClient: DataBrokerProtectionIPCClient) { - self.ipcClient = ipcClient - } - - public var status: DataBrokerProtectionSchedulerStatus { - ipcClient.schedulerStatus - } - - public var statusPublisher: Published.Publisher { - ipcClient.schedulerStatusPublisher - } - - public func startScheduler(showWebView: Bool) { - ipcClient.startScheduler(showWebView: showWebView) - } - - public func stopScheduler() { - ipcClient.stopScheduler() - } - - public func optOutAllBrokers(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - let completion = completion ?? { _ in } - ipcClient.optOutAllBrokers(showWebView: showWebView, completion: completion) - } - - public func startManualScan(showWebView: Bool, - startTime: Date, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - let completion = completion ?? { _ in } - ipcClient.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) - } - - public func runQueuedOperations(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - let completion = completion ?? { _ in } - ipcClient.runQueuedOperations(showWebView: showWebView, completion: completion) - } - - public func runAllOperations(showWebView: Bool) { - ipcClient.runAllOperations(showWebView: showWebView) - } - - public func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - ipcClient.getDebugMetadata(completion: completion) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 7f4ae3e840..0e7ca83d81 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -35,10 +35,10 @@ public final class DBPBackgroundAgentMetadata: NSObject, NSSecureCoding { let agentSchedulerState: String let lastSchedulerSessionStartTimestamp: Double? - init(backgroundAgentVersion: String, - isAgentRunning: Bool, - agentSchedulerState: String, - lastSchedulerSessionStartTimestamp: Double?) { + public init(backgroundAgentVersion: String, + isAgentRunning: Bool, + agentSchedulerState: String, + lastSchedulerSessionStartTimestamp: Double?) { self.backgroundAgentVersion = backgroundAgentVersion self.isAgentRunning = isAgentRunning self.agentSchedulerState = agentSchedulerState @@ -75,40 +75,17 @@ public final class DBPBackgroundAgentMetadata: NSObject, NSSecureCoding { /// This protocol describes the server-side IPC interface for controlling the tunnel /// -public protocol IPCServerInterface: AnyObject { +public protocol IPCServerInterface: AnyObject, DataBrokerProtectionAgentDebugCommands { /// Registers a connection with the server. /// /// This is the point where the server will start sending status updates to the client. /// func register() - // MARK: - Scheduler + // MARK: - DataBrokerProtectionAgentAppEvents - /// Start the scheduler - /// - func startScheduler(showWebView: Bool) - - /// Stop the scheduler - /// - func stopScheduler() - - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runAllOperations(showWebView: Bool) - - // MARK: - Debugging Features - - /// Opens a browser window with the specified domain - /// - func openBrowser(domain: String) - - /// Returns background agent metadata for debugging purposes - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) } /// This protocol describes the server-side XPC interface. @@ -124,40 +101,35 @@ protocol XPCServerInterface { /// func register() - // MARK: - Scheduler + // MARK: - DataBrokerProtectionAgentAppEvents - /// Start the scheduler - /// - func startScheduler(showWebView: Bool) - - /// Stop the scheduler - /// - func stopScheduler() + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) - func runAllOperations(showWebView: Bool) - - // MARK: - Debugging Features + // MARK: - DataBrokerProtectionAgentDebugCommands /// Opens a browser window with the specified domain /// func openBrowser(domain: String) + func startImmediateOperations(showWebView: Bool) + func startScheduledOperations(showWebView: Bool) + func runAllOptOuts(showWebView: Bool) func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } -public final class DataBrokerProtectionIPCServer { +protocol DataBrokerProtectionIPCServer: IPCClientInterface, XPCServerInterface { + var serverDelegate: DataBrokerProtectionAppToAgentInterface? { get set } + + init(machServiceName: String) + + func activate() +} + +public final class DefaultDataBrokerProtectionIPCServer: DataBrokerProtectionIPCServer { let xpc: XPCServer - /// The delegate. - /// - public weak var serverDelegate: IPCServerInterface? + public weak var serverDelegate: DataBrokerProtectionAppToAgentInterface? public init(machServiceName: String) { let clientInterface = NSXPCInterface(with: XPCClientInterface.self) @@ -171,6 +143,8 @@ public final class DataBrokerProtectionIPCServer { xpc.delegate = self } + // DataBrokerProtectionIPCServer + public func activate() { xpc.activate() } @@ -178,63 +152,51 @@ public final class DataBrokerProtectionIPCServer { // MARK: - Outgoing communication to the clients -extension DataBrokerProtectionIPCServer: IPCClientInterface { - - public func schedulerStatusChanges(_ status: DataBrokerProtectionSchedulerStatus) { - let payload: Data - - do { - payload = try JSONEncoder().encode(status) - } catch { - return - } - - xpc.forEachClient { client in - client.schedulerStatusChanged(payload) - } - } +extension DefaultDataBrokerProtectionIPCServer: IPCClientInterface { } // MARK: - Incoming communication from a client -extension DataBrokerProtectionIPCServer: XPCServerInterface { +extension DefaultDataBrokerProtectionIPCServer: XPCServerInterface { + func register() { - serverDelegate?.register() - } - func startScheduler(showWebView: Bool) { - serverDelegate?.startScheduler(showWebView: showWebView) } - func stopScheduler() { - serverDelegate?.stopScheduler() + // MARK: - DataBrokerProtectionAgentAppEvents + + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + xpcMessageReceivedCompletion(nil) + serverDelegate?.profileSaved() } - func optOutAllBrokers(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.optOutAllBrokers(showWebView: showWebView, completion: completion) + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + xpcMessageReceivedCompletion(nil) + serverDelegate?.appLaunched() } - func startManualScan(showWebView: Bool, - startTime: Date, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.startManualScan(showWebView: showWebView, startTime: startTime, completion: completion) + // MARK: - DataBrokerProtectionAgentDebugCommands + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) } - func runQueuedOperations(showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - serverDelegate?.runQueuedOperations(showWebView: showWebView, completion: completion) + func startImmediateOperations(showWebView: Bool) { + serverDelegate?.startImmediateOperations(showWebView: showWebView) } - func runAllOperations(showWebView: Bool) { - serverDelegate?.runAllOperations(showWebView: showWebView) + func startScheduledOperations(showWebView: Bool) { + serverDelegate?.startScheduledOperations(showWebView: showWebView) } - func openBrowser(domain: String) { - serverDelegate?.openBrowser(domain: domain) + func runAllOptOuts(showWebView: Bool) { + serverDelegate?.runAllOptOuts(showWebView: showWebView) } func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) { - serverDelegate?.getDebugMetadata(completion: completion) + Task { + let metaData = await serverDelegate?.getDebugMetadata() + completion(metaData) + } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerOperationData.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerJobData.swift similarity index 93% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerOperationData.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerJobData.swift index 258d4cba50..3a564ddc52 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerOperationData.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerJobData.swift @@ -1,5 +1,5 @@ // -// BrokerOperationData.swift +// BrokerJobData.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,7 +18,7 @@ import Foundation -protocol BrokerOperationData { +protocol BrokerJobData { var brokerId: Int64 { get } var profileQueryId: Int64 { get } var lastRunDate: Date? { get } @@ -26,7 +26,7 @@ protocol BrokerOperationData { var historyEvents: [HistoryEvent] { get } } -struct ScanOperationData: BrokerOperationData, Sendable { +struct ScanJobData: BrokerJobData, Sendable { let brokerId: Int64 let profileQueryId: Int64 let preferredRunDate: Date? @@ -67,7 +67,7 @@ struct ScanOperationData: BrokerOperationData, Sendable { } } -struct OptOutOperationData: BrokerOperationData, Sendable { +struct OptOutJobData: BrokerJobData, Sendable { let brokerId: Int64 let profileQueryId: Int64 let preferredRunDate: Date? diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift index 375f581128..45a23c5d28 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift @@ -22,15 +22,15 @@ import Common public struct BrokerProfileQueryData: Sendable { let dataBroker: DataBroker let profileQuery: ProfileQuery - let scanOperationData: ScanOperationData - let optOutOperationsData: [OptOutOperationData] + let scanJobData: ScanJobData + let optOutJobData: [OptOutJobData] - var operationsData: [BrokerOperationData] { - optOutOperationsData + [scanOperationData] + var operationsData: [BrokerJobData] { + optOutJobData + [scanJobData] } var extractedProfiles: [ExtractedProfile] { - optOutOperationsData.map { $0.extractedProfile } + optOutJobData.map { $0.extractedProfile } } var events: [HistoryEvent] { @@ -38,16 +38,16 @@ public struct BrokerProfileQueryData: Sendable { } var hasMatches: Bool { - !optOutOperationsData.isEmpty + !optOutJobData.isEmpty } init(dataBroker: DataBroker, profileQuery: ProfileQuery, - scanOperationData: ScanOperationData, - optOutOperationsData: [OptOutOperationData] = [OptOutOperationData]()) { + scanJobData: ScanJobData, + optOutJobData: [OptOutJobData] = [OptOutJobData]()) { self.profileQuery = profileQuery self.dataBroker = dataBroker - self.scanOperationData = scanOperationData - self.optOutOperationsData = optOutOperationsData + self.scanJobData = scanJobData + self.optOutJobData = optOutJobData } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index a43f6d9ae0..926ee58947 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -23,14 +23,13 @@ import BrowserServicesKit import Common protocol DBPUIScanOps: AnyObject { - func startScan(startDate: Date) -> Bool func updateCacheWithCurrentScans() async func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? } final class DBPUIViewModel { private let dataManager: DataBrokerProtectionDataManaging - private let scheduler: DataBrokerProtectionScheduler + private let agentInterface: DataBrokerProtectionAppToAgentInterface private let privacyConfig: PrivacyConfigurationManaging? private let prefs: ContentScopeProperties? @@ -40,13 +39,13 @@ final class DBPUIViewModel { private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() init(dataManager: DataBrokerProtectionDataManaging, - scheduler: DataBrokerProtectionScheduler, + agentInterface: DataBrokerProtectionAppToAgentInterface, webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable, privacyConfig: PrivacyConfigurationManaging? = nil, prefs: ContentScopeProperties? = nil, webView: WKWebView? = nil) { self.dataManager = dataManager - self.scheduler = scheduler + self.agentInterface = agentInterface self.webUISettings = webUISettings self.privacyConfig = privacyConfig self.prefs = prefs @@ -74,9 +73,8 @@ final class DBPUIViewModel { } extension DBPUIViewModel: DBPUIScanOps { - func startScan(startDate: Date) -> Bool { - scheduler.startManualScan(startTime: startDate) - return true + func profileSaved() { + agentInterface.profileSaved() } func updateCacheWithCurrentScans() async { @@ -89,10 +87,6 @@ extension DBPUIViewModel: DBPUIScanOps { } func getBackgroundAgentMetadata() async -> DBPBackgroundAgentMetadata? { - return await withCheckedContinuation { continuation in - scheduler.getDebugMetadata { metadata in - continuation.resume(returning: metadata) - } - } + return await agentInterface.getDebugMetadata() } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift new file mode 100644 index 0000000000..7aa7e6e07a --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift @@ -0,0 +1,312 @@ +// +// DataBrokerJob.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 WebKit +import BrowserServicesKit +import UserScript +import Common + +protocol DataBrokerJob: CCFCommunicationDelegate { + associatedtype ReturnValue + associatedtype InputValue + + var privacyConfig: PrivacyConfigurationManaging { get } + var prefs: ContentScopeProperties { get } + var query: BrokerProfileQueryData { get } + var emailService: EmailServiceProtocol { get } + var captchaService: CaptchaServiceProtocol { get } + var cookieHandler: CookieHandler { get } + var stageCalculator: StageDurationCalculator { get } + var pixelHandler: EventMapping { get } + var sleepObserver: SleepObserver { get } + + var webViewHandler: WebViewHandler? { get set } + var actionsHandler: ActionsHandler? { get } + var continuation: CheckedContinuation? { get set } + var extractedProfile: ExtractedProfile? { get set } + var shouldRunNextStep: () -> Bool { get } + var retriesCountOnError: Int { get set } + var clickAwaitTime: TimeInterval { get } + var postLoadingSiteStartTime: Date? { get set } + + func run(inputValue: InputValue, + webViewHandler: WebViewHandler?, + actionsHandler: ActionsHandler?, + showWebView: Bool) async throws -> ReturnValue + + func executeNextStep() async + func executeCurrentAction() async +} + +extension DataBrokerJob { + func run(inputValue: InputValue, + webViewHandler: WebViewHandler?, + actionsHandler: ActionsHandler?, + shouldRunNextStep: @escaping () -> Bool) async throws -> ReturnValue { + + try await run(inputValue: inputValue, + webViewHandler: webViewHandler, + actionsHandler: actionsHandler, + showWebView: false) + } +} + +extension DataBrokerJob { + + // MARK: - Shared functions + + // swiftlint:disable:next cyclomatic_complexity + func runNextAction(_ action: Action) async { + switch action { + case is GetCaptchaInfoAction: + stageCalculator.setStage(.captchaParse) + case is ClickAction: + stageCalculator.setStage(.fillForm) + case is FillFormAction: + stageCalculator.setStage(.fillForm) + case is ExpectationAction: + stageCalculator.setStage(.submit) + default: () + } + + if let emailConfirmationAction = action as? EmailConfirmationAction { + do { + stageCalculator.fireOptOutSubmit() + try await runEmailConfirmationAction(action: emailConfirmationAction) + await executeNextStep() + } catch { + await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) + } + + return + } + + if action as? SolveCaptchaAction != nil, let captchaTransactionId = actionsHandler?.captchaTransactionId { + actionsHandler?.captchaTransactionId = nil + stageCalculator.setStage(.captchaSolve) + if let captchaData = try? await captchaService.submitCaptchaToBeResolved(for: captchaTransactionId, + attemptId: stageCalculator.attemptId, + shouldRunNextStep: shouldRunNextStep) { + stageCalculator.fireOptOutCaptchaSolve() + await webViewHandler?.execute(action: action, data: .solveCaptcha(CaptchaToken(token: captchaData))) + } else { + await onError(error: DataBrokerProtectionError.captchaServiceError(CaptchaServiceError.nilDataWhenFetchingCaptchaResult)) + } + + return + } + + if action.needsEmail { + do { + stageCalculator.setStage(.emailGenerate) + let emailData = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url, attemptId: stageCalculator.attemptId) + extractedProfile?.email = emailData.emailAddress + stageCalculator.setEmailPattern(emailData.pattern) + stageCalculator.fireOptOutEmailGenerate() + } catch { + await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) + return + } + } + + await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile)) + } + + private func runEmailConfirmationAction(action: EmailConfirmationAction) async throws { + if let email = extractedProfile?.email { + stageCalculator.setStage(.emailReceive) + let url = try await emailService.getConfirmationLink( + from: email, + numberOfRetries: 100, // Move to constant + pollingInterval: action.pollingTime, + attemptId: stageCalculator.attemptId, + shouldRunNextStep: shouldRunNextStep + ) + stageCalculator.fireOptOutEmailReceive() + stageCalculator.setStage(.emailReceive) + do { + try await webViewHandler?.load(url: url) + } catch { + await onError(error: error) + return + } + + stageCalculator.fireOptOutEmailConfirm() + } else { + throw EmailError.cantFindEmail + } + } + + func complete(_ value: ReturnValue) { + self.firePostLoadingDurationPixel(hasError: false) + self.continuation?.resume(returning: value) + self.continuation = nil + } + + func failed(with error: Error) { + self.firePostLoadingDurationPixel(hasError: true) + self.continuation?.resume(throwing: error) + self.continuation = nil + } + + func initialize(handler: WebViewHandler?, + isFakeBroker: Bool = false, + showWebView: Bool) async { + if let handler = handler { // This help us swapping up the WebViewHandler on tests + self.webViewHandler = handler + } else { + self.webViewHandler = await DataBrokerProtectionWebViewHandler(privacyConfig: privacyConfig, prefs: prefs, delegate: self, isFakeBroker: isFakeBroker) + } + + await webViewHandler?.initializeWebView(showWebView: showWebView) + } + + // MARK: - CSSCommunicationDelegate + + func loadURL(url: URL) async { + let webSiteStartLoadingTime = Date() + + do { + // https://app.asana.com/0/1204167627774280/1206912494469284/f + if query.dataBroker.url == "spokeo.com" { + if let cookies = await cookieHandler.getAllCookiesFromDomain(url) { + await webViewHandler?.setCookies(cookies) + } + } + try await webViewHandler?.load(url: url) + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: false) + postLoadingSiteStartTime = Date() + await executeNextStep() + } catch { + fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: true) + await onError(error: error) + } + } + + private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { + if stageCalculator.isImmediateOperation { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL, sleepDuration: sleepObserver.totalSleepTime())) + } + } + + func firePostLoadingDurationPixel(hasError: Bool) { + if stageCalculator.isImmediateOperation, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { + let dataBrokerURL = self.query.dataBroker.url + let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) + pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL, sleepDuration: sleepObserver.totalSleepTime())) + } + } + + func success(actionId: String, actionType: ActionType) async { + switch actionType { + case .click: + stageCalculator.fireOptOutFillForm() + // We wait 40 seconds before tapping + try? await Task.sleep(nanoseconds: UInt64(clickAwaitTime) * 1_000_000_000) + await executeNextStep() + case .fillForm: + stageCalculator.fireOptOutFillForm() + await executeNextStep() + default: await executeNextStep() + } + } + + func captchaInformation(captchaInfo: GetCaptchaInfoResponse) async { + do { + stageCalculator.fireOptOutCaptchaParse() + stageCalculator.setStage(.captchaSend) + actionsHandler?.captchaTransactionId = try await captchaService.submitCaptchaInformation( + captchaInfo, + attemptId: stageCalculator.attemptId, + shouldRunNextStep: shouldRunNextStep) + stageCalculator.fireOptOutCaptchaSend() + await executeNextStep() + } catch { + if let captchaError = error as? CaptchaServiceError { + await onError(error: DataBrokerProtectionError.captchaServiceError(captchaError)) + } else { + await onError(error: DataBrokerProtectionError.captchaServiceError(.errorWhenSubmittingCaptcha)) + } + } + } + + func solveCaptcha(with response: SolveCaptchaResponse) async { + do { + try await webViewHandler?.evaluateJavaScript(response.callback.eval) + + await executeNextStep() + } catch { + await onError(error: DataBrokerProtectionError.solvingCaptchaWithCallbackError) + } + } + + func onError(error: Error) async { + if retriesCountOnError > 0 { + await executeCurrentAction() + } else { + await webViewHandler?.finish() + failed(with: error) + } + } + + func executeCurrentAction() async { + let waitTimeUntilRunningTheActionAgain: TimeInterval = 3 + try? await Task.sleep(nanoseconds: UInt64(waitTimeUntilRunningTheActionAgain) * 1_000_000_000) + + if let currentAction = self.actionsHandler?.currentAction() { + retriesCountOnError -= 1 + await runNextAction(currentAction) + } else { + retriesCountOnError = 0 + await onError(error: DataBrokerProtectionError.unknown("No current action to execute")) + } + } +} + +protocol CookieHandler { + func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? +} + +struct BrokerCookieHandler: CookieHandler { + + func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? { + guard let domainURL = extractSchemeAndHostAsURL(from: url.absoluteString) else { return nil } + do { + let (_, response) = try await URLSession.shared.data(from: domainURL) + guard let httpResponse = response as? HTTPURLResponse, + let allHeaderFields = httpResponse.allHeaderFields as? [String: String] else { return nil } + + let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: domainURL) + return cookies + } catch { + print("Error fetching data: \(error)") + } + + return nil + } + + private func extractSchemeAndHostAsURL(from url: String) -> URL? { + if let urlComponents = URLComponents(string: url), let scheme = urlComponents.scheme, let host = urlComponents.host { + return URL(string: "\(scheme)://\(host)") + } + return nil + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift similarity index 95% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift index f8c2b98aca..08916286f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunner.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift @@ -1,5 +1,5 @@ // -// DataBrokerOperationRunner.swift +// DataBrokerJobRunner.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,7 +20,7 @@ import Foundation import BrowserServicesKit import Common -protocol WebOperationRunner { +protocol WebJobRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, @@ -36,7 +36,7 @@ protocol WebOperationRunner { shouldRunNextStep: @escaping () -> Bool) async throws } -extension WebOperationRunner { +extension WebJobRunner { func scan(_ profileQuery: BrokerProfileQueryData, stageCalculator: StageDurationCalculator, @@ -66,7 +66,7 @@ extension WebOperationRunner { } @MainActor -final class DataBrokerOperationRunner: WebOperationRunner { +final class DataBrokerJobRunner: WebJobRunner { let privacyConfigManager: PrivacyConfigurationManaging let contentScopeProperties: ContentScopeProperties let emailService: EmailServiceProtocol @@ -88,7 +88,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws -> [ExtractedProfile] { let sleepObserver = DataBrokerProtectionSleepObserver(brokerProfileQueryData: profileQuery) - let scan = ScanOperation( + let scan = ScanJob( privacyConfig: privacyConfigManager, prefs: contentScopeProperties, query: profileQuery, @@ -109,7 +109,7 @@ final class DataBrokerOperationRunner: WebOperationRunner { showWebView: Bool, shouldRunNextStep: @escaping () -> Bool) async throws { let sleepObserver = DataBrokerProtectionSleepObserver(brokerProfileQueryData: profileQuery) - let optOut = OptOutOperation( + let optOut = OptOutJob( privacyConfig: privacyConfigManager, prefs: contentScopeProperties, query: profileQuery, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunnerProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunnerProvider.swift similarity index 80% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunnerProvider.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunnerProvider.swift index c9899856ab..d9bf0b8682 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationRunnerProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunnerProvider.swift @@ -1,5 +1,5 @@ // -// DataBrokerOperationRunnerProvider.swift +// DataBrokerJobRunnerProvider.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,15 +19,19 @@ import Foundation import BrowserServicesKit -struct DataBrokerOperationRunnerProvider: OperationRunnerProvider { +protocol JobRunnerProvider { + func getJobRunner() -> WebJobRunner +} + +struct DataBrokerJobRunnerProvider: JobRunnerProvider { var privacyConfigManager: PrivacyConfigurationManaging var contentScopeProperties: ContentScopeProperties var emailService: EmailServiceProtocol var captchaService: CaptchaServiceProtocol @MainActor - func getOperationRunner() -> WebOperationRunner { - DataBrokerOperationRunner(privacyConfigManager: privacyConfigManager, + func getJobRunner() -> WebJobRunner { + DataBrokerJobRunner(privacyConfigManager: privacyConfigManager, contentScopeProperties: contentScopeProperties, emailService: emailService, captchaService: captchaService diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index d9bee8a71f..fa09f9fc11 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -17,296 +17,199 @@ // import Foundation -import WebKit -import BrowserServicesKit -import UserScript import Common -protocol DataBrokerOperation: CCFCommunicationDelegate { - associatedtype ReturnValue - associatedtype InputValue - - var privacyConfig: PrivacyConfigurationManaging { get } - var prefs: ContentScopeProperties { get } - var query: BrokerProfileQueryData { get } - var emailService: EmailServiceProtocol { get } - var captchaService: CaptchaServiceProtocol { get } - var cookieHandler: CookieHandler { get } - var stageCalculator: StageDurationCalculator { get } +protocol DataBrokerOperationDependencies { + var database: DataBrokerProtectionRepository { get } + var config: DataBrokerExecutionConfig { get } + var runnerProvider: JobRunnerProvider { get } + var notificationCenter: NotificationCenter { get } var pixelHandler: EventMapping { get } - var sleepObserver: SleepObserver { get } - - var webViewHandler: WebViewHandler? { get set } - var actionsHandler: ActionsHandler? { get } - var continuation: CheckedContinuation? { get set } - var extractedProfile: ExtractedProfile? { get set } - var shouldRunNextStep: () -> Bool { get } - var retriesCountOnError: Int { get set } - var clickAwaitTime: TimeInterval { get } - var postLoadingSiteStartTime: Date? { get set } - - func run(inputValue: InputValue, - webViewHandler: WebViewHandler?, - actionsHandler: ActionsHandler?, - showWebView: Bool) async throws -> ReturnValue - - func executeNextStep() async - func executeCurrentAction() async + var userNotificationService: DataBrokerProtectionUserNotificationService { get } } -extension DataBrokerOperation { - func run(inputValue: InputValue, - webViewHandler: WebViewHandler?, - actionsHandler: ActionsHandler?, - shouldRunNextStep: @escaping () -> Bool) async throws -> ReturnValue { +struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { + let database: DataBrokerProtectionRepository + var config: DataBrokerExecutionConfig + let runnerProvider: JobRunnerProvider + let notificationCenter: NotificationCenter + let pixelHandler: EventMapping + let userNotificationService: DataBrokerProtectionUserNotificationService +} - try await run(inputValue: inputValue, - webViewHandler: webViewHandler, - actionsHandler: actionsHandler, - showWebView: false) - } +enum OperationType { + case scan + case optOut + case all } -extension DataBrokerOperation { - - // MARK: - Shared functions - - // swiftlint:disable:next cyclomatic_complexity - func runNextAction(_ action: Action) async { - switch action { - case is GetCaptchaInfoAction: - stageCalculator.setStage(.captchaParse) - case is ClickAction: - stageCalculator.setStage(.fillForm) - case is FillFormAction: - stageCalculator.setStage(.fillForm) - case is ExpectationAction: - stageCalculator.setStage(.submit) - default: () - } +protocol DataBrokerOperationErrorDelegate: AnyObject { + func dataBrokerOperationDidError(_ error: Error, withBrokerName brokerName: String?) +} - if let emailConfirmationAction = action as? EmailConfirmationAction { - do { - stageCalculator.fireOptOutSubmit() - try await runEmailConfirmationAction(action: emailConfirmationAction) - await executeNextStep() - } catch { - await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) - } +// swiftlint:disable explicit_non_final_class +class DataBrokerOperation: Operation { - return - } + private let dataBrokerID: Int64 + private let operationType: OperationType + private let priorityDate: Date? // The date to filter and sort operations priorities + private let showWebView: Bool + private(set) weak var errorDelegate: DataBrokerOperationErrorDelegate? // Internal read-only to enable mocking + private let operationDependencies: DataBrokerOperationDependencies - if action as? SolveCaptchaAction != nil, let captchaTransactionId = actionsHandler?.captchaTransactionId { - actionsHandler?.captchaTransactionId = nil - stageCalculator.setStage(.captchaSolve) - if let captchaData = try? await captchaService.submitCaptchaToBeResolved(for: captchaTransactionId, - attemptId: stageCalculator.attemptId, - shouldRunNextStep: shouldRunNextStep) { - stageCalculator.fireOptOutCaptchaSolve() - await webViewHandler?.execute(action: action, data: .solveCaptcha(CaptchaToken(token: captchaData))) - } else { - await onError(error: DataBrokerProtectionError.captchaServiceError(CaptchaServiceError.nilDataWhenFetchingCaptchaResult)) - } + private let id = UUID() + private var _isExecuting = false + private var _isFinished = false + + deinit { + os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) + } + + init(dataBrokerID: Int64, + operationType: OperationType, + priorityDate: Date? = nil, + showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, + operationDependencies: DataBrokerOperationDependencies) { + self.dataBrokerID = dataBrokerID + self.priorityDate = priorityDate + self.operationType = operationType + self.showWebView = showWebView + self.errorDelegate = errorDelegate + self.operationDependencies = operationDependencies + super.init() + } + + override func start() { + if isCancelled { + finish() return } - if action.needsEmail { - do { - stageCalculator.setStage(.emailGenerate) - let emailData = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url, attemptId: stageCalculator.attemptId) - extractedProfile?.email = emailData.emailAddress - stageCalculator.setEmailPattern(emailData.pattern) - stageCalculator.fireOptOutEmailGenerate() - } catch { - await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) - return - } - } + willChangeValue(forKey: #keyPath(isExecuting)) + _isExecuting = true + didChangeValue(forKey: #keyPath(isExecuting)) - await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile)) + main() } - private func runEmailConfirmationAction(action: EmailConfirmationAction) async throws { - if let email = extractedProfile?.email { - stageCalculator.setStage(.emailReceive) - let url = try await emailService.getConfirmationLink( - from: email, - numberOfRetries: 100, // Move to constant - pollingInterval: action.pollingTime, - attemptId: stageCalculator.attemptId, - shouldRunNextStep: shouldRunNextStep - ) - stageCalculator.fireOptOutEmailReceive() - stageCalculator.setStage(.emailReceive) - do { - try await webViewHandler?.load(url: url) - } catch { - await onError(error: error) - return - } - - stageCalculator.fireOptOutEmailConfirm() - } else { - throw EmailError.cantFindEmail - } + override var isAsynchronous: Bool { + return true } - func complete(_ value: ReturnValue) { - self.firePostLoadingDurationPixel(hasError: false) - self.continuation?.resume(returning: value) - self.continuation = nil + override var isExecuting: Bool { + return _isExecuting } - func failed(with error: Error) { - self.firePostLoadingDurationPixel(hasError: true) - self.continuation?.resume(throwing: error) - self.continuation = nil + override var isFinished: Bool { + return _isFinished } - func initialize(handler: WebViewHandler?, - isFakeBroker: Bool = false, - showWebView: Bool) async { - if let handler = handler { // This help us swapping up the WebViewHandler on tests - self.webViewHandler = handler - } else { - self.webViewHandler = await DataBrokerProtectionWebViewHandler(privacyConfig: privacyConfig, prefs: prefs, delegate: self, isFakeBroker: isFakeBroker) + override func main() { + Task { + await runOperation() + finish() } - - await webViewHandler?.initializeWebView(showWebView: showWebView) } - // MARK: - CSSCommunicationDelegate - - func loadURL(url: URL) async { - let webSiteStartLoadingTime = Date() + private func filterAndSortOperationsData(brokerProfileQueriesData: [BrokerProfileQueryData], operationType: OperationType, priorityDate: Date?) -> [BrokerJobData] { + let operationsData: [BrokerJobData] - do { - // https://app.asana.com/0/1204167627774280/1206912494469284/f - if query.dataBroker.url == "spokeo.com" { - if let cookies = await cookieHandler.getAllCookiesFromDomain(url) { - await webViewHandler?.setCookies(cookies) - } - } - try await webViewHandler?.load(url: url) - fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: false) - postLoadingSiteStartTime = Date() - await executeNextStep() - } catch { - fireSiteLoadingPixel(startTime: webSiteStartLoadingTime, hasError: true) - await onError(error: error) + switch operationType { + case .optOut: + operationsData = brokerProfileQueriesData.flatMap { $0.optOutJobData } + case .scan: + operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanJobData } + case .all: + operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } } - } - private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { - if stageCalculator.isManualScan { - let dataBrokerURL = self.query.dataBroker.url - let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) - pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL, sleepDuration: sleepObserver.totalSleepTime())) - } - } + let filteredAndSortedOperationsData: [BrokerJobData] - func firePostLoadingDurationPixel(hasError: Bool) { - if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { - let dataBrokerURL = self.query.dataBroker.url - let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) - pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL, sleepDuration: sleepObserver.totalSleepTime())) + if let priorityDate = priorityDate { + filteredAndSortedOperationsData = operationsData + .filter { $0.preferredRunDate != nil && $0.preferredRunDate! <= priorityDate } + .sorted { $0.preferredRunDate! < $1.preferredRunDate! } + } else { + filteredAndSortedOperationsData = operationsData } - } - func success(actionId: String, actionType: ActionType) async { - switch actionType { - case .click: - stageCalculator.fireOptOutFillForm() - // We wait 40 seconds before tapping - try? await Task.sleep(nanoseconds: UInt64(clickAwaitTime) * 1_000_000_000) - await executeNextStep() - case .fillForm: - stageCalculator.fireOptOutFillForm() - await executeNextStep() - default: await executeNextStep() - } + return filteredAndSortedOperationsData } - func captchaInformation(captchaInfo: GetCaptchaInfoResponse) async { - do { - stageCalculator.fireOptOutCaptchaParse() - stageCalculator.setStage(.captchaSend) - actionsHandler?.captchaTransactionId = try await captchaService.submitCaptchaInformation( - captchaInfo, - attemptId: stageCalculator.attemptId, - shouldRunNextStep: shouldRunNextStep) - stageCalculator.fireOptOutCaptchaSend() - await executeNextStep() - } catch { - if let captchaError = error as? CaptchaServiceError { - await onError(error: DataBrokerProtectionError.captchaServiceError(captchaError)) - } else { - await onError(error: DataBrokerProtectionError.captchaServiceError(.errorWhenSubmittingCaptcha)) - } - } - } + private func runOperation() async { + let allBrokerProfileQueryData: [BrokerProfileQueryData] - func solveCaptcha(with response: SolveCaptchaResponse) async { do { - try await webViewHandler?.evaluateJavaScript(response.callback.eval) - - await executeNextStep() + allBrokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() } catch { - await onError(error: DataBrokerProtectionError.solvingCaptchaWithCallbackError) + os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) + return } - } - func onError(error: Error) async { - if retriesCountOnError > 0 { - await executeCurrentAction() - } else { - await webViewHandler?.finish() - failed(with: error) - } - } + let brokerProfileQueriesData = allBrokerProfileQueryData.filter { $0.dataBroker.id == dataBrokerID } - func executeCurrentAction() async { - let waitTimeUntilRunningTheActionAgain: TimeInterval = 3 - try? await Task.sleep(nanoseconds: UInt64(waitTimeUntilRunningTheActionAgain) * 1_000_000_000) + let filteredAndSortedOperationsData = filterAndSortOperationsData(brokerProfileQueriesData: brokerProfileQueriesData, + operationType: operationType, + priorityDate: priorityDate) - if let currentAction = self.actionsHandler?.currentAction() { - retriesCountOnError -= 1 - await runNextAction(currentAction) - } else { - retriesCountOnError = 0 - await onError(error: DataBrokerProtectionError.unknown("No current action to execute")) - } - } -} + os_log("filteredAndSortedOperationsData count: %{public}d for brokerID %{public}d", log: .dataBrokerProtection, filteredAndSortedOperationsData.count, dataBrokerID) -protocol CookieHandler { - func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? -} + for operationData in filteredAndSortedOperationsData { + if isCancelled { + os_log("Cancelled operation, returning...", log: .dataBrokerProtection) + return + } -struct BrokerCookieHandler: CookieHandler { + let brokerProfileData = brokerProfileQueriesData.filter { + $0.dataBroker.id == operationData.brokerId && $0.profileQuery.id == operationData.profileQueryId + }.first - func getAllCookiesFromDomain(_ url: URL) async -> [HTTPCookie]? { - guard let domainURL = extractSchemeAndHostAsURL(from: url.absoluteString) else { return nil } - do { - let (_, response) = try await URLSession.shared.data(from: domainURL) - guard let httpResponse = response as? HTTPURLResponse, - let allHeaderFields = httpResponse.allHeaderFields as? [String: String] else { return nil } + guard let brokerProfileData = brokerProfileData else { + continue + } + do { + os_log("Running operation: %{public}@", log: .dataBrokerProtection, String(describing: operationData)) + + try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, + brokerProfileQueryData: brokerProfileData, + database: operationDependencies.database, + notificationCenter: operationDependencies.notificationCenter, + runner: operationDependencies.runnerProvider.getJobRunner(), + pixelHandler: operationDependencies.pixelHandler, + showWebView: showWebView, + isImmediateOperation: operationType == .scan, + userNotificationService: operationDependencies.userNotificationService, + shouldRunNextStep: { [weak self] in + guard let self = self else { return false } + return !self.isCancelled + }) + + let sleepInterval = operationDependencies.config.intervalBetweenSameBrokerOperations + os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) + try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) + } catch { + os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: domainURL) - return cookies - } catch { - print("Error fetching data: \(error)") + errorDelegate?.dataBrokerOperationDidError(error, withBrokerName: brokerProfileQueriesData.first?.dataBroker.name) + } } - return nil + finish() } - private func extractSchemeAndHostAsURL(from url: String) -> URL? { - if let urlComponents = URLComponents(string: url), let scheme = urlComponents.scheme, let host = urlComponents.host { - return URL(string: "\(scheme)://\(host)") - } - return nil + private func finish() { + willChangeValue(forKey: #keyPath(isExecuting)) + willChangeValue(forKey: #keyPath(isFinished)) + + _isExecuting = false + _isFinished = true + + didChangeValue(forKey: #keyPath(isExecuting)) + didChangeValue(forKey: #keyPath(isFinished)) + + os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) } } +// swiftlint:enable explicit_non_final_class diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift deleted file mode 100644 index d585da0cd7..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// DataBrokerOperationsCollection.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 Common - -protocol DataBrokerOperationsCollectionErrorDelegate: AnyObject { - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, - didError error: Error, - whileRunningBrokerOperationData: BrokerOperationData, - withDataBrokerName dataBrokerName: String?) - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, - didErrorBeforeStartingBrokerOperations error: Error) -} - -enum OperationType { - case manualScan - case optOut - case all -} - -final class DataBrokerOperationsCollection: Operation { - public var error: Error? - public weak var errorDelegate: DataBrokerOperationsCollectionErrorDelegate? - - private let dataBrokerID: Int64 - private let database: DataBrokerProtectionRepository - private let id = UUID() - private var _isExecuting = false - private var _isFinished = false - private let intervalBetweenOperations: TimeInterval? // The time in seconds to wait in-between operations - private let priorityDate: Date? // The date to filter and sort operations priorities - private let operationType: OperationType - private let notificationCenter: NotificationCenter - private let runner: WebOperationRunner - private let pixelHandler: EventMapping - private let showWebView: Bool - private let userNotificationService: DataBrokerProtectionUserNotificationService - - deinit { - os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) - } - - init(dataBrokerID: Int64, - database: DataBrokerProtectionRepository, - operationType: OperationType, - intervalBetweenOperations: TimeInterval? = nil, - priorityDate: Date? = nil, - notificationCenter: NotificationCenter = NotificationCenter.default, - runner: WebOperationRunner, - pixelHandler: EventMapping, - userNotificationService: DataBrokerProtectionUserNotificationService, - showWebView: Bool) { - - self.dataBrokerID = dataBrokerID - self.database = database - self.intervalBetweenOperations = intervalBetweenOperations - self.priorityDate = priorityDate - self.operationType = operationType - self.notificationCenter = notificationCenter - self.runner = runner - self.pixelHandler = pixelHandler - self.showWebView = showWebView - self.userNotificationService = userNotificationService - super.init() - } - - override func start() { - if isCancelled { - finish() - return - } - - willChangeValue(forKey: #keyPath(isExecuting)) - _isExecuting = true - didChangeValue(forKey: #keyPath(isExecuting)) - - main() - } - - override var isAsynchronous: Bool { - return true - } - - override var isExecuting: Bool { - return _isExecuting - } - - override var isFinished: Bool { - return _isFinished - } - - override func main() { - Task { - await runOperation() - finish() - } - } - - private func filterAndSortOperationsData(brokerProfileQueriesData: [BrokerProfileQueryData], operationType: OperationType, priorityDate: Date?) -> [BrokerOperationData] { - let operationsData: [BrokerOperationData] - - switch operationType { - case .optOut: - operationsData = brokerProfileQueriesData.flatMap { $0.optOutOperationsData } - case .manualScan: - operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanOperationData } - case .all: - operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } - } - - let filteredAndSortedOperationsData: [BrokerOperationData] - - if let priorityDate = priorityDate { - filteredAndSortedOperationsData = operationsData - .filter { $0.preferredRunDate != nil && $0.preferredRunDate! <= priorityDate } - .sorted { $0.preferredRunDate! < $1.preferredRunDate! } - } else { - filteredAndSortedOperationsData = operationsData - } - - return filteredAndSortedOperationsData - } - - // swiftlint:disable:next function_body_length - private func runOperation() async { - let allBrokerProfileQueryData: [BrokerProfileQueryData] - - do { - allBrokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() - } catch { - os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) - errorDelegate?.dataBrokerOperationsCollection(self, didErrorBeforeStartingBrokerOperations: error) - return - } - - let brokerProfileQueriesData = allBrokerProfileQueryData.filter { $0.dataBroker.id == dataBrokerID } - - let filteredAndSortedOperationsData = filterAndSortOperationsData(brokerProfileQueriesData: brokerProfileQueriesData, - operationType: operationType, - priorityDate: priorityDate) - - os_log("filteredAndSortedOperationsData count: %{public}d for brokerID %{public}d", log: .dataBrokerProtection, filteredAndSortedOperationsData.count, dataBrokerID) - - for operationData in filteredAndSortedOperationsData { - if isCancelled { - os_log("Cancelled operation, returning...", log: .dataBrokerProtection) - return - } - - let brokerProfileData = brokerProfileQueriesData.filter { - $0.dataBroker.id == operationData.brokerId && $0.profileQuery.id == operationData.profileQueryId - }.first - - guard let brokerProfileData = brokerProfileData else { - continue - } - do { - os_log("Running operation: %{public}@", log: .dataBrokerProtection, String(describing: operationData)) - - try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, - brokerProfileQueryData: brokerProfileData, - database: database, - notificationCenter: notificationCenter, - runner: runner, - pixelHandler: pixelHandler, - showWebView: showWebView, - isManualScan: operationType == .manualScan, - userNotificationService: userNotificationService, - shouldRunNextStep: { [weak self] in - guard let self = self else { return false } - return !self.isCancelled - }) - - if let sleepInterval = intervalBetweenOperations { - os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) - try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) - } - - } catch { - os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - self.error = error - errorDelegate?.dataBrokerOperationsCollection(self, - didError: error, - whileRunningBrokerOperationData: operationData, - withDataBrokerName: brokerProfileQueriesData.first?.dataBroker.name) - } - } - - finish() - } - - private func finish() { - willChangeValue(forKey: #keyPath(isExecuting)) - willChangeValue(forKey: #keyPath(isFinished)) - - _isExecuting = false - _isFinished = true - - didChangeValue(forKey: #keyPath(isExecuting)) - didChangeValue(forKey: #keyPath(isFinished)) - - os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index f7eeb24d2b..72b5772a8b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -28,24 +28,24 @@ protocol OperationsManager { // We want to refactor this to return a NSOperation in the future // so we have more control of stopping/starting the queue // for the time being, shouldRunNextStep: @escaping () -> Bool is being used - func runOperation(operationData: BrokerOperationData, + func runOperation(operationData: BrokerJobData, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, - runner: WebOperationRunner, + runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool, - isManualScan: Bool, + isImmediateOperation: Bool, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws } extension OperationsManager { - func runOperation(operationData: BrokerOperationData, + func runOperation(operationData: BrokerJobData, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, - runner: WebOperationRunner, + runner: WebJobRunner, pixelHandler: EventMapping, userNotificationService: DataBrokerProtectionUserNotificationService, isManual: Bool, @@ -58,7 +58,7 @@ extension OperationsManager { runner: runner, pixelHandler: pixelHandler, showWebView: false, - isManualScan: isManual, + isImmediateOperation: isManual, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } @@ -66,29 +66,29 @@ extension OperationsManager { struct DataBrokerProfileQueryOperationManager: OperationsManager { - internal func runOperation(operationData: BrokerOperationData, + internal func runOperation(operationData: BrokerJobData, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter = NotificationCenter.default, - runner: WebOperationRunner, + runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool = false, - isManualScan: Bool = false, + isImmediateOperation: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { - if operationData as? ScanOperationData != nil { + if operationData as? ScanJobData != nil { try await runScanOperation(on: runner, brokerProfileQueryData: brokerProfileQueryData, database: database, notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, - isManual: isManualScan, + isManual: isImmediateOperation, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) - } else if let optOutOperationData = operationData as? OptOutOperationData { - try await runOptOutOperation(for: optOutOperationData.extractedProfile, + } else if let optOutJobData = operationData as? OptOutJobData { + try await runOptOutOperation(for: optOutJobData.extractedProfile, on: runner, brokerProfileQueryData: brokerProfileQueryData, database: database, @@ -101,7 +101,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } // swiftlint:disable:next cyclomatic_complexity function_body_length - internal func runScanOperation(on runner: WebOperationRunner, + internal func runScanOperation(on runner: WebJobRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, @@ -126,7 +126,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler, - isManualScan: isManual) + isImmediateOperation: isManual) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) @@ -169,13 +169,13 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // This is done inside a transaction on the database side. We insert the extracted profile and then // we insert the opt-out operation, we do not want to do things separately in case creating an opt-out fails // causing the extracted profile to be orphan. - let optOutOperationData = OptOutOperationData(brokerId: brokerId, + let optOutJobData = OptOutJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: preferredRunOperation, historyEvents: [HistoryEvent](), extractedProfile: extractedProfile) - try database.saveOptOutOperation(optOut: optOutOperationData, extractedProfile: extractedProfile) + try database.saveOptOutJob(optOut: optOutJobData, extractedProfile: extractedProfile) os_log("Creating new opt-out operation data for: %@", log: .dataBrokerProtection, String(describing: extractedProfile.name)) } @@ -269,7 +269,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // swiftlint:disable:next function_body_length internal func runOptOutOperation(for extractedProfile: ExtractedProfile, - on runner: WebOperationRunner, + on runner: WebJobRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index cc0df841f6..8e47a7b6a7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -98,7 +98,13 @@ final class AppVersionNumber: AppVersionNumberProvider { var versionNumber: String = AppVersion.shared.versionNumber } -public struct DataBrokerProtectionBrokerUpdater { +protocol DataBrokerProtectionBrokerUpdater { + static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? + func updateBrokers() + func checkForUpdatesInBrokerJSONFiles() +} + +public struct DefaultDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrokerUpdater { private let repository: BrokerUpdaterRepository private let resources: ResourcesRepository @@ -118,9 +124,9 @@ public struct DataBrokerProtectionBrokerUpdater { self.pixelHandler = pixelHandler } - public static func provide() -> DataBrokerProtectionBrokerUpdater? { + public static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? { if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { - return DataBrokerProtectionBrokerUpdater(vault: vault) + return DefaultDataBrokerProtectionBrokerUpdater(vault: vault) } os_log("Error when trying to create vault for data broker protection updater debug menu item", log: .dataBrokerProtection) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift index 73cb7fb2b1..134de2edd0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift @@ -50,21 +50,21 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { guard let brokerProfileQuery = try database.brokerProfileQueryData(for: brokerId, and: profileQueryId) else { return } - try updateScanOperationDataDates(origin: origin, + try updateScanJobDataDates(origin: origin, + brokerId: brokerId, + profileQueryId: profileQueryId, + extractedProfileId: extractedProfileId, + schedulingConfig: schedulingConfig, + brokerProfileQuery: brokerProfileQuery) + + // We only need to update the optOut date if we have an extracted profile ID + if let extractedProfileId = extractedProfileId { + try updateOptOutJobDataDates(origin: origin, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId, schedulingConfig: schedulingConfig, brokerProfileQuery: brokerProfileQuery) - - // We only need to update the optOut date if we have an extracted profile ID - if let extractedProfileId = extractedProfileId { - try updateOptOutOperationDataDates(origin: origin, - brokerId: brokerId, - profileQueryId: profileQueryId, - extractedProfileId: extractedProfileId, - schedulingConfig: schedulingConfig, - brokerProfileQuery: brokerProfileQuery) } } @@ -88,14 +88,14 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } } - private func updateScanOperationDataDates(origin: OperationPreferredDateUpdaterOrigin, - brokerId: Int64, - profileQueryId: Int64, - extractedProfileId: Int64?, - schedulingConfig: DataBrokerScheduleConfig, - brokerProfileQuery: BrokerProfileQueryData) throws { + private func updateScanJobDataDates(origin: OperationPreferredDateUpdaterOrigin, + brokerId: Int64, + profileQueryId: Int64, + extractedProfileId: Int64?, + schedulingConfig: DataBrokerScheduleConfig, + brokerProfileQuery: BrokerProfileQueryData) throws { - let currentScanPreferredRunDate = brokerProfileQuery.scanOperationData.preferredRunDate + let currentScanPreferredRunDate = brokerProfileQuery.scanJobData.preferredRunDate var newScanPreferredRunDate = try calculator.dateForScanOperation(currentPreferredRunDate: currentScanPreferredRunDate, historyEvents: brokerProfileQuery.events, @@ -114,15 +114,15 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } } - private func updateOptOutOperationDataDates(origin: OperationPreferredDateUpdaterOrigin, - brokerId: Int64, - profileQueryId: Int64, - extractedProfileId: Int64?, - schedulingConfig: DataBrokerScheduleConfig, - brokerProfileQuery: BrokerProfileQueryData) throws { + private func updateOptOutJobDataDates(origin: OperationPreferredDateUpdaterOrigin, + brokerId: Int64, + profileQueryId: Int64, + extractedProfileId: Int64?, + schedulingConfig: DataBrokerScheduleConfig, + brokerProfileQuery: BrokerProfileQueryData) throws { - let optOutOperation = brokerProfileQuery.optOutOperationsData.filter { $0.extractedProfile.id == extractedProfileId }.first - let currentOptOutPreferredRunDate = optOutOperation?.preferredRunDate + let optOutJob = brokerProfileQuery.optOutJobData.filter { $0.extractedProfile.id == extractedProfileId }.first + let currentOptOutPreferredRunDate = optOutJob?.preferredRunDate var newOptOutPreferredDate = try calculator.dateForOptOutOperation(currentPreferredRunDate: currentOptOutPreferredRunDate, historyEvents: brokerProfileQuery.events, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift similarity index 98% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift index 98b29f6827..b1d32657e2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift @@ -1,5 +1,5 @@ // -// OptOutOperation.swift +// OptOutJob.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +22,7 @@ import BrowserServicesKit import UserScript import Common -final class OptOutOperation: DataBrokerOperation { +final class OptOutJob: DataBrokerJob { typealias ReturnValue = Void typealias InputValue = ExtractedProfile diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift similarity index 85% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift index d4d83f7b2a..ae72a5ae18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift @@ -1,5 +1,5 @@ // -// MismatchCalculatorUseCase.swift +// MismatchCalculator.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -36,7 +36,12 @@ enum MismatchValues: Int { } } -struct MismatchCalculatorUseCase { +protocol MismatchCalculator { + init(database: DataBrokerProtectionRepository, pixelHandler: EventMapping) + func calculateMismatches() +} + +struct DefaultMismatchCalculator: MismatchCalculator { let database: DataBrokerProtectionRepository let pixelHandler: EventMapping @@ -52,14 +57,14 @@ struct MismatchCalculatorUseCase { let parentBrokerProfileQueryData = brokerProfileQueryData.filter { $0.dataBroker.parent == nil } for parent in parentBrokerProfileQueryData { - guard let parentMatches = parent.scanOperationData.historyEvents.matchesForLastEvent() else { continue } + guard let parentMatches = parent.scanJobData.historyEvents.matchesForLastEvent() else { continue } let children = brokerProfileQueryData.filter { $0.dataBroker.parent == parent.dataBroker.url && $0.profileQuery.id == parent.profileQuery.id } for child in children { - guard let childMatches = child.scanOperationData.historyEvents.matchesForLastEvent() else { continue } + guard let childMatches = child.scanJobData.historyEvents.matchesForLastEvent() else { continue } let mismatchValue = MismatchValues.calculate(parent: parentMatches, child: childMatches) pixelHandler.fire( diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift similarity index 98% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift index 7f7ba274a9..334017ae18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift @@ -1,5 +1,5 @@ // -// ScanOperation.swift +// ScanJob.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +22,7 @@ import BrowserServicesKit import UserScript import Common -final class ScanOperation: DataBrokerOperation { +final class ScanJob: DataBrokerJob { typealias ReturnValue = [ExtractedProfile] typealias InputValue = Void diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift index 589ab33afb..ea371e0d27 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift @@ -102,7 +102,7 @@ final class DataBrokerProtectionEventPixels { var removalsInTheLastWeek = 0 for query in data { - let allHistoryEventsForQuery = query.scanOperationData.historyEvents + query.optOutOperationsData.flatMap { $0.historyEvents } + let allHistoryEventsForQuery = query.scanJobData.historyEvents + query.optOutJobData.flatMap { $0.historyEvents } let historyEventsInThePastWeek = allHistoryEventsForQuery.filter { !didWeekPassedBetweenDates(start: $0.date, end: Date()) } @@ -132,7 +132,7 @@ final class DataBrokerProtectionEventPixels { } private func hadScanThisWeek(_ brokerProfileQuery: BrokerProfileQueryData) -> Bool { - return brokerProfileQuery.scanOperationData.historyEvents.contains { historyEvent in + return brokerProfileQuery.scanJobData.historyEvents.contains { historyEvent in !didWeekPassedBetweenDates(start: historyEvent.date, end: Date()) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index c9edeedfc4..f83d3c6aeb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -64,7 +64,7 @@ public enum DataBrokerProtectionPixels { static let wasOnWaitlist = "was_on_waitlist" static let httpCode = "http_code" static let backendServiceCallSite = "backend_service_callsite" - static let isManualScan = "is_manual_scan" + static let isImmediateOperation = "is_manual_scan" static let durationInMs = "duration_in_ms" static let profileQueries = "profile_queries" static let hasError = "has_error" @@ -99,39 +99,22 @@ public enum DataBrokerProtectionPixels { // Backgrond Agent events case backgroundAgentStarted case backgroundAgentStartedStoppingDueToAnotherInstanceRunning - case backgroundAgentRunOperationsAndStartSchedulerIfPossible - case backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile - // There's currently no point firing this because the scheduler never calls the completion with an error - // case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackError(error: Error) - case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler // IPC server events - case ipcServerStartSchedulerCalledByApp - case ipcServerStartSchedulerReceivedByAgent - case ipcServerStartSchedulerXPCError(error: Error?) - - case ipcServerStopSchedulerCalledByApp - case ipcServerStopSchedulerReceivedByAgent - case ipcServerStopSchedulerXPCError(error: Error?) - - case ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions - case ipcServerScanAllBrokersAttemptedToCallInWrongDirectory - case ipcServerScanAllBrokersCalledByApp - case ipcServerScanAllBrokersReceivedByAgent - case ipcServerScanAllBrokersXPCError(error: Error?) - - case ipcServerScanAllBrokersCompletedOnAgentWithoutError - case ipcServerScanAllBrokersCompletedOnAgentWithError(error: Error?) - case ipcServerScanAllBrokersCompletionCalledOnAppWithoutError - case ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: Error?) - case ipcServerScanAllBrokersInterruptedOnAgent - case ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption - - case ipcServerOptOutAllBrokers - case ipcServerOptOutAllBrokersCompletion(error: Error?) - case ipcServerRunQueuedOperations - case ipcServerRunQueuedOperationsCompletion(error: Error?) - case ipcServerRunAllOperations + case ipcServerProfileSavedCalledByApp + case ipcServerProfileSavedReceivedByAgent + case ipcServerProfileSavedXPCError(error: Error?) + case ipcServerImmediateScansInterrupted + case ipcServerImmediateScansFinishedWithoutError + case ipcServerImmediateScansFinishedWithError(error: Error?) + + case ipcServerAppLaunchedCalledByApp + case ipcServerAppLaunchedReceivedByAgent + case ipcServerAppLaunchedXPCError(error: Error?) + case ipcServerAppLaunchedScheduledScansBlocked + case ipcServerAppLaunchedScheduledScansInterrupted + case ipcServerAppLaunchedScheduledScansFinishedWithoutError + case ipcServerAppLaunchedScheduledScansFinishedWithError(error: Error?) // DataBrokerProtection User Notifications case dataBrokerProtectionNotificationSentFirstScanComplete @@ -144,9 +127,9 @@ public enum DataBrokerProtectionPixels { case dataBrokerProtectionNotificationOpenedAllRecordsRemoved // Scan/Search pixels - case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isManualScan: Bool) - case scanFailed(dataBroker: String, duration: Double, tries: Int, isManualScan: Bool) - case scanError(dataBroker: String, duration: Double, category: String, details: String, isManualScan: Bool) + case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isImmediateOperation: Bool) + case scanFailed(dataBroker: String, duration: Double, tries: Int, isImmediateOperation: Bool) + case scanError(dataBroker: String, duration: Double, category: String, details: String, isImmediateOperation: Bool) // KPIs - engagement case dailyActiveUser @@ -220,35 +203,21 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentStarted: return "m_mac_dbp_background-agent_started" case .backgroundAgentStartedStoppingDueToAnotherInstanceRunning: return "m_mac_dbp_background-agent_started_stopping-due-to-another-instance-running" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossible: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_no-saved-profile" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" - - case .ipcServerStartSchedulerCalledByApp: return "m_mac_dbp_ipc-server_start-scheduler_called-by-app" - case .ipcServerStartSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_start-scheduler_received-by-agent" - case .ipcServerStartSchedulerXPCError: return "m_mac_dbp_ipc-server_start-scheduler_xpc-error" - - case .ipcServerStopSchedulerCalledByApp: return "m_mac_dbp_ipc-server_stop-scheduler_called-by-app" - case .ipcServerStopSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_stop-scheduler_received-by-agent" - case .ipcServerStopSchedulerXPCError: return "m_mac_dbp_ipc-server_stop-scheduler_xpc-error" - - case .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions: return "m_mac_dbp_ipc-server_scan-all-brokers_attempted-to-call-without-login-item-permissions" - case .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory: return "m_mac_dbp_ipc-server_scan-all-brokers_attempted-to-call-in-wrong-directory" - case .ipcServerScanAllBrokersCalledByApp: return "m_mac_dbp_ipc-server_scan-all-brokers_called-by-app" - case .ipcServerScanAllBrokersReceivedByAgent: return "m_mac_dbp_ipc-server_scan-all-brokers_received-by-agent" - case .ipcServerScanAllBrokersXPCError: return "m_mac_dbp_ipc-server_scan-all-brokers_xpc-error" - case .ipcServerScanAllBrokersCompletedOnAgentWithoutError: return "m_mac_dbp_ipc-server_scan-all-brokers_completed-on-agent_without-error" - case .ipcServerScanAllBrokersCompletedOnAgentWithError: return "m_mac_dbp_ipc-server_scan-all-brokers_completed-on-agent_with-error" - case .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_without-error" - case .ipcServerScanAllBrokersCompletionCalledOnAppWithError: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_with-error" - case .ipcServerScanAllBrokersInterruptedOnAgent: return "m_mac_dbp_ipc-server_scan-all-brokers_interrupted-on-agent" - case .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: return "m_mac_dbp_ipc-server_scan-all-brokers_completion-called-on-app_after-interruption" - - case .ipcServerOptOutAllBrokers: return "m_mac_dbp_ipc-server_opt-out-all-brokers" - case .ipcServerOptOutAllBrokersCompletion: return "m_mac_dbp_ipc-server_opt-out-all-brokers_completion" - case .ipcServerRunQueuedOperations: return "m_mac_dbp_ipc-server_run-queued-operations" - case .ipcServerRunQueuedOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" - case .ipcServerRunAllOperations: return "m_mac_dbp_ipc-server_run-all-operations" + // IPC Server Pixels + case .ipcServerProfileSavedCalledByApp: return "m_mac_dbp_ipc-server_profile-saved_called-by-app" + case .ipcServerProfileSavedReceivedByAgent: return "m_mac_dbp_ipc-server_profile-saved_received-by-agent" + case .ipcServerProfileSavedXPCError: return "m_mac_dbp_ipc-server_profile-saved_xpc-error" + case .ipcServerImmediateScansInterrupted: return "m_mac_dbp_ipc-server_immediate-scans_interrupted" + case .ipcServerImmediateScansFinishedWithoutError: return "m_mac_dbp_ipc-server_immediate-scans_finished_without-error" + case .ipcServerImmediateScansFinishedWithError: return "m_mac_dbp_ipc-server_immediate-scans_finished_with-error" + + case .ipcServerAppLaunchedCalledByApp: return "m_mac_dbp_ipc-server_app-launched_called-by-app" + case .ipcServerAppLaunchedReceivedByAgent: return "m_mac_dbp_ipc-server_app-launched_received-by-agent" + case .ipcServerAppLaunchedXPCError: return "m_mac_dbp_ipc-server_app-launched_xpc-error" + case .ipcServerAppLaunchedScheduledScansBlocked: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_blocked" + case .ipcServerAppLaunchedScheduledScansInterrupted: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_interrupted" + case .ipcServerAppLaunchedScheduledScansFinishedWithoutError: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_finished_without-error" + case .ipcServerAppLaunchedScheduledScansFinishedWithError: return "m_mac_dbp_ipc-server_app-launched_scheduled-scans_finished_with-error" // User Notifications case .dataBrokerProtectionNotificationSentFirstScanComplete: @@ -372,9 +341,6 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .webUILoadingFailed(let error): return [Consts.errorCategoryKey: error] case .backgroundAgentStarted, - .backgroundAgentRunOperationsAndStartSchedulerIfPossible, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, .dataBrokerProtectionNotificationSentFirstScanComplete, .dataBrokerProtectionNotificationOpenedFirstScanComplete, @@ -399,35 +365,26 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .secureVaultInitError, .secureVaultError: return [:] - case .ipcServerStartSchedulerCalledByApp, - .ipcServerStartSchedulerReceivedByAgent, - .ipcServerStartSchedulerXPCError, - .ipcServerStopSchedulerCalledByApp, - .ipcServerStopSchedulerReceivedByAgent, - .ipcServerStopSchedulerXPCError, - .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, - .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, - .ipcServerScanAllBrokersCalledByApp, - .ipcServerScanAllBrokersReceivedByAgent, - .ipcServerScanAllBrokersXPCError, - .ipcServerScanAllBrokersCompletedOnAgentWithoutError, - .ipcServerScanAllBrokersCompletedOnAgentWithError, - .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, - .ipcServerScanAllBrokersCompletionCalledOnAppWithError, - .ipcServerScanAllBrokersInterruptedOnAgent, - .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption, - .ipcServerOptOutAllBrokers, - .ipcServerOptOutAllBrokersCompletion, - .ipcServerRunQueuedOperations, - .ipcServerRunQueuedOperationsCompletion, - .ipcServerRunAllOperations: + case .ipcServerProfileSavedCalledByApp, + .ipcServerProfileSavedReceivedByAgent, + .ipcServerProfileSavedXPCError, + .ipcServerImmediateScansInterrupted, + .ipcServerImmediateScansFinishedWithoutError, + .ipcServerImmediateScansFinishedWithError, + .ipcServerAppLaunchedCalledByApp, + .ipcServerAppLaunchedReceivedByAgent, + .ipcServerAppLaunchedXPCError, + .ipcServerAppLaunchedScheduledScansBlocked, + .ipcServerAppLaunchedScheduledScansInterrupted, + .ipcServerAppLaunchedScheduledScansFinishedWithoutError, + .ipcServerAppLaunchedScheduledScansFinishedWithError: return [Consts.bundleIDParamKey: Bundle.main.bundleIdentifier ?? "nil"] - case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] - case .scanFailed(let dataBroker, let duration, let tries, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] - case .scanError(let dataBroker, let duration, let category, let details, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isManualScan: isManualScan.description] + case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] + case .scanFailed(let dataBroker, let duration, let tries, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] + case .scanError(let dataBroker, let duration, let category, let details, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isImmediateOperation: isImmediateOperation.description] case .generateEmailHTTPErrorDaily(let statusCode, let environment, let wasOnWaitlist): return [Consts.environmentKey: environment, Consts.httpCode: String(statusCode), @@ -465,26 +422,20 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double func durationSinceStartTime() -> Double @@ -63,7 +63,7 @@ protocol StageDurationCalculator { } final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { - let isManualScan: Bool + let isImmediateOperation: Bool let handler: EventMapping let attemptId: UUID let dataBroker: String @@ -77,13 +77,13 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator startTime: Date = Date(), dataBroker: String, handler: EventMapping, - isManualScan: Bool = false) { + isImmediateOperation: Bool = false) { self.attemptId = attemptId self.startTime = startTime self.lastStateTime = startTime self.dataBroker = dataBroker self.handler = handler - self.isManualScan = isManualScan + self.isImmediateOperation = isImmediateOperation } /// Returned in milliseconds @@ -163,11 +163,11 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator } func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isImmediateOperation: isImmediateOperation)) } func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isImmediateOperation: isImmediateOperation)) } func fireScanError(error: Error) { @@ -205,7 +205,7 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator duration: durationSinceStartTime(), category: errorCategory.toString, details: error.localizedDescription, - isManualScan: isManualScan + isImmediateOperation: isImmediateOperation ) ) } diff --git a/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/BrowserWindowManager.swift similarity index 100% rename from DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/BrowserWindowManager.swift diff --git a/DuckDuckGoDBPBackgroundAgent/DBPMocks.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DBPMocks.swift similarity index 81% rename from DuckDuckGoDBPBackgroundAgent/DBPMocks.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DBPMocks.swift index 5e4e36ef2f..470d81cad6 100644 --- a/DuckDuckGoDBPBackgroundAgent/DBPMocks.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DBPMocks.swift @@ -26,7 +26,7 @@ import Common We ideally should refactor the privacy config out of the main app Into a local package, so that it can be be used here */ -final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { +public final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { var data: Data { let configString = """ @@ -47,13 +47,13 @@ final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { return data! } - var currentConfig: Data { + public var currentConfig: Data { data } - var updatesPublisher: AnyPublisher = .init(Just(())) + public var updatesPublisher: AnyPublisher = .init(Just(())) - var privacyConfig: BrowserServicesKit.PrivacyConfiguration { + public var privacyConfig: BrowserServicesKit.PrivacyConfiguration { guard let privacyConfigurationData = try? PrivacyConfigurationData(data: data) else { fatalError("Could not retrieve privacy configuration data") } @@ -63,11 +63,11 @@ final class PrivacyConfigurationManagingMock: PrivacyConfigurationManaging { return privacyConfig } - var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) - var toggleProtectionsCounter: ToggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in + public var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) + public var toggleProtectionsCounter: ToggleProtectionsCounter = ToggleProtectionsCounter(eventReporting: EventMapping { _, _, _, _ in }) - func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + public func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { .downloaded } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift similarity index 78% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift index 3281b66c37..7720725d85 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionProcessorConfiguration.swift +// DataBrokerExecutionConfig.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,19 +18,22 @@ import Foundation -struct DataBrokerProtectionProcessorConfiguration { - // Arbitrary numbers for now +public struct DataBrokerExecutionConfig { let intervalBetweenSameBrokerOperations: TimeInterval = 2 + private let concurrentOperationsDifferentBrokers: Int = 2 // https://app.asana.com/0/481882893211075/1206981742767469/f private let concurrentOperationsOnManualScans: Int = 6 - func concurrentOperationsFor(_ operation: OperationType) -> Int { switch operation { case .all, .optOut: return concurrentOperationsDifferentBrokers - case .manualScan: + case .scan: return concurrentOperationsOnManualScans } } + + let activitySchedulerTriggerInterval: TimeInterval = 20 * 60 // 20 minutes + let activitySchedulerIntervalTolerance: TimeInterval = 10 * 60 // 10 minutes + let activitySchedulerQOS: QualityOfService = .background } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift new file mode 100644 index 0000000000..aba746a849 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift @@ -0,0 +1,59 @@ +// +// DataBrokerOperationsCreator.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 Common +import Foundation + +protocol DataBrokerOperationsCreator { + func operations(forOperationType operationType: OperationType, + withPriorityDate priorityDate: Date?, + showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, + operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] +} + +final class DefaultDataBrokerOperationsCreator: DataBrokerOperationsCreator { + + func operations(forOperationType operationType: OperationType, + withPriorityDate priorityDate: Date?, + showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, + operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] { + + let brokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() + var operations: [DataBrokerOperation] = [] + var visitedDataBrokerIDs: Set = [] + + for queryData in brokerProfileQueryData { + guard let dataBrokerID = queryData.dataBroker.id else { continue } + + if !visitedDataBrokerIDs.contains(dataBrokerID) { + let collection = DataBrokerOperation(dataBrokerID: dataBrokerID, + operationType: operationType, + priorityDate: priorityDate, + showWebView: showWebView, + errorDelegate: errorDelegate, + operationDependencies: operationDependencies) + operations.append(collection) + visitedDataBrokerIDs.insert(dataBrokerID) + } + } + + return operations + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift new file mode 100644 index 0000000000..fae04e7e17 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -0,0 +1,285 @@ +// +// DataBrokerProtectionAgentManager.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 Common +import BrowserServicesKit +import PixelKit + +// This is to avoid exposing all the dependancies outside of the DBP package +public class DataBrokerProtectionAgentManagerProvider { + // swiftlint:disable:next function_body_length + public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging) -> DataBrokerProtectionAgentManager { + let pixelHandler = DataBrokerProtectionPixelsHandler() + + let executionConfig = DataBrokerExecutionConfig() + let activityScheduler = DefaultDataBrokerProtectionBackgroundActivityScheduler(config: executionConfig) + + let notificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) + let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned + let ipcServer = DefaultDataBrokerProtectionIPCServer(machServiceName: Bundle.main.bundleIdentifier!) + + let features = ContentScopeFeatureToggles(emailProtection: false, + emailProtectionIncontextSignup: false, + credentialsAutofill: false, + identitiesAutofill: false, + creditCardsAutofill: false, + credentialsSaving: false, + passwordGeneration: false, + inlineIconCredentials: false, + thirdPartyCredentialsProvider: false) + let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, + sessionKey: UUID().uuidString, + featureToggles: features) + + let fakeBroker = DataBrokerDebugFlagFakeBroker() + let dataManager = DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBroker) + + let operationQueue = OperationQueue() + let operationsBuilder = DefaultDataBrokerOperationsCreator() + let mismatchCalculator = DefaultMismatchCalculator(database: dataManager.database, + pixelHandler: pixelHandler) + + var brokerUpdater: DataBrokerProtectionBrokerUpdater? + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + brokerUpdater = DefaultDataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) + } + let queueManager = DefaultDataBrokerProtectionQueueManager(operationQueue: operationQueue, + operationsCreator: operationsBuilder, + mismatchCalculator: mismatchCalculator, + brokerUpdater: brokerUpdater, + pixelHandler: pixelHandler) + + let emailService = EmailService(authenticationManager: authenticationManager) + let captchaService = CaptchaService(authenticationManager: authenticationManager) + let runnerProvider = DataBrokerJobRunnerProvider(privacyConfigManager: privacyConfigurationManager, + contentScopeProperties: contentScopeProperties, + emailService: emailService, + captchaService: captchaService) + let operationDependencies = DefaultDataBrokerOperationDependencies( + database: dataManager.database, + config: executionConfig, + runnerProvider: runnerProvider, + notificationCenter: NotificationCenter.default, + pixelHandler: pixelHandler, + userNotificationService: notificationService) + + return DataBrokerProtectionAgentManager( + userNotificationService: notificationService, + activityScheduler: activityScheduler, + ipcServer: ipcServer, + queueManager: queueManager, + dataManager: dataManager, + operationDependencies: operationDependencies, + pixelHandler: pixelHandler) + } +} + +public final class DataBrokerProtectionAgentManager { + + private let userNotificationService: DataBrokerProtectionUserNotificationService + private var activityScheduler: DataBrokerProtectionBackgroundActivityScheduler + private var ipcServer: DataBrokerProtectionIPCServer + private let queueManager: DataBrokerProtectionQueueManager + private let dataManager: DataBrokerProtectionDataManaging + private let operationDependencies: DataBrokerOperationDependencies + private let pixelHandler: EventMapping + + // Used for debug functions only, so not injected + private lazy var browserWindowManager = BrowserWindowManager() + + private var didStartActivityScheduler = false + + init(userNotificationService: DataBrokerProtectionUserNotificationService, + activityScheduler: DataBrokerProtectionBackgroundActivityScheduler, + ipcServer: DataBrokerProtectionIPCServer, + queueManager: DataBrokerProtectionQueueManager, + dataManager: DataBrokerProtectionDataManaging, + operationDependencies: DataBrokerOperationDependencies, + pixelHandler: EventMapping) { + self.userNotificationService = userNotificationService + self.activityScheduler = activityScheduler + self.ipcServer = ipcServer + self.queueManager = queueManager + self.dataManager = dataManager + self.operationDependencies = operationDependencies + self.pixelHandler = pixelHandler + + self.activityScheduler.delegate = self + self.ipcServer.serverDelegate = self + self.ipcServer.activate() + } + + public func agentFinishedLaunching() { + + do { + // If there's no saved profile we don't need to start the scheduler + // Theoretically this should never happen, if there's no data, the agent shouldn't be running + guard (try dataManager.fetchProfile()) != nil else { + return + } + } catch { + os_log("Error during AgentManager.agentFinishedLaunching when trying to fetchProfile, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + return + } + + activityScheduler.startScheduler() + didStartActivityScheduler = true + queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionBackgroundActivitySchedulerDelegate { + + public func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtection.DataBrokerProtectionBackgroundActivityScheduler, completion: (() -> Void)?) { + queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { _ in + completion?() + } + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { + + public func profileSaved() { + let backgroundAgentInitialScanStartTime = Date() + + userNotificationService.requestNotificationPermission() + queueManager.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in + guard let self = self else { return } + + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + switch oneTimeError { + case DataBrokerProtectionQueueError.interrupted: + self.pixelHandler.fire(.ipcServerImmediateScansInterrupted) + os_log("Interrupted during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + default: + self.pixelHandler.fire(.ipcServerImmediateScansFinishedWithError(error: oneTimeError)) + os_log("Error during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + + if errors?.oneTimeError == nil { + self.pixelHandler.fire(.ipcServerImmediateScansFinishedWithoutError) + self.userNotificationService.sendFirstScanCompletedNotification() + } + + if let hasMatches = try? self.dataManager.hasMatches(), + hasMatches { + self.userNotificationService.scheduleCheckInNotificationIfPossible() + } + + fireImmediateScansCompletionPixel(startTime: backgroundAgentInitialScanStartTime) + } + } + + public func appLaunched() { + queueManager.startScheduledOperationsIfPermitted(showWebView: false, + operationDependencies: + operationDependencies) { [weak self] errors in + guard let self = self else { return } + + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + switch oneTimeError { + case DataBrokerProtectionQueueError.interrupted: + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansInterrupted) + os_log("Interrupted during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + case DataBrokerProtectionQueueError.cannotInterrupt: + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansBlocked) + os_log("Cannot interrupt during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted()") + default: + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansFinishedWithError(error: oneTimeError)) + os_log("Error during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + + if errors?.oneTimeError == nil { + self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansFinishedWithoutError) + } + } + } + + private func fireImmediateScansCompletionPixel(startTime: Date) { + do { + let profileQueries = try dataManager.profileQueriesCount() + let durationSinceStart = Date().timeIntervalSince(startTime) * 1000 + self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), + profileQueries: profileQueries)) + } catch { + os_log("Initial Scans Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) + } + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentDebugCommands { + public func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } + + public func startImmediateOperations(showWebView: Bool) { + queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: nil) + } + + public func startScheduledOperations(showWebView: Bool) { + queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: nil) + } + + public func runAllOptOuts(showWebView: Bool) { + queueManager.execute(.startOptOutOperations(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: nil)) + } + + public func getDebugMetadata() async -> DataBrokerProtection.DBPBackgroundAgentMetadata? { + + if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, + let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { + + return DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", + isAgentRunning: true, + agentSchedulerState: queueManager.debugRunningStatusString, + lastSchedulerSessionStartTimestamp: activityScheduler.lastTriggerTimestamp?.timeIntervalSince1970) + } else { + return DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", + isAgentRunning: true, + agentSchedulerState: queueManager.debugRunningStatusString, + lastSchedulerSessionStartTimestamp: activityScheduler.lastTriggerTimestamp?.timeIntervalSince1970) + } + } +} + +extension DataBrokerProtectionAgentManager: DataBrokerProtectionAppToAgentInterface { + +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift new file mode 100644 index 0000000000..8df894d8c1 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift @@ -0,0 +1,61 @@ +// +// DataBrokerProtectionBackgroundActivityScheduler.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 + +public protocol DataBrokerProtectionBackgroundActivityScheduler { + func startScheduler() + var delegate: DataBrokerProtectionBackgroundActivitySchedulerDelegate? { get set } + + var lastTriggerTimestamp: Date? { get } +} + +public protocol DataBrokerProtectionBackgroundActivitySchedulerDelegate: AnyObject { + func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtectionBackgroundActivityScheduler, completion: (() -> Void)?) +} + +public final class DefaultDataBrokerProtectionBackgroundActivityScheduler: DataBrokerProtectionBackgroundActivityScheduler { + + private let activity: NSBackgroundActivityScheduler + private let schedulerIdentifier = "com.duckduckgo.macos.browser.databroker-protection-scheduler" + + public weak var delegate: DataBrokerProtectionBackgroundActivitySchedulerDelegate? + public private(set) var lastTriggerTimestamp: Date? + + public init(config: DataBrokerExecutionConfig) { + activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) + activity.repeats = true + activity.interval = config.activitySchedulerTriggerInterval + activity.tolerance = config.activitySchedulerIntervalTolerance + activity.qualityOfService = config.activitySchedulerQOS + } + + public func startScheduler() { + activity.schedule { completion in + + self.lastTriggerTimestamp = Date() + os_log("Scheduler running...", log: .dataBrokerProtection) + self.delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) { + os_log("Scheduler finished...", log: .dataBrokerProtection) + completion(.finished) + } + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift deleted file mode 100644 index 41e02edaca..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionNoOpScheduler.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// DataBrokerProtectionNoOpScheduler.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 - -/// Convenience class for SwiftUI previews. -/// -/// Do not use this for any production code. -/// -final class DataBrokerProtectionNoOpScheduler: DataBrokerProtectionScheduler { - - private(set) public var status: DataBrokerProtectionSchedulerStatus = .idle - - private var internalStatusPublisher: Published = .init(initialValue: .idle) - - public var statusPublisher: Published.Publisher { - internalStatusPublisher.projectedValue - } - - func startScheduler(showWebView: Bool) { } - func stopScheduler() { } - func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func runQueuedOperations(showWebView: Bool, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } - func runAllOperations(showWebView: Bool) { } - func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift deleted file mode 100644 index c96a3c31d8..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// DataBrokerProtectionProcessor.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 Common -import BrowserServicesKit - -protocol OperationRunnerProvider { - func getOperationRunner() -> WebOperationRunner -} - -final class DataBrokerProtectionProcessor { - private let database: DataBrokerProtectionRepository - private let config: DataBrokerProtectionProcessorConfiguration - private let operationRunnerProvider: OperationRunnerProvider - private let notificationCenter: NotificationCenter - private let operationQueue: OperationQueue - private var pixelHandler: EventMapping - private let userNotificationService: DataBrokerProtectionUserNotificationService - private let engagementPixels: DataBrokerProtectionEngagementPixels - private let eventPixels: DataBrokerProtectionEventPixels - - init(database: DataBrokerProtectionRepository, - config: DataBrokerProtectionProcessorConfiguration = DataBrokerProtectionProcessorConfiguration(), - operationRunnerProvider: OperationRunnerProvider, - notificationCenter: NotificationCenter = NotificationCenter.default, - pixelHandler: EventMapping, - userNotificationService: DataBrokerProtectionUserNotificationService) { - - self.database = database - self.config = config - self.operationRunnerProvider = operationRunnerProvider - self.notificationCenter = notificationCenter - self.operationQueue = OperationQueue() - self.pixelHandler = pixelHandler - self.userNotificationService = userNotificationService - self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) - self.eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) - } - - // MARK: - Public functions - func startManualScans(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - - operationQueue.cancelAllOperations() - runOperations(operationType: .manualScan, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Scans done", log: .dataBrokerProtection) - completion?(errors) - self.calculateMisMatches() - } - } - - private func calculateMisMatches() { - let mismatchUseCase = MismatchCalculatorUseCase(database: database, pixelHandler: pixelHandler) - mismatchUseCase.calculateMismatches() - } - - func runAllOptOutOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - operationQueue.cancelAllOperations() - runOperations(operationType: .optOut, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Optouts done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func runQueuedOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { - runOperations(operationType: .all, - priorityDate: Date(), - showWebView: showWebView) { errors in - os_log("Queued operations done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func runAllOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { - runOperations(operationType: .all, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Queued operations done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func stopAllOperations() { - operationQueue.cancelAllOperations() - } - - // MARK: - Private functions - private func runOperations(operationType: OperationType, - priorityDate: Date?, - showWebView: Bool, - completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { - - self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsFor(operationType) - // Before running new operations we check if there is any updates to the broker files. - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { - let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) - brokerUpdater.checkForUpdatesInBrokerJSONFiles() - } - - // This will fire the DAU/WAU/MAU pixels, - engagementPixels.fireEngagementPixel() - // This will try to fire the event weekly report pixels - eventPixels.tryToFireWeeklyPixels() - - let dataBrokerOperationCollections: [DataBrokerOperationsCollection] - - do { - let brokersProfileData = try database.fetchAllBrokerProfileQueryData() - dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, - operationType: operationType, - priorityDate: priorityDate, - showWebView: showWebView) - - for collection in dataBrokerOperationCollections { - operationQueue.addOperation(collection) - } - } catch { - os_log("DataBrokerProtectionProcessor error: runOperations, error: %{public}@", log: .error, error.localizedDescription) - operationQueue.addBarrierBlock { - completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) - } - return - } - - operationQueue.addBarrierBlock { - let operationErrors = dataBrokerOperationCollections.compactMap { $0.error } - let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionSchedulerErrorCollection(operationErrors: operationErrors) : nil - completion(errorCollection) - } - } - - private func createDataBrokerOperationCollections(from brokerProfileQueriesData: [BrokerProfileQueryData], - operationType: OperationType, - priorityDate: Date?, - showWebView: Bool) -> [DataBrokerOperationsCollection] { - - var collections: [DataBrokerOperationsCollection] = [] - var visitedDataBrokerIDs: Set = [] - - for queryData in brokerProfileQueriesData { - guard let dataBrokerID = queryData.dataBroker.id else { continue } - - if !visitedDataBrokerIDs.contains(dataBrokerID) { - let collection = DataBrokerOperationsCollection(dataBrokerID: dataBrokerID, - database: database, - operationType: operationType, - intervalBetweenOperations: config.intervalBetweenSameBrokerOperations, - priorityDate: priorityDate, - notificationCenter: notificationCenter, - runner: operationRunnerProvider.getOperationRunner(), - pixelHandler: pixelHandler, - userNotificationService: userNotificationService, - showWebView: showWebView) - collection.errorDelegate = self - collections.append(collection) - - visitedDataBrokerIDs.insert(dataBrokerID) - } - } - - return collections - } - - deinit { - os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) - } -} - -extension DataBrokerProtectionProcessor: DataBrokerOperationsCollectionErrorDelegate { - - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, didErrorBeforeStartingBrokerOperations error: Error) { - - } - - func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, - didError error: Error, - whileRunningBrokerOperationData: BrokerOperationData, - withDataBrokerName dataBrokerName: String?) { - if let error = error as? DataBrokerProtectionError, - let dataBrokerName = dataBrokerName { - pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) - } else { - os_log("Cant handle error", log: .dataBrokerProtection) - } - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift new file mode 100644 index 0000000000..3567279d89 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -0,0 +1,269 @@ +// +// DataBrokerProtectionQueueManager.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 Common +import Foundation + +protocol DataBrokerProtectionOperationQueue { + var maxConcurrentOperationCount: Int { get set } + func cancelAllOperations() + func addOperation(_ op: Operation) + func addBarrierBlock(_ barrier: @escaping @Sendable () -> Void) +} + +extension OperationQueue: DataBrokerProtectionOperationQueue {} + +enum DataBrokerProtectionQueueMode { + case idle + case immediate(completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + case scheduled(completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + + var priorityDate: Date? { + switch self { + case .idle, .immediate: + return nil + case .scheduled: + return Date() + } + } + + func canBeInterruptedBy(newMode: DataBrokerProtectionQueueMode) -> Bool { + switch (self, newMode) { + case (.idle, _): + return true + case (_, .immediate): + return true + default: + return false + } + } +} + +enum DataBrokerProtectionQueueError: Error { + case cannotInterrupt + case interrupted +} + +enum DataBrokerProtectionQueueManagerDebugCommand { + case startOptOutOperations(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) +} + +protocol DataBrokerProtectionQueueManager { + + init(operationQueue: DataBrokerProtectionOperationQueue, + operationsCreator: DataBrokerOperationsCreator, + mismatchCalculator: MismatchCalculator, + brokerUpdater: DataBrokerProtectionBrokerUpdater?, + pixelHandler: EventMapping) + + func startImmediateOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + + func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) + var debugRunningStatusString: String { get } +} + +final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueManager { + + private var operationQueue: DataBrokerProtectionOperationQueue + private let operationsCreator: DataBrokerOperationsCreator + private let mismatchCalculator: MismatchCalculator + private let brokerUpdater: DataBrokerProtectionBrokerUpdater? + private let pixelHandler: EventMapping + + private var mode = DataBrokerProtectionQueueMode.idle + private var operationErrors: [Error] = [] + + var debugRunningStatusString: String { + switch mode { + case .idle: + return "idle" + case .immediate, + .scheduled: + return "running" + } + } + + init(operationQueue: DataBrokerProtectionOperationQueue, + operationsCreator: DataBrokerOperationsCreator, + mismatchCalculator: MismatchCalculator, + brokerUpdater: DataBrokerProtectionBrokerUpdater?, + pixelHandler: EventMapping) { + + self.operationQueue = operationQueue + self.operationsCreator = operationsCreator + self.mismatchCalculator = mismatchCalculator + self.brokerUpdater = brokerUpdater + self.pixelHandler = pixelHandler + } + + func startImmediateOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + let newMode = DataBrokerProtectionQueueMode.immediate(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: .scan, + showWebView: showWebView, + operationDependencies: operationDependencies) { [weak self] errors in + completion?(errors) + self?.mismatchCalculator.calculateMismatches() + } + } + + func startScheduledOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + let newMode = DataBrokerProtectionQueueMode.scheduled(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: .all, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) { + guard case .startOptOutOperations(let showWebView, + let operationDependencies, + let completion) = command else { return } + + addOperations(withType: .optOut, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } +} + +private extension DefaultDataBrokerProtectionQueueManager { + + func startOperationsIfPermitted(forNewMode newMode: DataBrokerProtectionQueueMode, + type: OperationType, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + guard mode.canBeInterruptedBy(newMode: newMode) else { + let error = DataBrokerProtectionQueueError.cannotInterrupt + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: error) + completion?(errorCollection) + return + } + + cancelCurrentModeAndResetIfNeeded() + + mode = newMode + + updateBrokerData() + + firePixels(operationDependencies: operationDependencies) + + addOperations(withType: type, + priorityDate: mode.priorityDate, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func cancelCurrentModeAndResetIfNeeded() { + switch mode { + case .immediate(let completion), .scheduled(let completion): + operationQueue.cancelAllOperations() + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: DataBrokerProtectionQueueError.interrupted, operationErrors: operationErrorsForCurrentOperations()) + completion?(errorCollection) + resetModeAndClearErrors() + default: + break + } + } + + func resetModeAndClearErrors() { + mode = .idle + operationErrors = [] + } + + func updateBrokerData() { + // Update broker files if applicable + brokerUpdater?.checkForUpdatesInBrokerJSONFiles() + } + + func addOperations(withType type: OperationType, + priorityDate: Date? = nil, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + operationQueue.maxConcurrentOperationCount = operationDependencies.config.concurrentOperationsFor(type) + + // Use builder to build operations + let operations: [DataBrokerOperation] + do { + operations = try operationsCreator.operations(forOperationType: type, + withPriorityDate: priorityDate, + showWebView: showWebView, + errorDelegate: self, + operationDependencies: operationDependencies) + + for collection in operations { + operationQueue.addOperation(collection) + } + } catch { + os_log("DataBrokerProtectionProcessor error: addOperations, error: %{public}@", log: .error, error.localizedDescription) + completion?(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) + return + } + + operationQueue.addBarrierBlock { [weak self] in + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: nil, operationErrors: self?.operationErrorsForCurrentOperations()) + completion?(errorCollection) + self?.resetModeAndClearErrors() + } + } + + func operationErrorsForCurrentOperations() -> [Error]? { + return operationErrors.count != 0 ? operationErrors : nil + } + + func firePixels(operationDependencies: DataBrokerOperationDependencies) { + let database = operationDependencies.database + let pixelHandler = operationDependencies.pixelHandler + + let engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) + let eventPixels = DataBrokerProtectionEventPixels(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() + } +} + +extension DefaultDataBrokerProtectionQueueManager: DataBrokerOperationErrorDelegate { + func dataBrokerOperationDidError(_ error: any Error, withBrokerName brokerName: String?) { + operationErrors.append(error) + + if let error = error as? DataBrokerProtectionError, let dataBrokerName = brokerName { + pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift deleted file mode 100644 index b0f0d324b6..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ /dev/null @@ -1,393 +0,0 @@ -// -// DataBrokerProtectionScheduler.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 Common -import BrowserServicesKit -import Combine - -public enum DataBrokerProtectionSchedulerStatus: Codable { - case stopped - case idle - case running -} - -public enum DataBrokerProtectionSchedulerError: Error { - case loginItemDoesNotHaveNecessaryPermissions - case appInWrongDirectory - case operationsInterrupted -} - -@objc -public class DataBrokerProtectionSchedulerErrorCollection: NSObject, NSSecureCoding { - /* - This needs to be an NSObject (rather than a struct) so it can be represented in Objective C - and confrom to NSSecureCoding for the IPC layer. - */ - - private enum NSSecureCodingKeys { - static let oneTimeError = "oneTimeError" - static let operationErrors = "operationErrors" - } - - public let oneTimeError: Error? - public let operationErrors: [Error]? - - public init(oneTimeError: Error? = nil, operationErrors: [Error]? = nil) { - self.oneTimeError = oneTimeError - self.operationErrors = operationErrors - super.init() - } - - // MARK: - NSSecureCoding - - public static var supportsSecureCoding: Bool { - return true - } - - public func encode(with coder: NSCoder) { - coder.encode(oneTimeError, forKey: NSSecureCodingKeys.oneTimeError) - coder.encode(operationErrors, forKey: NSSecureCodingKeys.operationErrors) - } - - public required init?(coder: NSCoder) { - oneTimeError = coder.decodeObject(of: NSError.self, forKey: NSSecureCodingKeys.oneTimeError) - operationErrors = coder.decodeArrayOfObjects(ofClass: NSError.self, forKey: NSSecureCodingKeys.operationErrors) - } -} - -public protocol DataBrokerProtectionScheduler { - - var status: DataBrokerProtectionSchedulerStatus { get } - var statusPublisher: Published.Publisher { get } - - func startScheduler(showWebView: Bool) - func stopScheduler() - - func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func startManualScan(showWebView: Bool, startTime: Date, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) - func runAllOperations(showWebView: Bool) - - /// Debug operations - - func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) -} - -extension DataBrokerProtectionScheduler { - public func startScheduler() { - startScheduler(showWebView: false) - } - - public func runAllOperations() { - runAllOperations(showWebView: false) - } - - public func startManualScan(startTime: Date) { - startManualScan(showWebView: false, startTime: startTime, completion: nil) - } -} - -public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionScheduler { - - private enum SchedulerCycle { - // Arbitrary numbers for now - - static let interval: TimeInterval = 40 * 60 // 40 minutes - static let tolerance: TimeInterval = 20 * 60 // 20 minutes - } - - private enum DataBrokerProtectionCurrentOperation { - case idle - case queued - case manualScan - case optOutAll - case all - } - - private let privacyConfigManager: PrivacyConfigurationManaging - private let contentScopeProperties: ContentScopeProperties - private let dataManager: DataBrokerProtectionDataManager - private let activity: NSBackgroundActivityScheduler - private let pixelHandler: EventMapping - private let schedulerIdentifier = "com.duckduckgo.macos.browser.databroker-protection-scheduler" - private let notificationCenter: NotificationCenter - private let emailService: EmailServiceProtocol - private let captchaService: CaptchaServiceProtocol - private let userNotificationService: DataBrokerProtectionUserNotificationService - private var currentOperation: DataBrokerProtectionCurrentOperation = .idle - private let authenticationManager: DataBrokerProtectionAuthenticationManaging - - /// Ensures that only one scheduler operation is executed at the same time. - /// - private let schedulerDispatchQueue = DispatchQueue(label: "schedulerDispatchQueue", qos: .background) - - @Published public var status: DataBrokerProtectionSchedulerStatus = .stopped - - public var statusPublisher: Published.Publisher { $status } - - private var lastSchedulerSessionStartTimestamp: Date? - - private lazy var dataBrokerProcessor: DataBrokerProtectionProcessor = { - - let runnerProvider = DataBrokerOperationRunnerProvider(privacyConfigManager: privacyConfigManager, - contentScopeProperties: contentScopeProperties, - emailService: emailService, - captchaService: captchaService) - - return DataBrokerProtectionProcessor(database: dataManager.database, - operationRunnerProvider: runnerProvider, - notificationCenter: notificationCenter, - pixelHandler: pixelHandler, - userNotificationService: userNotificationService) - }() - - public init(privacyConfigManager: PrivacyConfigurationManaging, - contentScopeProperties: ContentScopeProperties, - dataManager: DataBrokerProtectionDataManager, - notificationCenter: NotificationCenter = NotificationCenter.default, - pixelHandler: EventMapping, - authenticationManager: DataBrokerProtectionAuthenticationManaging, - userNotificationService: DataBrokerProtectionUserNotificationService - ) { - activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) - activity.repeats = true - activity.interval = SchedulerCycle.interval - activity.tolerance = SchedulerCycle.tolerance - activity.qualityOfService = QualityOfService.default - - self.dataManager = dataManager - self.privacyConfigManager = privacyConfigManager - self.contentScopeProperties = contentScopeProperties - self.pixelHandler = pixelHandler - self.notificationCenter = notificationCenter - self.userNotificationService = userNotificationService - self.authenticationManager = authenticationManager - - self.emailService = EmailService(authenticationManager: authenticationManager) - self.captchaService = CaptchaService(authenticationManager: authenticationManager) - } - - public func startScheduler(showWebView: Bool = false) { - guard status == .stopped else { - os_log("Trying to start scheduler when it's already running, returning...", log: .dataBrokerProtection) - return - } - - status = .idle - activity.schedule { completion in - guard self.status != .stopped else { - os_log("Activity started when scheduler was already running, returning...", log: .dataBrokerProtection) - completion(.finished) - return - } - - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - completion(.finished) - return - } - self.lastSchedulerSessionStartTimestamp = Date() - self.status = .running - os_log("Scheduler running...", log: .dataBrokerProtection) - self.currentOperation = .queued - self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during startScheduler in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduler")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during startScheduler in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.status = .idle - self?.currentOperation = .idle - completion(.finished) - } - } - } - - public func stopScheduler() { - os_log("Stopping scheduler...", log: .dataBrokerProtection) - activity.invalidate() - status = .stopped - dataBrokerProcessor.stopAllOperations() - } - - public func runAllOperations(showWebView: Bool = false) { - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Running all operations...", log: .dataBrokerProtection) - self.currentOperation = .all - self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runAllOperations")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.currentOperation = .idle - } - } - - public func runQueuedOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Running queued operations...", log: .dataBrokerProtection) - self.currentOperation = .queued - dataBrokerProcessor.runQueuedOperations(showWebView: showWebView, - completion: { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runQueuedOperations")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - completion?(errors) - self?.currentOperation = .idle - }) - - } - - public func startManualScan(showWebView: Bool = false, - startTime: Date, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { - pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) - let backgroundAgentManualScanStartTime = Date() - stopScheduler() - - userNotificationService.requestNotificationPermission() - self.currentOperation = .manualScan - os_log("Scanning all brokers...", log: .dataBrokerProtection) - dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in - guard let self = self else { return } - - self.startScheduler(showWebView: showWebView) - - if errors?.oneTimeError == nil { - self.userNotificationService.sendFirstScanCompletedNotification() - } - - if let hasMatches = try? self.dataManager.hasMatches(), - hasMatches { - self.userNotificationService.scheduleCheckInNotificationIfPossible() - } - - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - switch oneTimeError { - case DataBrokerProtectionSchedulerError.operationsInterrupted: - os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - default: - os_log("Error during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startManualScan")) - } - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self.currentOperation = .idle - fireManualScanCompletionPixel(startTime: backgroundAgentManualScanStartTime) - completion?(errors) - } - } - - private func fireManualScanCompletionPixel(startTime: Date) { - do { - let profileQueries = try dataManager.profileQueriesCount() - let durationSinceStart = Date().timeIntervalSince(startTime) * 1000 - self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), - profileQueries: profileQueries)) - } catch { - os_log("Manual Scan Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) - } - } - - public func optOutAllBrokers(showWebView: Bool = false, - completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { - - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Opting out all brokers...", log: .dataBrokerProtection) - self.currentOperation = .optOutAll - self.dataBrokerProcessor.runAllOptOutOperations(showWebView: showWebView, - completion: { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.optOutAllBrokers")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.currentOperation = .idle - completion?(errors) - }) - } - - public func getDebugMetadata(completion: (DBPBackgroundAgentMetadata?) -> Void) { - if let backgroundAgentVersion = Bundle.main.releaseVersionNumber, let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String { - completion(DBPBackgroundAgentMetadata(backgroundAgentVersion: backgroundAgentVersion + " (build: \(buildNumber))", - isAgentRunning: status == .running, - agentSchedulerState: status.toString, - lastSchedulerSessionStartTimestamp: lastSchedulerSessionStartTimestamp?.timeIntervalSince1970)) - } else { - completion(DBPBackgroundAgentMetadata(backgroundAgentVersion: "ERROR: Error fetching background agent version", - isAgentRunning: status == .running, - agentSchedulerState: status.toString, - lastSchedulerSessionStartTimestamp: lastSchedulerSessionStartTimestamp?.timeIntervalSince1970)) - } - } -} - -extension DataBrokerProtectionSchedulerStatus { - var toString: String { - switch self { - case .idle: - return "idle" - case .running: - return "running" - case .stopped: - return "stopped" - } - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift index c854055aab..3001b6624b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionSecureVault.swift @@ -56,16 +56,16 @@ protocol DataBrokerProtectionSecureVault: SecureVault { func save(brokerId: Int64, profileQueryId: Int64, lastRunDate: Date?, preferredRunDate: Date?) throws func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws - func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanOperationData? - func fetchAllScans() throws -> [ScanOperationData] + func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanJobData? + func fetchAllScans() throws -> [ScanJobData] func save(brokerId: Int64, profileQueryId: Int64, extractedProfile: ExtractedProfile, lastRunDate: Date?, preferredRunDate: Date?) throws func save(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64, lastRunDate: Date?, preferredRunDate: Date?) throws func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws - func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutOperationData? - func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutOperationData] - func fetchAllOptOuts() throws -> [OptOutOperationData] + func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutJobData? + func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutJobData] + func fetchAllOptOuts() throws -> [OptOutJobData] func save(historyEvent: HistoryEvent, brokerId: Int64, profileQueryId: Int64) throws func save(historyEvent: HistoryEvent, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws @@ -206,7 +206,7 @@ final class DefaultDataBrokerProtectionSecureVault ScanOperationData? { + func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanJobData? { if let scanDB = try self.providers.database.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) { let scanEvents = try self.providers.database.fetchScanEvents(brokerId: brokerId, profileQueryId: profileQueryId) let mapper = MapperToModel(mechanism: l2Decrypt(data:)) @@ -217,9 +217,9 @@ final class DefaultDataBrokerProtectionSecureVault [ScanOperationData] { + func fetchAllScans() throws -> [ScanJobData] { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) - var scans = [ScanOperationData]() + var scans = [ScanJobData]() let scansDB = try self.providers.database.fetchAllScans() for scan in scansDB { @@ -260,7 +260,7 @@ final class DefaultDataBrokerProtectionSecureVault OptOutOperationData? { + func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutJobData? { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) if let optOutResult = try self.providers.database.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) { let optOutEvents = try self.providers.database.fetchOptOutEvents(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) @@ -270,7 +270,7 @@ final class DefaultDataBrokerProtectionSecureVault [OptOutOperationData] { + func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutJobData] { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) return try self.providers.database.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId).map { @@ -283,7 +283,7 @@ final class DefaultDataBrokerProtectionSecureVault [OptOutOperationData] { + func fetchAllOptOuts() throws -> [OptOutJobData] { let mapper = MapperToModel(mechanism: l2Decrypt(data:)) return try self.providers.database.fetchAllOptOuts().map { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 81cf02f70d..37bae0a2f5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -199,7 +199,7 @@ struct MapperToModel { ) } - func mapToModel(_ scanDB: ScanDB, events: [ScanHistoryEventDB]) throws -> ScanOperationData { + func mapToModel(_ scanDB: ScanDB, events: [ScanHistoryEventDB]) throws -> ScanJobData { .init( brokerId: scanDB.brokerId, profileQueryId: scanDB.profileQueryId, @@ -209,7 +209,7 @@ struct MapperToModel { ) } - func mapToModel(_ optOutDB: OptOutDB, extractedProfileDB: ExtractedProfileDB, events: [OptOutHistoryEventDB]) throws -> OptOutOperationData { + func mapToModel(_ optOutDB: OptOutDB, extractedProfileDB: ExtractedProfileDB, events: [OptOutHistoryEventDB]) throws -> OptOutJobData { .init( brokerId: optOutDB.brokerId, profileQueryId: optOutDB.profileQueryId, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index 4f26b33b8e..95007d8aab 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -26,7 +26,6 @@ import Combine final public class DataBrokerProtectionViewController: NSViewController { private let dataManager: DataBrokerProtectionDataManaging - private let scheduler: DataBrokerProtectionScheduler private var webView: WKWebView? private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable @@ -37,20 +36,19 @@ final public class DataBrokerProtectionViewController: NSViewController { private let openURLHandler: (URL?) -> Void private var reloadObserver: NSObjectProtocol? - public init(scheduler: DataBrokerProtectionScheduler, + public init(agentInterface: DataBrokerProtectionAppToAgentInterface, dataManager: DataBrokerProtectionDataManaging, privacyConfig: PrivacyConfigurationManaging? = nil, prefs: ContentScopeProperties? = nil, webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable, openURLHandler: @escaping (URL?) -> Void) { - self.scheduler = scheduler self.dataManager = dataManager self.openURLHandler = openURLHandler self.webUISettings = webUISettings self.pixelHandler = DataBrokerProtectionPixelsHandler() self.webUIPixel = DataBrokerProtectionWebUIPixels(pixelHandler: pixelHandler) self.webUIViewModel = DBPUIViewModel(dataManager: dataManager, - scheduler: scheduler, + agentInterface: agentInterface, webUISettings: webUISettings, privacyConfig: privacyConfig, prefs: prefs, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index ee89f641f2..27ef5477a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -105,9 +105,9 @@ struct MapperToUI { brokerProfileQueryData.forEach { let dataBroker = $0.dataBroker - let scanOperation = $0.scanOperationData - for optOutOperation in $0.optOutOperationsData { - let extractedProfile = optOutOperation.extractedProfile + let scanJob = $0.scanJobData + for optOutJob in $0.optOutJobData { + let extractedProfile = optOutJob.extractedProfile let profileMatch = mapToUI(dataBroker, extractedProfile: extractedProfile) if extractedProfile.removedDate == nil { @@ -116,7 +116,7 @@ struct MapperToUI { removedProfiles.append(profileMatch) } - if let closestMatchesFoundEvent = scanOperation.closestMatchesFoundEvent() { + if let closestMatchesFoundEvent = scanJob.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) @@ -160,8 +160,8 @@ struct MapperToUI { format: String = "dd/MM/yyyy") -> DBPUIScanDate { let eightDaysBeforeToday = currentDate.addingTimeInterval(-8 * 24 * 60 * 60) let scansInTheLastEightDays = brokerProfileQueryData - .filter { $0.scanOperationData.lastRunDate != nil && $0.scanOperationData.lastRunDate! <= currentDate && $0.scanOperationData.lastRunDate! > eightDaysBeforeToday } - .sorted { $0.scanOperationData.lastRunDate! < $1.scanOperationData.lastRunDate! } + .filter { $0.scanJobData.lastRunDate != nil && $0.scanJobData.lastRunDate! <= currentDate && $0.scanJobData.lastRunDate! > eightDaysBeforeToday } + .sorted { $0.scanJobData.lastRunDate! < $1.scanJobData.lastRunDate! } .reduce(into: [BrokerProfileQueryData]()) { result, element in if !result.contains(where: { $0.dataBroker.url == element.dataBroker.url }) { result.append(element) @@ -169,10 +169,10 @@ struct MapperToUI { } .flatMap { var brokers = [DBPUIDataBroker]() - brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanOperationData.lastRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970)) - for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanOperationData.lastRunDate! { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanOperationData.lastRunDate!.timeIntervalSince1970)) + for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanJobData.lastRunDate! { + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970)) } return brokers @@ -190,8 +190,8 @@ struct MapperToUI { format: String = "dd/MM/yyyy") -> DBPUIScanDate { let eightDaysAfterToday = currentDate.addingTimeInterval(8 * 24 * 60 * 60) let scansHappeningInTheNextEightDays = brokerProfileQueryData - .filter { $0.scanOperationData.preferredRunDate != nil && $0.scanOperationData.preferredRunDate! > currentDate && $0.scanOperationData.preferredRunDate! < eightDaysAfterToday } - .sorted { $0.scanOperationData.preferredRunDate! < $1.scanOperationData.preferredRunDate! } + .filter { $0.scanJobData.preferredRunDate != nil && $0.scanJobData.preferredRunDate! > currentDate && $0.scanJobData.preferredRunDate! < eightDaysAfterToday } + .sorted { $0.scanJobData.preferredRunDate! < $1.scanJobData.preferredRunDate! } .reduce(into: [BrokerProfileQueryData]()) { result, element in if !result.contains(where: { $0.dataBroker.url == element.dataBroker.url }) { result.append(element) @@ -199,15 +199,15 @@ struct MapperToUI { } .flatMap { var brokers = [DBPUIDataBroker]() - brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanOperationData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) for mirrorSite in $0.dataBroker.mirrorSites { if let removedDate = mirrorSite.removedAt { - if removedDate > $0.scanOperationData.preferredRunDate! { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanOperationData.preferredRunDate!.timeIntervalSince1970)) + if removedDate > $0.scanJobData.preferredRunDate! { + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) } } else { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanOperationData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) } } @@ -313,8 +313,8 @@ fileprivate extension BrokerProfileQueryData { } var sitesScanned: [String] { - if scanOperationData.lastRunDate != nil { - let scanEvents = scanOperationData.scanStartedEvents() + if scanJobData.lastRunDate != nil { + let scanEvents = scanJobData.scanStartedEvents() var sitesScanned = [dataBroker.name] for mirrorSite in dataBroker.mirrorSites { @@ -344,7 +344,7 @@ fileprivate extension Array where Element == BrokerProfileQueryData { var currentScans: Int { guard let broker = self.first?.dataBroker else { return 0 } - let didAllQueriesFinished = allSatisfy { $0.scanOperationData.lastRunDate != nil } + let didAllQueriesFinished = allSatisfy { $0.scanJobData.lastRunDate != nil } if !didAllQueriesFinished { return 0 @@ -353,7 +353,7 @@ fileprivate extension Array where Element == BrokerProfileQueryData { } } - var lastOperation: BrokerOperationData? { + var lastOperation: BrokerJobData? { let allOperations = flatMap { $0.operationsData } let lastOperation = allOperations.sorted(by: { if let date1 = $0.lastRunDate, let date2 = $1.lastRunDate { @@ -378,7 +378,7 @@ fileprivate extension Array where Element == BrokerProfileQueryData { return lastError } - var lastStartedOperation: BrokerOperationData? { + var lastStartedOperation: BrokerJobData? { let allOperations = flatMap { $0.operationsData } return allOperations.sorted(by: { @@ -393,9 +393,9 @@ fileprivate extension Array where Element == BrokerProfileQueryData { } } -fileprivate extension BrokerOperationData { +fileprivate extension BrokerJobData { var toString: String { - if (self as? OptOutOperationData) != nil { + if (self as? OptOutJobData) != nil { return "optOut" } else { return "scan" diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift similarity index 83% rename from LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift rename to LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift index fe8eec11c1..cb54af211b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionProcessorConfigurationTests.swift +// DataBrokerExecutionConfigTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,12 +20,12 @@ import XCTest import Foundation @testable import DataBrokerProtection -final class DataBrokerProtectionProcessorConfigurationTests: XCTestCase { +final class DataBrokerExecutionConfigTests: XCTestCase { - private let sut = DataBrokerProtectionProcessorConfiguration() + private let sut = DataBrokerExecutionConfig() func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { - let value = sut.concurrentOperationsFor(.manualScan) + let value = sut.concurrentOperationsFor(.scan) let expectedValue = 6 XCTAssertEqual(value, expectedValue) } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index 2b46c2b05f..4f0ea9f97a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -40,7 +40,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let emailConfirmationAction = EmailConfirmationAction(id: "", actionType: .emailConfirmation, pollingTime: 1, dataSource: nil) let step = Step(type: .optOut, actions: [emailConfirmationAction]) let extractedProfile = ExtractedProfile(email: "test@duck.com") - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -66,7 +66,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let emailConfirmationAction = EmailConfirmationAction(id: "", actionType: .emailConfirmation, pollingTime: 1, dataSource: nil) let step = Step(type: .optOut, actions: [emailConfirmationAction]) let noEmailExtractedProfile = ExtractedProfile() - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -99,7 +99,7 @@ final class DataBrokerOperationActionTests: XCTestCase { let step = Step(type: .optOut, actions: [emailConfirmationAction]) let extractedProfile = ExtractedProfile(email: "test@duck.com") emailService.shouldThrow = true - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -130,7 +130,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 step = Step(type: .optOut, actions: [fillFormAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -154,7 +154,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 step = Step(type: .optOut, actions: [fillFormAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -183,7 +183,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenClickActionSucceeds_thenWeWaitForWebViewToLoad() async { - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -205,7 +205,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenAnActionThatIsNotClickSucceeds_thenWeDoNotWaitForWebViewToLoad() async { - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -228,7 +228,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenSolveCaptchaActionIsRun_thenCaptchaIsResolved() async { let solveCaptchaAction = SolveCaptchaAction(id: "1", actionType: .solveCaptcha, selector: "g-captcha", dataSource: nil) let step = Step(type: .optOut, actions: [solveCaptchaAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -252,7 +252,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenSolveCapchaActionFailsToSubmitDataToTheBackend_thenOperationFails() async { let solveCaptchaAction = SolveCaptchaAction(id: "1", actionType: .solveCaptcha, selector: "g-captcha", dataSource: nil) let step = Step(type: .optOut, actions: [solveCaptchaAction]) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(with: [step]), @@ -283,7 +283,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenCaptchaInformationIsReturned_thenWeSubmitItTotTheBackend() async { let getCaptchaResponse = GetCaptchaInfoResponse(siteKey: "siteKey", url: "url", type: "recaptcha") let step = Step(type: .optOut, actions: [Action]()) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -307,7 +307,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenCaptchaInformationFailsToBeSubmitted_thenTheOperationFails() async { let getCaptchaResponse = GetCaptchaInfoResponse(siteKey: "siteKey", url: "url", type: "recaptcha") let step = Step(type: .optOut, actions: [Action]()) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -332,7 +332,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenRunningActionWithoutExtractedProfile_thenExecuteIsCalledWithProfileData() async { let expectationAction = ExpectationAction(id: "1", actionType: .expectation, expectations: [Item](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -352,7 +352,7 @@ final class DataBrokerOperationActionTests: XCTestCase { } func testWhenLoadURLDelegateIsCalled_thenCorrectMethodIsExecutedOnWebViewHandler() async { - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -374,7 +374,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenGetCaptchaActionRuns_thenStageIsSetToCaptchaParse() async { let mockStageCalculator = MockStageDurationCalculator() let captchaAction = GetCaptchaInfoAction(id: "1", actionType: .getCaptchaInfo, selector: "captcha", dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -395,7 +395,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenClickActionRuns_thenStageIsSetToSubmit() async { let mockStageCalculator = MockStageDurationCalculator() let clickAction = ClickAction(id: "1", actionType: .click, elements: [PageElement](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -416,7 +416,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenExpectationActionRuns_thenStageIsSetToSubmit() async { let mockStageCalculator = MockStageDurationCalculator() let expectationAction = ExpectationAction(id: "1", actionType: .expectation, expectations: [Item](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -437,7 +437,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenFillFormActionRuns_thenStageIsSetToFillForm() async { let mockStageCalculator = MockStageDurationCalculator() let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "", elements: [PageElement](), dataSource: nil) - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(), @@ -457,7 +457,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenLoadUrlOnSpokeo_thenSetCookiesIsCalled() async { let mockCookieHandler = MockCookieHandler() - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(url: "spokeo.com"), @@ -480,7 +480,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenLoadUrlOnOtherBroker_thenSetCookiesIsNotCalled() async { let mockCookieHandler = MockCookieHandler() - let sut = OptOutOperation( + let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock, query: BrokerProfileQueryData.mock(url: "verecor.com"), diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift new file mode 100644 index 0000000000..a19e16863a --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -0,0 +1,82 @@ +// +// DataBrokerOperationsCreatorTests.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. +// + +@testable import DataBrokerProtection +import XCTest + +final class DataBrokerOperationsCreatorTests: XCTestCase { + + private let sut: DataBrokerOperationsCreator = DefaultDataBrokerOperationsCreator() + + // Dependencies + private var mockDatabase: MockDatabase! + private var mockSchedulerConfig = DataBrokerExecutionConfig() + private var mockRunnerProvider: MockRunnerProvider! + private var mockPixelHandler: MockPixelHandler! + private var mockUserNotificationService: MockUserNotificationService! + var mockDependencies: DefaultDataBrokerOperationDependencies! + + override func setUpWithError() throws { + mockDatabase = MockDatabase() + mockRunnerProvider = MockRunnerProvider() + mockPixelHandler = MockPixelHandler() + mockUserNotificationService = MockUserNotificationService() + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + config: mockSchedulerConfig, + runnerProvider: mockRunnerProvider, + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockUserNotificationService) + } + + func testWhenBuildOperations_andBrokerQueryDataHasDuplicateBrokers_thenDuplicatesAreIgnored() throws { + // Given + let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ + .init(dataBroker: .mock(withId: 1), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 1)), + .init(dataBroker: .mock(withId: 1), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 1)), + .init(dataBroker: .mock(withId: 2), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + .init(dataBroker: .mock(withId: 2), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + .init(dataBroker: .mock(withId: 2), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + .init(dataBroker: .mock(withId: 3), + profileQuery: .mock, + scanJobData: .mock(withBrokerId: 2)), + ] + mockDatabase.brokerProfileQueryDataToReturn = dataBrokerProfileQueries + + // When + let result = try! sut.operations(forOperationType: .scan, + withPriorityDate: Date(), + showWebView: false, + errorDelegate: MockDataBrokerOperationErrorDelegate(), + operationDependencies: mockDependencies) + + // Then + XCTAssert(result.count == 3) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 19d92e74d6..bd3aeafb71 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -26,13 +26,13 @@ import PixelKit final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let sut = DataBrokerProfileQueryOperationManager() - let mockWebOperationRunner = MockWebOperationRunner() + let mockWebOperationRunner = MockWebJobRunner() let mockDatabase = MockDatabase() - let mockUserNotification = MockUserNotification() + let mockUserNotificationService = MockUserNotificationService() override func tearDown() { mockWebOperationRunner.clear() - mockUserNotification.reset() + mockUserNotificationService.reset() } // MARK: - Notification tests @@ -50,16 +50,16 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) let extractedProfileSaved = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc") - let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved)] + let optOutData = [OptOutJobData.mock(with: extractedProfileSaved)] let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, - scanOperationData: mockScanOperation, - optOutOperationsData: optOutData) + scanJobData: mockScanOperation, + optOutJobData: optOutData) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] mockWebOperationRunner.scanResults = [] @@ -68,17 +68,17 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: extractedProfileSaved)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: extractedProfileSaved)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: mockUserNotification, + userNotificationService: mockUserNotificationService, shouldRunNextStep: { true } ) - XCTAssertTrue(mockUserNotification.allInfoRemovedWasSent) - XCTAssertFalse(mockUserNotification.firstRemovedNotificationWasSent) + XCTAssertTrue(mockUserNotificationService.allInfoRemovedWasSent) + XCTAssertFalse(mockUserNotificationService.firstRemovedNotificationWasSent) } catch { XCTFail("Should not throw") } @@ -98,18 +98,18 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc", identifier: "abc") let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz", identifier: "zxz") - let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + let optOutData = [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, - scanOperationData: mockScanOperation, - optOutOperationsData: optOutData) + scanJobData: mockScanOperation, + optOutJobData: optOutData) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] mockWebOperationRunner.scanResults = [extractedProfileSaved1] @@ -118,18 +118,18 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: mockUserNotification, + userNotificationService: mockUserNotificationService, shouldRunNextStep: { true } ) - XCTAssertFalse(mockUserNotification.allInfoRemovedWasSent) - XCTAssertTrue(mockUserNotification.firstRemovedNotificationWasSent) + XCTAssertFalse(mockUserNotificationService.allInfoRemovedWasSent) + XCTAssertTrue(mockUserNotificationService.firstRemovedNotificationWasSent) } catch { XCTFail("Should not throw") } @@ -149,18 +149,18 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc") let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz") - let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + let optOutData = [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, - scanOperationData: mockScanOperation, - optOutOperationsData: optOutData) + scanJobData: mockScanOperation, + optOutJobData: optOutData) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] mockWebOperationRunner.scanResults = [extractedProfileSaved1, extractedProfileSaved2] @@ -169,18 +169,18 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: extractedProfileSaved1), - OptOutOperationData.mock(with: extractedProfileSaved2)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: extractedProfileSaved1), + OptOutJobData.mock(with: extractedProfileSaved2)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: mockUserNotification, + userNotificationService: mockUserNotificationService, shouldRunNextStep: { true } ) - XCTAssertFalse(mockUserNotification.allInfoRemovedWasSent) - XCTAssertFalse(mockUserNotification.firstRemovedNotificationWasSent) + XCTAssertFalse(mockUserNotificationService.allInfoRemovedWasSent) + XCTAssertFalse(mockUserNotificationService.firstRemovedNotificationWasSent) } catch { XCTFail("Should not throw") } @@ -195,12 +195,12 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mockWithoutId, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -217,12 +217,12 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithoutId, profileQuery: .mock, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id for broker") @@ -238,12 +238,12 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertEqual(mockDatabase.eventsAdded.first?.type, .scanStarted) @@ -259,12 +259,12 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock + scanJobData: .mock ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .noMatchFound })) @@ -282,13 +282,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasUpdateRemoveDateCalled) @@ -308,13 +308,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasUpdateRemoveDateCalled) @@ -332,13 +332,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasUpdateRemoveDateCalled) @@ -356,13 +356,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasSaveOptOutOperationCalled) @@ -379,13 +379,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutConfirmed })) @@ -404,13 +404,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutConfirmed })) @@ -430,13 +430,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Should throw!") @@ -459,13 +459,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithoutId, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -483,13 +483,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mockWithoutId, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -507,13 +507,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutId)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutId)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -531,13 +531,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasDatabaseCalled) @@ -555,13 +555,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithParentOptOut, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasDatabaseCalled) @@ -579,13 +579,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutStarted })) @@ -602,13 +602,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutRequested })) @@ -626,13 +626,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("Should throw!") @@ -652,13 +652,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mockWithParentOptOut, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) @@ -685,13 +685,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) @@ -724,13 +724,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) if let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last { @@ -763,13 +763,13 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { brokerProfileQueryData: .init( dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mock, - optOutOperationsData: [OptOutOperationData.mock(with: .mockWithoutRemovedDate)] + scanJobData: .mock, + optOutJobData: [OptOutJobData.mock(with: .mockWithoutRemovedDate)] ), database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), - userNotificationService: MockUserNotification(), + userNotificationService: MockUserNotificationService(), shouldRunNextStep: { true } ) XCTFail("The code above should throw") @@ -894,9 +894,9 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) - let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanOperationData: mockScanOperation) + let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanJobData: mockScanOperation) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] try sut.updateOperationDataDates(origin: .optOut, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId, schedulingConfig: config, database: mockDatabase) @@ -919,9 +919,9 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] - let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) + let mockScanOperation = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) - let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanOperationData: mockScanOperation) + let mockBrokerProfileQuery = BrokerProfileQueryData(dataBroker: mockDataBroker, profileQuery: mockProfileQuery, scanJobData: mockScanOperation) mockDatabase.brokerProfileQueryDataToReturn = [mockBrokerProfileQuery] try sut.updateOperationDataDates(origin: .scan, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId, schedulingConfig: config, database: mockDatabase) @@ -932,7 +932,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { } } -final class MockWebOperationRunner: WebOperationRunner { +final class MockWebJobRunner: WebJobRunner { var shouldScanThrow = false var shouldOptOutThrow = false var scanResults = [ExtractedProfile]() @@ -966,24 +966,9 @@ final class MockWebOperationRunner: WebOperationRunner { } } -extension ScanOperationData { +extension OptOutJobData { - static var mock: ScanOperationData { - .init( - brokerId: 1, - profileQueryId: 1, - historyEvents: [HistoryEvent]() - ) - } - - static func mockWith(historyEvents: [HistoryEvent]) -> ScanOperationData { - ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) - } -} - -extension OptOutOperationData { - - static func mock(with extractedProfile: ExtractedProfile) -> OptOutOperationData { + static func mock(with extractedProfile: ExtractedProfile) -> OptOutJobData { .init(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent](), extractedProfile: extractedProfile) } } @@ -1055,17 +1040,6 @@ extension DataBroker { } } -extension ProfileQuery { - - static var mock: ProfileQuery { - .init(id: 1, firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) - } - - static var mockWithoutId: ProfileQuery { - .init(firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) - } -} - extension ExtractedProfile { static var mockWithRemovedDate: ExtractedProfile { @@ -1085,43 +1059,6 @@ extension ExtractedProfile { } } -final class MockUserNotification: DataBrokerProtectionUserNotificationService { - - var requestPermissionWasAsked = false - var firstScanNotificationWasSent = false - var firstRemovedNotificationWasSent = false - var checkInNotificationWasScheduled = false - var allInfoRemovedWasSent = false - - func requestNotificationPermission() { - requestPermissionWasAsked = true - } - - func sendFirstScanCompletedNotification() { - firstScanNotificationWasSent = true - } - - func sendFirstRemovedNotificationIfPossible() { - firstRemovedNotificationWasSent = true - } - - func sendAllInfoRemovedNotificationIfPossible() { - allInfoRemovedWasSent = true - } - - func scheduleCheckInNotificationIfPossible() { - checkInNotificationWasScheduled = true - } - - func reset() { - requestPermissionWasAsked = false - firstScanNotificationWasSent = false - firstRemovedNotificationWasSent = false - checkInNotificationWasScheduled = false - allInfoRemovedWasSent = false - } -} - extension AttemptInformation { static var mock: AttemptInformation { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift new file mode 100644 index 0000000000..f4d00a5d5c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift @@ -0,0 +1,306 @@ +// +// DataBrokerProtectionAgentManagerTests.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 DataBrokerProtection + +final class DataBrokerProtectionAgentManagerTests: XCTestCase { + + private var sut: DataBrokerProtectionAgentManager! + + private var mockActivityScheduler: MockDataBrokerProtectionBackgroundActivityScheduler! + private var mockNotificationService: MockUserNotificationService! + private var mockQueueManager: MockDataBrokerProtectionOperationQueueManager! + private var mockDataManager: MockDataBrokerProtectionDataManager! + private var mockIPCServer: MockIPCServer! + private var mockPixelHandler: MockPixelHandler! + private var mockDependencies: DefaultDataBrokerOperationDependencies! + private var mockProfile: DataBrokerProtectionProfile! + + override func setUpWithError() throws { + + mockPixelHandler = MockPixelHandler() + mockActivityScheduler = MockDataBrokerProtectionBackgroundActivityScheduler() + mockNotificationService = MockUserNotificationService() + + let mockDatabase = MockDatabase() + let mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) + mockQueueManager = MockDataBrokerProtectionOperationQueueManager( + operationQueue: MockDataBrokerProtectionOperationQueue(), + operationsCreator: MockDataBrokerOperationsCreator(), + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: MockDataBrokerProtectionBrokerUpdater(), + pixelHandler: mockPixelHandler) + + mockIPCServer = MockIPCServer(machServiceName: "") + + let fakeBroker = DataBrokerDebugFlagFakeBroker() + mockDataManager = MockDataBrokerProtectionDataManager(pixelHandler: mockPixelHandler, fakeBrokerFlag: fakeBroker) + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + config: DataBrokerExecutionConfig(), + runnerProvider: MockRunnerProvider(), + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockNotificationService) + + mockProfile = DataBrokerProtectionProfile( + names: [], + addresses: [], + phones: [], + birthYear: 1992) + } + + func testWhenAgentStart_andProfileExists_thenActivityIsScheduled_andSheduledOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = mockProfile + + var schedulerStarted = false + mockActivityScheduler.startSchedulerCompletion = { + schedulerStarted = true + } + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.agentFinishedLaunching() + + // Then + XCTAssertTrue(schedulerStarted) + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenAgentStart_andProfileDoesNotExist_thenActivityIsNotScheduled_andSheduledOpereationsNotRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = nil + + var schedulerStarted = false + mockActivityScheduler.startSchedulerCompletion = { + schedulerStarted = true + } + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.agentFinishedLaunching() + + // Then + XCTAssertFalse(schedulerStarted) + XCTAssertFalse(startScheduledScansCalled) + } + + func testWhenActivitySchedulerTriggers_thenSheduledOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = mockProfile + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + mockActivityScheduler.triggerDelegateCall() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenProfileSaved_thenImmediateOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockDataManager.profileToReturn = mockProfile + + var startImmediateScansCalled = false + mockQueueManager.startImmediateOperationsIfPermittedCalledCompletion = { _ in + startImmediateScansCalled = true + } + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(startImmediateScansCalled) + } + + func testWhenProfileSaved_thenUserNotificationPermissionAsked() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(mockNotificationService.requestPermissionWasAsked) + } + + func testWhenProfileSaved_andScansCompleted_andNoScanError_thenUserNotificationSent() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(mockNotificationService.firstScanNotificationWasSent) + } + + func testWhenProfileSaved_andScansCompleted_andScanError_thenUserNotificationNotSent() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + mockQueueManager.startImmediateOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) + + // When + sut.profileSaved() + + // Then + XCTAssertFalse(mockNotificationService.firstScanNotificationWasSent) + } + + func testWhenProfileSaved_andScansCompleted_andHasMatches_thenCheckInNotificationScheduled() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + mockDataManager.shouldReturnHasMatches = true + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(mockNotificationService.checkInNotificationWasScheduled) + } + + func testWhenProfileSaved_andScansCompleted_andHasNoMatches_thenCheckInNotificationNotScheduled() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + mockNotificationService.reset() + mockDataManager.shouldReturnHasMatches = false + + // When + sut.profileSaved() + + // Then + XCTAssertFalse(mockNotificationService.checkInNotificationWasScheduled) + } + + func testWhenAppLaunched_thenSheduledOpereationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler) + + var startScheduledScansCalled = false + mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.appLaunched() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift index 42c3bc94be..b825cad871 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift @@ -96,7 +96,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance @@ -120,7 +120,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [reAppereanceThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance @@ -144,7 +144,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [newMatchesPriorToThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [newMatchesPriorToThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches @@ -168,7 +168,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [newMatchesThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [newMatchesThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches @@ -192,7 +192,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [removalsPriorToThisWeekEvent])) + scanJobData: .mockWith(historyEvents: [removalsPriorToThisWeekEvent])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals @@ -217,7 +217,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [removalThisWeekEventOne, removalThisWeekEventTwo])) + scanJobData: .mockWith(historyEvents: [removalThisWeekEventOne, removalThisWeekEventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals @@ -244,7 +244,7 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mock, profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne, eventTwo, eventThree, eventFour])) + scanJobData: .mockWith(historyEvents: [eventOne, eventTwo, eventThree, eventFour])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries @@ -274,16 +274,16 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mockWithURL("www.brokerone.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokertwo.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokerthree.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokerfour.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])) + scanJobData: .mockWith(historyEvents: [eventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries @@ -313,16 +313,16 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mockWithURL("www.brokerone.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokertwo.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventOne])), + scanJobData: .mockWith(historyEvents: [eventOne])), .init(dataBroker: .mockWithURL("www.brokerthree.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokerfour.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])) + scanJobData: .mockWith(historyEvents: [eventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries @@ -352,16 +352,16 @@ final class DataBrokerProtectionEventPixelsTests: XCTestCase { let dataBrokerProfileQueries: [BrokerProfileQueryData] = [ .init(dataBroker: .mockWithURL("www.brokerone.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokertwo.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokerthree.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])), + scanJobData: .mockWith(historyEvents: [eventTwo])), .init(dataBroker: .mockWithURL("www.brokerfour.com"), profileQuery: .mock, - scanOperationData: .mockWith(historyEvents: [eventTwo])) + scanJobData: .mockWith(historyEvents: [eventTwo])) ] let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler) database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift index 0668f38d35..db8e447d4d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift @@ -194,8 +194,8 @@ final class DataBrokerProtectionProfileTests: XCTestCase { vault.brokers = [DataBroker.mock] vault.profileQueries = [ProfileQuery.mock] - vault.scanOperationData = [ScanOperationData.mock] - vault.optOutOperationData = [OptOutOperationData.mock(with: ExtractedProfile.mockWithoutRemovedDate)] + vault.scanJobData = [ScanJobData.mock] + vault.optOutJobData = [OptOutJobData.mock(with: ExtractedProfile.mockWithoutRemovedDate)] vault.profile = DataBrokerProtectionProfile( names: [ @@ -238,7 +238,7 @@ final class DataBrokerProtectionProfileTests: XCTestCase { vault.brokers = [DataBroker.mock] vault.profileQueries = [ProfileQuery.mock] - vault.scanOperationData = [ScanOperationData.mock] + vault.scanJobData = [ScanJobData.mock] vault.profile = DataBrokerProtectionProfile( names: [ diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift new file mode 100644 index 0000000000..e69d984c64 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift @@ -0,0 +1,320 @@ +// +// DataBrokerProtectionQueueManagerTests.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 DataBrokerProtection + +final class DataBrokerProtectionQueueManagerTests: XCTestCase { + + private var sut: DefaultDataBrokerProtectionQueueManager! + + private var mockQueue: MockDataBrokerProtectionOperationQueue! + private var mockOperationsCreator: MockDataBrokerOperationsCreator! + private var mockDatabase: MockDatabase! + private var mockPixelHandler: MockPixelHandler! + private var mockMismatchCalculator: MockMismatchCalculator! + private var mockUpdater: MockDataBrokerProtectionBrokerUpdater! + private var mockSchedulerConfig = DataBrokerExecutionConfig() + private var mockRunnerProvider: MockRunnerProvider! + private var mockUserNotification: MockUserNotificationService! + private var mockOperationErrorDelegate: MockDataBrokerOperationErrorDelegate! + private var mockDependencies: DefaultDataBrokerOperationDependencies! + + override func setUpWithError() throws { + mockQueue = MockDataBrokerProtectionOperationQueue() + mockOperationsCreator = MockDataBrokerOperationsCreator() + mockDatabase = MockDatabase() + mockPixelHandler = MockPixelHandler() + mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) + mockUpdater = MockDataBrokerProtectionBrokerUpdater() + mockRunnerProvider = MockRunnerProvider() + mockUserNotification = MockUserNotificationService() + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + config: DataBrokerExecutionConfig(), + runnerProvider: mockRunnerProvider, + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockUserNotification) + } + + func testWhenStartImmediateScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.scan) + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartScheduledScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.all) + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNotNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartImmediateScan_andCurrentModeIsScheduled_thenCurrentOperationsAreInterrupted_andCurrentCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(errorCollection.operationErrors?.count == 2) + let error = errorCollection.oneTimeError as? DataBrokerProtectionQueueError + XCTAssertEqual(error, .interrupted) + XCTAssert(mockQueue.didCallCancelCount == 1) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) + } + + func testWhenStartImmediateScan_andCurrentModeIsImmediate_thenCurrentOperationsAreInterrupted_andCurrentCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(errorCollection.operationErrors?.count == 2) + let error = errorCollection.oneTimeError as? DataBrokerProtectionQueueError + XCTAssertEqual(error, .interrupted) + XCTAssert(mockQueue.didCallCancelCount == 1) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) + } + + func testWhenSecondImmedateScanInterruptsFirst_andFirstHadErrors_thenSecondCompletesOnlyWithNewErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + var mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollectionFirst: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollectionFirst = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + var errorCollectionSecond: DataBrokerProtectionAgentErrorCollection! + mockOperationsWithError = (5...6).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } + mockOperations = (7...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollectionSecond = errors + } + + mockQueue.completeAllOperations() + + // Then + XCTAssert(errorCollectionFirst.operationErrors?.count == 2) + XCTAssert(errorCollectionSecond.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + } + + func testWhenStartScheduledScan_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + var mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(mockQueue.operationCount == 10) + + // Given + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + let expectedError = DataBrokerProtectionQueueError.cannotInterrupt + var completionCalled = false + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + completionCalled.toggle() + } + + // Then + XCTAssert(mockQueue.didCallCancelCount == 0) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 10) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count == 0) + XCTAssertEqual((errorCollection.oneTimeError as? DataBrokerProtectionQueueError), expectedError) + XCTAssert(completionCalled) + } + + func testWhenOperationBuildingFails_thenCompletionIsCalledOnOperationCreationOneTimeError() async throws { + // Given + mockOperationsCreator.shouldError = true + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let expectation = expectation(description: "Expected completion to be called") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssertNotNil(errorCollection.oneTimeError) + } + + func testWhenCallDebugOptOutCommand_thenOptOutOperationsAreCreated() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.optOut) + XCTAssert(mockOperationsCreator.createdType == .scan) + + // When + sut.execute(.startOptOutOperations(showWebView: false, + operationDependencies: mockDependencies, + completion: nil)) + + // Then + XCTAssert(mockOperationsCreator.createdType == .optOut) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift new file mode 100644 index 0000000000..ef2ab05b38 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift @@ -0,0 +1,122 @@ +// +// DataBrokerProtectionQueueModeTests.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. +// + +@testable import DataBrokerProtection +import XCTest + +final class DataBrokerProtectionQueueModeTests: XCTestCase { + + func testCurrentModeIdle_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeIdle_andNewModeScheduled_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeImmediate_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: { _ in })) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeImmediate_andNewModeScheduled_thenInterruptionNotAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertFalse(result) + } + + func testCurrentModeScheduled_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeScheduled_andNewModeScheduled_thenInterruptionNotAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertFalse(result) + } + + func testWhenModeIsIdle_thenPriorityDateIsNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.priorityDate + + // Then + XCTAssertNil(result) + } + + func testWhenModeIsImmediate_thenPriorityDateIsNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.priorityDate + + // Then + XCTAssertNil(result) + } + + func testWhenModeIsScheduled_thenPriorityDateIsNotNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.priorityDate + + // Then + XCTAssertNotNil(result) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index 2036ac9a1d..4f70f8934a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -39,7 +39,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenNoVersionIsStored_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil sut.checkForUpdatesInBrokerJSONFiles() @@ -53,7 +53,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndPatchIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.1")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.1")) repository.lastCheckedVersion = "1.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -67,7 +67,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndMinorIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "1.73.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -81,7 +81,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndMajorIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "0.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -95,7 +95,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndIsEqualOrGreaterThanCurrentOne_thenCheckingUpdatesIsSkipped() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "1.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -109,7 +109,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenSavedBrokerIsOnAnOldVersion_thenWeUpdateIt() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true @@ -127,7 +127,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenSavedBrokerIsOnTheCurrentVersion_thenWeDoNotUpdateIt() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true @@ -144,7 +144,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenFileBrokerIsNotStored_thenWeAddTheBrokerAndScanOperations() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index f10a26fb67..fb2af7862b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -40,7 +40,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -65,7 +65,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -90,7 +90,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -115,7 +115,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -136,7 +136,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { database.brokerProfileQueryDataToReturn = [ .mockParentWith(historyEvents: parentHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -158,7 +158,7 @@ extension BrokerProfileQueryData { schedulingConfig: DataBrokerScheduleConfig.mock ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), - scanOperationData: ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) + scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) ) } @@ -173,7 +173,7 @@ extension BrokerProfileQueryData { parent: "parent.com" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), - scanOperationData: ScanOperationData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) + scanJobData: ScanJobData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) ) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 99f01aa8ff..c4b2804e3f 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -45,12 +45,12 @@ extension BrokerProfileQueryData { mirrorSites: mirrorSites ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50, deprecated: deprecated), - scanOperationData: ScanOperationData(brokerId: 1, + scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, preferredRunDate: preferredRunDate, historyEvents: scanHistoryEvents, lastRunDate: lastRunDate), - optOutOperationsData: extractedProfile != nil ? [.mock(with: extractedProfile!)] : [OptOutOperationData]() + optOutJobData: extractedProfile != nil ? [.mock(with: extractedProfile!)] : [OptOutJobData]() ) } } @@ -478,8 +478,8 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault var profile: DataBrokerProtectionProfile? var profileQueries = [ProfileQuery]() var brokers = [DataBroker]() - var scanOperationData = [ScanOperationData]() - var optOutOperationData = [OptOutOperationData]() + var scanJobData = [ScanJobData]() + var optOutJobData = [OptOutJobData]() var lastPreferredRunDateOnScan: Date? typealias DatabaseProvider = SecureStorageDatabaseProviderMock @@ -498,8 +498,8 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault profile = nil profileQueries.removeAll() brokers.removeAll() - scanOperationData.removeAll() - optOutOperationData.removeAll() + scanJobData.removeAll() + optOutJobData.removeAll() lastPreferredRunDateOnScan = nil } @@ -565,12 +565,12 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { } - func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanOperationData? { - scanOperationData.first + func fetchScan(brokerId: Int64, profileQueryId: Int64) throws -> ScanJobData? { + scanJobData.first } - func fetchAllScans() throws -> [ScanOperationData] { - return scanOperationData + func fetchAllScans() throws -> [ScanJobData] { + return scanJobData } func save(brokerId: Int64, profileQueryId: Int64, extractedProfile: ExtractedProfile, lastRunDate: Date?, preferredRunDate: Date?) throws { @@ -585,16 +585,16 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { } - func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutOperationData? { - optOutOperationData.first + func fetchOptOut(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> OptOutJobData? { + optOutJobData.first } - func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutOperationData] { - return optOutOperationData + func fetchOptOuts(brokerId: Int64, profileQueryId: Int64) throws -> [OptOutJobData] { + return optOutJobData } - func fetchAllOptOuts() throws -> [OptOutOperationData] { - return optOutOperationData + func fetchAllOptOuts() throws -> [OptOutJobData] { + return optOutJobData } func save(historyEvent: HistoryEvent, brokerId: Int64, profileQueryId: Int64) throws { @@ -739,7 +739,7 @@ final class MockDatabase: DataBrokerProtectionRepository { wasDeleteProfileDataCalled = true } - func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { + func saveOptOutJob(optOut: OptOutJobData, extractedProfile: ExtractedProfile) throws { wasSaveOptOutOperationCalled = true } @@ -751,9 +751,9 @@ final class MockDatabase: DataBrokerProtectionRepository { } if let lastHistoryEventToReturn = self.lastHistoryEventToReturn { - let scanOperationData = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, historyEvents: [lastHistoryEventToReturn]) + let scanJobData = ScanJobData(brokerId: brokerId, profileQueryId: profileQueryId, historyEvents: [lastHistoryEventToReturn]) - return BrokerProfileQueryData(dataBroker: .mock, profileQuery: .mock, scanOperationData: scanOperationData) + return BrokerProfileQueryData(dataBroker: .mock, profileQuery: .mock, scanJobData: scanJobData) } else { return nil } @@ -868,7 +868,7 @@ final class MockAppVersion: AppVersionNumberProvider { } final class MockStageDurationCalculator: StageDurationCalculator { - var isManualScan: Bool = false + var isImmediateOperation: Bool = false var attemptId: UUID = UUID() var stage: Stage? @@ -961,6 +961,413 @@ final class MockDataBrokerProtectionBackendServicePixels: DataBrokerProtectionBa } } +final class MockRunnerProvider: JobRunnerProvider { + func getJobRunner() -> any WebJobRunner { + MockWebJobRunner() + } +} + +final class MockPixelHandler: EventMapping { + + init() { + super.init { event, _, _, _ in } + } +} + +extension ProfileQuery { + + static var mock: ProfileQuery { + .init(id: 1, firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) + } + + static var mockWithoutId: ProfileQuery { + .init(firstName: "First", lastName: "Last", city: "City", state: "State", birthYear: 1980) + } +} + +extension ScanJobData { + + static var mock: ScanJobData { + .init( + brokerId: 1, + profileQueryId: 1, + historyEvents: [HistoryEvent]() + ) + } + + static func mockWith(historyEvents: [HistoryEvent]) -> ScanJobData { + ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) + } + + static func mock(withBrokerId brokerId: Int64) -> ScanJobData { + .init( + brokerId: brokerId, + profileQueryId: 1, + historyEvents: [HistoryEvent]() + ) + } +} + +extension DataBroker { + + static func mock(withId id: Int64) -> DataBroker { + DataBroker( + id: id, + name: "Test broker", + url: "testbroker.com", + steps: [Step](), + version: "1.0", + schedulingConfig: DataBrokerScheduleConfig( + retryError: 0, + confirmOptOutScan: 0, + maintenanceScan: 0 + ) + ) + } +} + +final class MockDataBrokerProtectionOperationQueueManager: DataBrokerProtectionQueueManager { + var debugRunningStatusString: String { return "" } + + var startImmediateOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startScheduledOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + + var startImmediateOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startScheduledOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + + init(operationQueue: DataBrokerProtection.DataBrokerProtectionOperationQueue, operationsCreator: DataBrokerProtection.DataBrokerOperationsCreator, mismatchCalculator: DataBrokerProtection.MismatchCalculator, brokerUpdater: DataBrokerProtection.DataBrokerProtectionBrokerUpdater?, pixelHandler: Common.EventMapping) { + + } + + func startImmediateOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startImmediateOperationsIfPermittedCompletionError) + startImmediateOperationsIfPermittedCalledCompletion?(startImmediateOperationsIfPermittedCompletionError) + } + + func startScheduledOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startScheduledOperationsIfPermittedCompletionError) + startScheduledOperationsIfPermittedCalledCompletion?(startScheduledOperationsIfPermittedCompletionError) + } + + func execute(_ command: DataBrokerProtection.DataBrokerProtectionQueueManagerDebugCommand) { + } +} + +final class MockUserNotificationService: DataBrokerProtectionUserNotificationService { + + var requestPermissionWasAsked = false + var firstScanNotificationWasSent = false + var firstRemovedNotificationWasSent = false + var checkInNotificationWasScheduled = false + var allInfoRemovedWasSent = false + + func requestNotificationPermission() { + requestPermissionWasAsked = true + } + + func sendFirstScanCompletedNotification() { + firstScanNotificationWasSent = true + } + + func sendFirstRemovedNotificationIfPossible() { + firstRemovedNotificationWasSent = true + } + + func sendAllInfoRemovedNotificationIfPossible() { + allInfoRemovedWasSent = true + } + + func scheduleCheckInNotificationIfPossible() { + checkInNotificationWasScheduled = true + } + + func reset() { + requestPermissionWasAsked = false + firstScanNotificationWasSent = false + firstRemovedNotificationWasSent = false + checkInNotificationWasScheduled = false + allInfoRemovedWasSent = false + } +} + +final class MockDataBrokerProtectionBackgroundActivityScheduler: DataBrokerProtectionBackgroundActivityScheduler { + + var delegate: DataBrokerProtection.DataBrokerProtectionBackgroundActivitySchedulerDelegate? + var lastTriggerTimestamp: Date? + + var startSchedulerCompletion: (() -> Void)? + + func startScheduler() { + startSchedulerCompletion?() + } + + func triggerDelegateCall() { + delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self, completion: nil) + } +} + +final class MockDataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { + + var profileToReturn: DataBrokerProtectionProfile? + var shouldReturnHasMatches = false + + var cache: DataBrokerProtection.InMemoryDataCache + var delegate: DataBrokerProtection.DataBrokerProtectionDataManagerDelegate? + + init(pixelHandler: Common.EventMapping, fakeBrokerFlag: DataBrokerProtection.DataBrokerDebugFlag) { + cache = InMemoryDataCache() + } + + func saveProfile(_ profile: DataBrokerProtection.DataBrokerProtectionProfile) async throws { + } + + func fetchProfile() throws -> DataBrokerProtection.DataBrokerProtectionProfile? { + return profileToReturn + } + + func prepareProfileCache() throws { + } + + func fetchBrokerProfileQueryData(ignoresCache: Bool) throws -> [DataBrokerProtection.BrokerProfileQueryData] { + return [] + } + + func prepareBrokerProfileQueryDataCache() throws { + } + + func hasMatches() throws -> Bool { + return shouldReturnHasMatches + } + + func profileQueriesCount() throws -> Int { + return 0 + } +} + +final class MockIPCServer: DataBrokerProtectionIPCServer { + + var serverDelegate: DataBrokerProtection.DataBrokerProtectionAppToAgentInterface? + + init(machServiceName: String) { + } + + func activate() { + } + + func register() { + } + + func profileSaved(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + serverDelegate?.profileSaved() + } + + func appLaunched(xpcMessageReceivedCompletion: @escaping (Error?) -> Void) { + serverDelegate?.appLaunched() + } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } + + func startImmediateOperations(showWebView: Bool) { + serverDelegate?.startImmediateOperations(showWebView: showWebView) + } + + func startScheduledOperations(showWebView: Bool) { + serverDelegate?.startScheduledOperations(showWebView: showWebView) + } + + func runAllOptOuts(showWebView: Bool) { + serverDelegate?.runAllOptOuts(showWebView: showWebView) + } + + func getDebugMetadata(completion: @escaping (DataBrokerProtection.DBPBackgroundAgentMetadata?) -> Void) { + serverDelegate?.profileSaved() + } +} + +final class MockDataBrokerProtectionOperationQueue: DataBrokerProtectionOperationQueue { + var maxConcurrentOperationCount = 1 + + var operations: [Operation] = [] + var operationCount: Int { + operations.count + } + + private(set) var didCallCancelCount = 0 + private(set) var didCallAddCount = 0 + private(set) var didCallAddBarrierBlockCount = 0 + + private var barrierBlock: (@Sendable () -> Void)? + + func cancelAllOperations() { + didCallCancelCount += 1 + self.operations.forEach { $0.cancel() } + } + + func addOperation(_ op: Operation) { + didCallAddCount += 1 + self.operations.append(op) + } + + func addBarrierBlock(_ barrier: @escaping @Sendable () -> Void) { + didCallAddBarrierBlockCount += 1 + self.barrierBlock = barrier + } + + func completeAllOperations() { + operations.forEach { $0.start() } + operations.removeAll() + barrierBlock?() + } + + func completeOperationsUpTo(index: Int) { + guard index < operationCount else { return } + + (0.. [DataBrokerOperation] { + guard !shouldError else { throw DataBrokerProtectionError.unknown("")} + self.createdType = operationType + self.priorityDate = priorityDate + return operationCollections + } +} + +final class MockMismatchCalculator: MismatchCalculator { + + private(set) var didCallCalculateMismatches = false + + init(database: any DataBrokerProtectionRepository, pixelHandler: Common.EventMapping) { } + + func calculateMismatches() { + didCallCalculateMismatches = true + } +} + +final class MockDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrokerUpdater { + + private(set) var didCallUpdateBrokers = false + private(set) var didCallCheckForUpdates = false + + static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? { + nil + } + + func updateBrokers() { + didCallUpdateBrokers = true + } + + func checkForUpdatesInBrokerJSONFiles() { + didCallCheckForUpdates = true + } +} + final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManaging { var isUserAuthenticatedValue = false var accessTokenValue: String? = "fake token" From 0950da898a7b33968458ce93529ffcdf6bb3e311 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 22 May 2024 20:36:17 +0000 Subject: [PATCH 11/42] Bump version to 1.89.0 (191) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index a8018d8541..189892f698 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 190 +CURRENT_PROJECT_VERSION = 191 From 8876562112afddc22f9522cc1622fa5cec6e30e5 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Thu, 23 May 2024 00:40:23 +0200 Subject: [PATCH 12/42] Subscription refactoring (#2764) Task/Issue URL: https://app.asana.com/0/72649045549333/1206805455884775/f Tech Design URL: https://app.asana.com/0/481882893211075/1207147511614062/f Subscription refactoring for allowing unit testing. - DI - Removal of all singletons - Removal of all static functions use --- DuckDuckGo.xcodeproj/project.pbxproj | 194 +++++++++++++++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../DuckDuckGo Privacy Browser.xcscheme | 3 + DuckDuckGo/Application/AppDelegate.swift | 138 ++++++------- DuckDuckGo/Application/Application.swift | 12 +- DuckDuckGo/Application/URLEventHandler.swift | 98 +++++---- DuckDuckGo/Common/Logging/Logging.swift | 40 ++-- .../DBP/DataBrokerProtectionDebugMenu.swift | 25 +-- .../DBP/DataBrokerProtectionManager.swift | 2 +- ...erProtectionSubscriptionEventHandler.swift | 9 +- DuckDuckGo/Menus/MainMenu.swift | 37 ++-- DuckDuckGo/Menus/MainMenuActions.swift | 131 +++++++----- .../View/AddressBarTextField.swift | 8 +- .../NavigationBar/View/MoreOptionsMenu.swift | 12 +- .../View/NavigationBarViewController.swift | 16 +- ...rkProtection+ConvenienceInitializers.swift | 8 +- .../NetworkProtectionAppEvents.swift | 2 +- .../NetworkProtectionDebugMenu.swift | 44 +--- .../NetworkProtectionNavBarButtonModel.swift | 2 +- .../NetworkProtectionTunnelController.swift | 9 +- .../VPNLocation/VPNLocationViewModel.swift | 2 +- ...NetworkProtectionIPCTunnelController.swift | 2 +- .../NetworkProtectionRemoteMessaging.swift | 2 +- ...rkProtectionSubscriptionEventHandler.swift | 11 +- .../MacPacketTunnelProvider.swift | 35 +++- ...ore+SubscriptionTokenKeychainStorage.swift | 3 +- .../Model/PreferencesSection.swift | 7 +- .../Model/PreferencesSidebarModel.swift | 4 +- .../Model/VPNPreferencesModel.swift | 2 +- .../View/PreferencesRootView.swift | 8 +- .../View/PreferencesViewController.swift | 4 +- ...BrokerProtectionSettings+Environment.swift | 34 +++ ...atureAvailability+DefaultInitializer.swift | 4 +- .../SubscriptionEnvironment+Default.swift | 48 +++++ ...riptionManager+StandardConfiguration.swift | 57 +++++ .../SubscriptionRedirectManager.swift | 17 +- .../VPNSettings+Environment.swift | 34 +++ DuckDuckGo/Sync/SyncDebugMenu.swift | 2 +- DuckDuckGo/Tab/Model/Tab+Navigation.swift | 12 +- DuckDuckGo/Tab/Model/TabContent.swift | 10 +- .../RedirectNavigationResponder.swift | 17 +- ...ntityTheftRestorationPagesUserScript.swift | 2 +- .../SubscriptionAppStoreRestorer.swift | 72 ++++++- .../SubscriptionErrorReporter.swift | 2 +- .../SubscriptionPagesUserScript.swift | 142 +++++++------ DuckDuckGo/Tab/UserScripts/UserScripts.swift | 3 +- .../VPNFeedbackFormViewController.swift | 2 +- .../VPNFeedbackFormViewModel.swift | 3 +- .../VPNMetadataCollector.swift | 22 +- .../NetworkProtectionFeatureVisibility.swift | 15 +- ...taBrokerAuthenticationManagerBuilder.swift | 8 +- ...ataBrokerProtectionBackgroundManager.swift | 116 +++++++++++ ...kDuckGoDBPBackgroundAgentAppDelegate.swift | 20 +- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 51 ++++- DuckDuckGoVPN/NetworkProtectionBouncer.swift | 8 +- ...BrokerProtectionSubscriptionManaging.swift | 19 +- ...scriptionPurchaseEnvironmentManaging.swift | 37 ---- .../DebugMenu/DebugPurchaseModel.swift | 14 +- .../DebugPurchaseViewController.swift | 8 +- .../DebugMenu/SubscriptionDebugMenu.swift | 110 +++++----- .../PreferencesSubscriptionModel.swift | 41 ++-- .../ActivateSubscriptionAccessModel.swift | 15 +- .../Model/ShareSubscriptionAccessModel.swift | 22 +- .../SubscriptionAccessViewController.swift | 15 +- .../Sources/SubscriptionUI/UserText.swift | 4 +- UnitTests/Menus/MoreOptionsMenuTests.swift | 120 ++++++----- .../SubscriptionRedirectManagerTests.swift | 15 +- .../TabBar/View/TabBarViewItemTests.swift | 6 +- 68 files changed, 1335 insertions(+), 666 deletions(-) create mode 100644 DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift create mode 100644 DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift create mode 100644 DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift create mode 100644 DuckDuckGo/Subscription/VPNSettings+Environment.swift create mode 100644 DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6caadc88f1..f4c1f200d8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1568,8 +1568,6 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB108582A43375D000AB95F /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; - 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; - 7BBA7CE72BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; 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 */; }; @@ -1763,10 +1761,6 @@ 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; - 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; - 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; - 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; - 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; @@ -2589,6 +2583,8 @@ F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */; }; + F118EA7D2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; + F118EA7E2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; @@ -2615,14 +2611,62 @@ F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; F1B33DF72BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; + F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428D2BFB9F9C00A31506 /* Subscription */; }; + F1D042902BFB9FA300A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428F2BFB9FA300A31506 /* Subscription */; }; + F1D042912BFB9FD700A31506 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1D042922BFB9FD800A31506 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; + F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; + F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429B2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429F2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D042A02BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; + F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; F1D43AF32B98E47800BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */; }; F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */; }; + F1DA51862BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA51872BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA51882BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA51892BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA518A2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA518B2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA518C2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */; }; + F1DA518D2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */; }; + F1DA51922BF6081C00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51932BF6081D00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51962BF6083700CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51972BF6083A00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51A32BF6114200CF29FA /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A22BF6114200CF29FA /* Subscription */; }; + F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */; }; + F1DA51A72BF6114B00CF29FA /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A62BF6114B00CF29FA /* Subscription */; }; + F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */; }; F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 08D4923DC968236E22E373E2 /* Crashes */; }; F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 537FC71EA5115A983FAF3170 /* Crashes */; }; F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = DC3F73D49B2D44464AFEFCD8 /* Subscription */; }; F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = F1DF95E62BD188B60045E591 /* LoginItems */; }; + F1FDC9292BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92A2BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92B2BF4DFEC006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92C2BF4DFED006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92D2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92E2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC9382BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93B2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93C2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93D2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */; }; F4A6198C283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */; }; @@ -3372,7 +3416,6 @@ 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionIPCTunnelController.swift; sourceTree = ""; }; 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 = ""; }; - 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; 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 = ""; }; @@ -3519,8 +3562,6 @@ 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionPixelHandler.swift; sourceTree = ""; }; - 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; - 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManagerTests.swift; sourceTree = ""; }; 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; @@ -4090,13 +4131,20 @@ EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+VPNAgentConvenienceInitializers.swift"; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; + F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; F118EA842BEACC7000F77634 /* NonStandardPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonStandardPixel.swift; sourceTree = ""; }; F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPixel.swift; sourceTree = ""; }; F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProPixel.swift; sourceTree = ""; }; F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppStoreRestorer.swift; sourceTree = ""; }; F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionErrorReporter.swift; sourceTree = ""; }; + F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataBrokerProtectionSettings+Environment.swift"; sourceTree = ""; }; + F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionManager+StandardConfiguration.swift"; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; + F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; + F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; + F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; + F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VPNSettings+Environment.swift"; sourceTree = ""; }; F41D174025CB131900472416 /* NSColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSColorExtension.swift; sourceTree = ""; }; F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAppearanceExtension.swift; sourceTree = ""; }; F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScopeFeatureFlagging.swift; sourceTree = ""; }; @@ -4150,6 +4198,8 @@ 3706FE88293F661700E42796 /* OHHTTPStubs in Frameworks */, F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, + F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */, + F1DA51A32BF6114200CF29FA /* Subscription in Frameworks */, 3706FE89293F661700E42796 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4284,6 +4334,7 @@ buildActionMask = 2147483647; files = ( 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */, + F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */, 9D9AE8F92AAA3AD00026E7DC /* DataBrokerProtection in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4293,6 +4344,7 @@ buildActionMask = 2147483647; files = ( 315A023F2B6421AE00BFA577 /* Networking in Frameworks */, + F1D042902BFB9FA300A31506 /* Subscription in Frameworks */, 9D9AE8FB2AAA3AD90026E7DC /* DataBrokerProtection in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4344,6 +4396,8 @@ B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */, F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, + F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */, + F1DA51A72BF6114B00CF29FA /* Subscription in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4541,16 +4595,6 @@ path = Services; sourceTree = ""; }; - 1EA7B8D62B7E124E000330A4 /* Subscription */ = { - isa = PBXGroup; - children = ( - 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, - 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */, - 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */, - ); - path = Subscription; - sourceTree = ""; - }; 3169132B2BD2C7960051B46D /* ErrorView */ = { isa = PBXGroup; children = ( @@ -6634,7 +6678,7 @@ 4B677422255DBEB800025BD8 /* SmarterEncryption */, B68458AE25C7E75100DC17B6 /* StateRestoration */, B6A9E44E26142AF90067D1B9 /* Statistics */, - 1EA7B8D62B7E124E000330A4 /* Subscription */, + F118EA7B2BEA2B8700F77634 /* Subscription */, AACB8E7224A4C8BC005F2218 /* Suggestions */, 3775913429AB99DA00E26367 /* Sync */, AA86491B24D837DE001BABEE /* Tab */, @@ -8245,6 +8289,20 @@ path = JSAlert; sourceTree = ""; }; + F118EA7B2BEA2B8700F77634 /* Subscription */ = { + isa = PBXGroup; + children = ( + F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */, + F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, + F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */, + F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */, + F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */, + F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */, + F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */, + ); + path = Subscription; + sourceTree = ""; + }; F1B33DF92BAD9C83001128B3 /* Subscription */ = { isa = PBXGroup; children = ( @@ -8335,6 +8393,8 @@ 3706FDD8293F661700E42796 /* OHHTTPStubsSwift */, B65CD8CE2B316E0200A595BB /* SnapshotTesting */, F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */, + F1DA51A22BF6114200CF29FA /* Subscription */, + F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */, ); productName = DuckDuckGoTests; productReference = 3706FE99293F661700E42796 /* Unit Tests App Store.xctest */; @@ -8624,6 +8684,7 @@ packageProductDependencies = ( 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */, 9DEF97E02B06C4EE00764F03 /* Networking */, + F1D0428D2BFB9F9C00A31506 /* Subscription */, ); productName = DuckDuckGoAgent; productReference = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; @@ -8646,6 +8707,7 @@ packageProductDependencies = ( 9D9AE8FA2AAA3AD90026E7DC /* DataBrokerProtection */, 315A023E2B6421AE00BFA577 /* Networking */, + F1D0428F2BFB9FA300A31506 /* Subscription */, ); productName = DuckDuckGoAgent; productReference = 9D9AE8F22AAA39D30026E7DC /* DuckDuckGo Personal Information Removal App Store.app */; @@ -8730,6 +8792,8 @@ B6DA44182616C13800DD1EC2 /* OHHTTPStubsSwift */, B65CD8CA2B316DF100A595BB /* SnapshotTesting */, F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */, + F1DA51A62BF6114B00CF29FA /* Subscription */, + F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -9509,6 +9573,7 @@ buildActionMask = 2147483647; files = ( 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, + F1DA518D2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */, 3706FA7B293F65D500E42796 /* FaviconUserScript.swift in Sources */, 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, @@ -9555,6 +9620,7 @@ 3706FAA2293F65D500E42796 /* MainWindow.swift in Sources */, 3707C727294B5D2900682A9F /* WKWebView+SessionState.swift in Sources */, 4B44FEF42B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */, + F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */, @@ -9582,7 +9648,6 @@ 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, - 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9714,6 +9779,7 @@ 85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */, 3706FB19293F65D500E42796 /* FireViewController.swift in Sources */, B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */, + F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */, 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, @@ -9761,6 +9827,7 @@ 3706FB3D293F65D500E42796 /* FocusRingView.swift in Sources */, 3706FB3E293F65D500E42796 /* BookmarksBarViewModel.swift in Sources */, 3706FB3F293F65D500E42796 /* NSPopUpButtonView.swift in Sources */, + F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* ContextualMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, @@ -9769,6 +9836,7 @@ 3706FB43293F65D500E42796 /* DuckPlayer.swift in Sources */, 3706FB44293F65D500E42796 /* Favicon.swift in Sources */, 3706FB45293F65D500E42796 /* SuggestionContainerViewModel.swift in Sources */, + F1FDC92A2BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 3706FB46293F65D500E42796 /* FirePopoverWrapperViewController.swift in Sources */, 3706FB47293F65D500E42796 /* NSPasteboardItemExtension.swift in Sources */, 3706FB48293F65D500E42796 /* AutofillPreferencesModel.swift in Sources */, @@ -9803,6 +9871,7 @@ 3706FB5C293F65D500E42796 /* AVCaptureDevice+SwizzledAuthState.swift in Sources */, 3706FB5D293F65D500E42796 /* VisitMenuItem.swift in Sources */, 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, + F1DA51872BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, @@ -10019,7 +10088,6 @@ B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, - 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, 3706FBED293F65D500E42796 /* TabCollection.swift in Sources */, B6C0BB6B29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, @@ -10093,7 +10161,6 @@ 3706FC19293F65D500E42796 /* NSNotificationName+Favicons.swift in Sources */, 3706FC1A293F65D500E42796 /* PinningManager.swift in Sources */, 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, - 7BBA7CE72BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 3706FC1B293F65D500E42796 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */, 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, @@ -10102,6 +10169,7 @@ 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 3706FC20293F65D500E42796 /* PreferencesAutofillView.swift in Sources */, 3706FC21293F65D500E42796 /* UserText+PasswordManager.swift in Sources */, + F118EA7E2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 3706FC22293F65D500E42796 /* LoadingProgressView.swift in Sources */, 3706FC23293F65D500E42796 /* StatisticsStore.swift in Sources */, 1DDD3EBD2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, @@ -10666,11 +10734,15 @@ B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, + F1DA51932BF6081D00CF29FA /* AttributionPixelHandler.swift in Sources */, + F1FDC92C2BF4DFED006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, + F1DA51892BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8192A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 4B2D06322A11C1D300DE1F49 /* NSApplicationExtension.swift in Sources */, + F1FDC93B2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, @@ -10679,6 +10751,8 @@ 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, + F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1DA51972BF6083A00CF29FA /* PrivacyProPixel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10692,9 +10766,11 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, + F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, + F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, @@ -10705,6 +10781,8 @@ 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, + F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, + F1FDC92D2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -10712,8 +10790,10 @@ B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, + F1DA518A2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 7BA7CC442AD11E490042E5CE /* UserText.swift in Sources */, 4BF0E5142AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, + F1FDC93C2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 7BA7CC422AD11E420042E5CE /* NetworkProtectionBouncer.swift in Sources */, B65DA5F12A77D2BC00CBEE8D /* BundleExtension.swift in Sources */, ); @@ -10729,9 +10809,11 @@ 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, + 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 */, 7BA7CC412AD11E420042E5CE /* NetworkProtectionBouncer.swift in Sources */, @@ -10742,6 +10824,8 @@ 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, + F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, + F1FDC92E2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -10749,8 +10833,10 @@ 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, + F1DA518B2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, + F1FDC93D2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); @@ -10785,17 +10871,23 @@ 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, + F1DA51962BF6083700CF29FA /* PrivacyProPixel.swift in Sources */, EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, + F1D0429B2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, + F1DA51922BF6081C00CF29FA /* AttributionPixelHandler.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, + F1FDC92B2BF4DFEC006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, + F1DA51882BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, + F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10846,9 +10938,15 @@ buildActionMask = 2147483647; files = ( 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, + F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, + 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + F1D042912BFB9FD700A31506 /* SubscriptionEnvironment+Default.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, + 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + F1D0429F2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10858,9 +10956,15 @@ buildActionMask = 2147483647; files = ( 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, + F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, + F1D042922BFB9FD800A31506 /* SubscriptionEnvironment+Default.swift in Sources */, + 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + F1D042A02BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10876,6 +10980,7 @@ 4B9DB0412A983B24000927DB /* WaitlistDialogView.swift in Sources */, 37D2377A287EB8CA00BCE03B /* TabIndex.swift in Sources */, B60C6F8D29B200AB007BFAA8 /* SavePanelAccessoryView.swift in Sources */, + F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */, 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */, 4B723E0E26B0006300E14D75 /* LoginImport.swift in Sources */, @@ -11026,6 +11131,7 @@ 1DFAB51D2A8982A600A0F7F6 /* SetExtension.swift in Sources */, 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */, 37AFCE9227DB8CAD00471A10 /* PreferencesAboutView.swift in Sources */, + F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 4B2F565C2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 9826B0A02747DF3D0092F683 /* ContentBlocking.swift in Sources */, 4B379C2227BDBA29008A968E /* LocalAuthenticationService.swift in Sources */, @@ -11056,6 +11162,7 @@ AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, + 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, @@ -11124,6 +11231,7 @@ B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, 37D23785287F4E6500BCE03B /* PinnedTabsHostingView.swift in Sources */, 4BB99CFE26FE191E001E4761 /* FirefoxBookmarksReader.swift in Sources */, + F1DA518C2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */, 4BBC16A227C485BC00E00A38 /* DeviceIdleStateDetector.swift in Sources */, 4B379C2427BDE1B0008A968E /* FlatButton.swift in Sources */, 37054FC92873301700033B6F /* PinnedTabView.swift in Sources */, @@ -11131,6 +11239,7 @@ 4BA1A6A0258B079600F6F690 /* DataEncryption.swift in Sources */, B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */, B626A76D29928B1600053070 /* TestsClosureNavigationResponder.swift in Sources */, + F1DA51862BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 85B7184E27677CBB00B4277F /* RootView.swift in Sources */, AABEE6AF24AD22B90043105B /* AddressBarTextField.swift in Sources */, B693954C26F04BEB0015B914 /* FocusRingView.swift in Sources */, @@ -11165,7 +11274,6 @@ 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, - 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, @@ -11223,7 +11331,6 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, - 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11468,6 +11575,7 @@ 858A797F26A79EAA00A75A42 /* UserText+PasswordManager.swift in Sources */, B693954E26F04BEB0015B914 /* LoadingProgressView.swift in Sources */, B69B503C2726A12500758A2B /* StatisticsStore.swift in Sources */, + F1FDC9382BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */, 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, @@ -11537,6 +11645,7 @@ AAC82C60258B6CB5009B6B42 /* TabPreviewWindowController.swift in Sources */, AAC5E4E425D6BA9C007F5990 /* NSSizeExtension.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, + F1FDC9292BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, @@ -11577,6 +11686,7 @@ B6C00ECB292F839D009C73A6 /* AutofillTabExtension.swift in Sources */, B6E319382953446000DD3BCF /* Assertions.swift in Sources */, AAB549DF25DAB8F80058460B /* BookmarkViewModel.swift in Sources */, + F118EA7D2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 85707F28276A34D900DC0649 /* DaxSpeech.swift in Sources */, 31F28C5328C8EECA00119F70 /* DuckURLSchemeHandler.swift in Sources */, AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */, @@ -12924,8 +13034,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 145.3.3; + branch = fcappelli/subscription_refactoring_2; + kind = branch; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13574,6 +13684,16 @@ package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; productName = SwiftLintTool; }; + F1D0428D2BFB9F9C00A31506 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F1D0428F2BFB9FA300A31506 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */ = { isa = XCSwiftPackageProductDependency; package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; @@ -13584,6 +13704,26 @@ package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; productName = BareBonesBrowserKit; }; + F1DA51A22BF6114200CF29FA /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SubscriptionTestingUtilities; + }; + F1DA51A62BF6114B00CF29FA /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SubscriptionTestingUtilities; + }; F1DF95E62BD188B60045E591 /* LoginItems */ = { isa = XCSwiftPackageProductDependency; productName = LoginItems; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0e5fe45de..981f226c05 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" : "a49bbac8aa58033981a5a946d220886366dd471b", - "version" : "145.3.3" + "branch" : "fcappelli/subscription_refactoring_2", + "revision" : "874ae4269db821797742655e134e72199c2813c8" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index 7198d6f122..3afb1cb16d 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -154,6 +154,9 @@ + + diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index afbeaf6d23..f0811eca7f 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -34,12 +34,12 @@ import ServiceManagement import SyncDataProviders import UserNotifications import Lottie - import NetworkProtection import Subscription import NetworkProtectionIPC +import DataBrokerProtection -@MainActor +// @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { #if DEBUG @@ -86,20 +86,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - // Needs to be lazy as indirectly depends on AppDelegate - private lazy var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler = { - - let ipcClient = TunnelControllerIPCClient() - let tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) - let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) - - return NetworkProtectionSubscriptionEventHandler( - tunnelController: tunnelController, - vpnUninstaller: vpnUninstaller) - }() + private var accountManager: AccountManaging { + subscriptionManager.accountManager + } + public let subscriptionManager: SubscriptionManaging + public let vpnSettings = VPNSettings(defaults: .netP) + private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler? #if DBP - private let dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler() + private var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler? #endif private var didFinishLaunching = false @@ -188,19 +183,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager ) - #if APPSTORE || !STRIPE - SubscriptionPurchaseEnvironment.current = .appStore - #else - SubscriptionPurchaseEnvironment.current = .stripe - #endif - } + // Configure Subscription + subscriptionManager = SubscriptionManager() - static func configurePixelKit() { -#if DEBUG - Self.setUpPixelKit(dryRun: true) -#else - Self.setUpPixelKit(dryRun: false) -#endif + // Update VPN environment and match the Subscription environment + vpnSettings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) + + // Update DBP environment and match the Subscription environment + DataBrokerProtectionSettings().alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) } func applicationWillFinishLaunching(_ notification: Notification) { @@ -217,6 +207,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif appIconChanger = AppIconChanger(internalUserDecider: internalUserDecider) + + // Configure Event handlers + let ipcClient = TunnelControllerIPCClient() + let tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) + let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) + + networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager, + tunnelController: tunnelController, + vpnUninstaller: vpnUninstaller) +#if DBP + dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager) +#endif } // swiftlint:disable:next function_body_length @@ -264,19 +266,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { startupSync() - let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.default - - let currentEnvironment = UserDefaultsWrapper(key: .subscriptionEnvironment, - defaultValue: defaultEnvironment).wrappedValue - SubscriptionPurchaseEnvironment.currentServiceEnvironment = currentEnvironment - - Task { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - if let token = accountManager.accessToken { - _ = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) - _ = await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData) - } - } + subscriptionManager.loadInitialData() if [.normal, .uiTests].contains(NSApp.runType) { stateRestorationManager.applicationDidFinishLaunching() @@ -314,13 +304,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { UserDefaultsWrapper.clearRemovedKeys() - networkProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() + networkProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() - NetworkProtectionAppEvents().applicationDidFinishLaunching() + NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidFinishLaunching() UNUserNotificationCenter.current().delegate = self #if DBP - dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() + dataBrokerProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() #endif #if DBP @@ -336,15 +326,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() - NetworkProtectionAppEvents().applicationDidBecomeActive() - + NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() #if DBP DataBrokerProtectionAppEvents().applicationDidBecomeActive() #endif AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.toggleProtectionsCounter.sendEventsIfNeeded() - updateSubscriptionStatus() + subscriptionManager.updateSubscriptionStatus { isActive in + if isActive { + PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) + } + } } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { @@ -386,9 +379,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { urlEventHandler.handleFiles(files) } - private func applyPreferredTheme() { - let appearancePreferences = AppearancePreferences() - appearancePreferences.updateUserInterfaceStyle() + // MARK: - PixelKit + + static func configurePixelKit() { +#if DEBUG + Self.setUpPixelKit(dryRun: true) +#else + Self.setUpPixelKit(dryRun: false) +#endif } private static func setUpPixelKit(dryRun: Bool) { @@ -415,6 +413,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + // MARK: - Theme + + private func applyPreferredTheme() { + let appearancePreferences = AppearancePreferences() + appearancePreferences.updateUserInterfaceStyle() + } + // MARK: - Sync private func startupSync() { @@ -490,7 +495,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { switch response { case .alertSecondButtonReturn: alert.window.sheetParent?.endSheet(alert.window) - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) + DispatchQueue.main.async { + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) + } default: break } @@ -567,30 +574,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } private func setUpAutoClearHandler() { - autoClearHandler = AutoClearHandler(preferences: .shared, - fireViewModel: FireCoordinator.fireViewModel, - stateRestorationManager: stateRestorationManager) - autoClearHandler.handleAppLaunch() - autoClearHandler.onAutoClearCompleted = { - NSApplication.shared.reply(toApplicationShouldTerminate: true) - } - } - -} - -func updateSubscriptionStatus() { - Task { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - - guard let token = accountManager.accessToken else { return } - - if case .success(let subscription) = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { - if subscription.isActive { - PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) + DispatchQueue.main.async { + self.autoClearHandler = AutoClearHandler(preferences: .shared, + fireViewModel: FireCoordinator.fireViewModel, + stateRestorationManager: self.stateRestorationManager) + self.autoClearHandler.handleAppLaunch() + self.autoClearHandler.onAutoClearCompleted = { + NSApplication.shared.reply(toApplicationShouldTerminate: true) } } - - _ = await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData) } } @@ -609,7 +601,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate { #if DBP if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) + DispatchQueue.main.async { + DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) + } } #endif } diff --git a/DuckDuckGo/Application/Application.swift b/DuckDuckGo/Application/Application.swift index 2099e4aef8..73045c9ca9 100644 --- a/DuckDuckGo/Application/Application.swift +++ b/DuckDuckGo/Application/Application.swift @@ -23,16 +23,18 @@ import Foundation final class Application: NSApplication { private let copyHandler = CopyHandler() - private var _delegate: AppDelegate! +// private var _delegate: AppDelegate! + public static var appDelegate: AppDelegate! override init() { super.init() - _delegate = AppDelegate() - self.delegate = _delegate + let delegate = AppDelegate() + self.delegate = delegate + Application.appDelegate = delegate - let mainMenu = MainMenu(featureFlagger: _delegate.featureFlagger, - bookmarkManager: _delegate.bookmarksManager, + let mainMenu = MainMenu(featureFlagger: delegate.featureFlagger, + bookmarkManager: delegate.bookmarksManager, faviconManager: FaviconManager.shared, copyHandler: copyHandler) self.mainMenu = mainMenu diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 003cf5500b..723fbc2123 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -20,6 +20,7 @@ import Common import Foundation import AppKit import PixelKit +import Subscription import NetworkProtectionUI @@ -27,10 +28,10 @@ import NetworkProtectionUI import DataBrokerProtection #endif -@MainActor +// @MainActor final class URLEventHandler { - private let handler: @MainActor (URL) -> Void + private let handler: (URL) -> Void private var didFinishLaunching = false private var urlsToOpen = [URL]() @@ -50,7 +51,9 @@ final class URLEventHandler { if !urlsToOpen.isEmpty { for url in urlsToOpen { - self.handler(url) + DispatchQueue.main.async { + self.handler(url) + } } self.urlsToOpen = [] @@ -96,7 +99,9 @@ final class URLEventHandler { private func handleURLs(_ urls: [URL]) { if didFinishLaunching { - urls.forEach { self.handler($0) } + urls.forEach { + self.handler($0) + } } else { self.urlsToOpen.append(contentsOf: urls) } @@ -113,54 +118,58 @@ final class URLEventHandler { } #endif - if url.isFileURL && url.pathExtension == WebKitDownloadTask.downloadExtension { - guard let mainViewController = { - if let mainWindowController = WindowControllersManager.shared.lastKeyMainWindowController { - return mainWindowController.mainViewController + DispatchQueue.main.async { + if url.isFileURL && url.pathExtension == WebKitDownloadTask.downloadExtension { + guard let mainViewController = { + if let mainWindowController = WindowControllersManager.shared.lastKeyMainWindowController { + return mainWindowController.mainViewController + } + return WindowsManager.openNewWindow(with: .newtab, source: .ui, isBurner: false)?.contentViewController as? MainViewController + }() else { return } + + if !mainViewController.navigationBarViewController.isDownloadsPopoverShown { + mainViewController.navigationBarViewController.toggleDownloadsPopover(keepButtonVisible: false) } - return WindowsManager.openNewWindow(with: .newtab, source: .ui, isBurner: false)?.contentViewController as? MainViewController - }() else { return } - if !mainViewController.navigationBarViewController.isDownloadsPopoverShown { - mainViewController.navigationBarViewController.toggleDownloadsPopover(keepButtonVisible: false) + return } - return - } - - if url.scheme?.isNetworkProtectionScheme == false && url.scheme?.isDataBrokerProtectionScheme == false { - WaitlistModalDismisser.dismissWaitlistModalViewControllerIfNecessary(url) - WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) + if url.scheme?.isNetworkProtectionScheme == false && url.scheme?.isDataBrokerProtectionScheme == false { + WaitlistModalDismisser.dismissWaitlistModalViewControllerIfNecessary(url) + WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) + } } } /// Handles NetP URLs - /// private static func handleNetworkProtectionURL(_ url: URL) { - switch url { - case AppLaunchCommand.showStatus.launchURL: - Task { - await WindowControllersManager.shared.showNetworkProtectionStatus() - } - case AppLaunchCommand.showSettings.launchURL: - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) - case AppLaunchCommand.shareFeedback.launchURL: - WindowControllersManager.shared.showShareFeedbackModal() - case AppLaunchCommand.justOpen.launchURL: - WindowControllersManager.shared.showMainWindow() - case AppLaunchCommand.showVPNLocations.launchURL: - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) - WindowControllersManager.shared.showLocationPickerSheet() - case AppLaunchCommand.showPrivacyPro.launchURL: - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) - PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + DispatchQueue.main.async { + switch url { + case AppLaunchCommand.showStatus.launchURL: + Task { + await WindowControllersManager.shared.showNetworkProtectionStatus() + } + case AppLaunchCommand.showSettings.launchURL: + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) + case AppLaunchCommand.shareFeedback.launchURL: + WindowControllersManager.shared.showShareFeedbackModal() + case AppLaunchCommand.justOpen.launchURL: + WindowControllersManager.shared.showMainWindow() + case AppLaunchCommand.showVPNLocations.launchURL: + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) + WindowControllersManager.shared.showLocationPickerSheet() + case AppLaunchCommand.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: - // this should be run after NSApplication.shared is set - PFMoveToApplicationsFolderIfNecessary(false) + case AppLaunchCommand.moveAppToApplications.launchURL: + // this should be run after NSApplication.shared is set + PFMoveToApplicationsFolderIfNecessary(false) #endif - default: - return + default: + return + } } } @@ -171,15 +180,14 @@ final class URLEventHandler { switch url { case DataBrokerProtectionNotificationCommand.showDashboard.url: NotificationCenter.default.post(name: DataBrokerProtectionNotifications.shouldReloadUI, object: nil) - - WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + } default: return } } - #endif - } private extension String { diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift index 72a121cd48..3e05109032 100644 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ b/DuckDuckGo/Common/Logging/Logging.swift @@ -52,26 +52,26 @@ extension OSLog { } } - @OSLogWrapper(.atb) static var atb - @OSLogWrapper(.config) static var config - @OSLogWrapper(.downloads) static var downloads - @OSLogWrapper(.fire) static var fire - @OSLogWrapper(.dataImportExport) static var dataImportExport - @OSLogWrapper(.pixel) static var pixel - @OSLogWrapper(.httpsUpgrade) static var httpsUpgrade - @OSLogWrapper(.favicons) static var favicons - @OSLogWrapper(.autoLock) static var autoLock - @OSLogWrapper(.tabLazyLoading) static var tabLazyLoading - @OSLogWrapper(.autoconsent) static var autoconsent - @OSLogWrapper(.bookmarks) static var bookmarks - @OSLogWrapper(.attribution) static var attribution - @OSLogWrapper(.bitwarden) static var bitwarden - @OSLogWrapper(.navigation) static var navigation - @OSLogWrapper(.duckPlayer) static var duckPlayer - @OSLogWrapper(.tabSnapshots) static var tabSnapshots - @OSLogWrapper(.sync) static var sync - @OSLogWrapper(.networkProtection) static var networkProtection - @OSLogWrapper(.dbp) static var dbp + @OSLogWrapper(AppCategories.atb) static var atb + @OSLogWrapper(AppCategories.config) static var config + @OSLogWrapper(AppCategories.downloads) static var downloads + @OSLogWrapper(AppCategories.fire) static var fire + @OSLogWrapper(AppCategories.dataImportExport) static var dataImportExport + @OSLogWrapper(AppCategories.pixel) static var pixel + @OSLogWrapper(AppCategories.httpsUpgrade) static var httpsUpgrade + @OSLogWrapper(AppCategories.favicons) static var favicons + @OSLogWrapper(AppCategories.autoLock) static var autoLock + @OSLogWrapper(AppCategories.tabLazyLoading) static var tabLazyLoading + @OSLogWrapper(AppCategories.autoconsent) static var autoconsent + @OSLogWrapper(AppCategories.bookmarks) static var bookmarks + @OSLogWrapper(AppCategories.attribution) static var attribution + @OSLogWrapper(AppCategories.bitwarden) static var bitwarden + @OSLogWrapper(AppCategories.navigation) static var navigation + @OSLogWrapper(AppCategories.duckPlayer) static var duckPlayer + @OSLogWrapper(AppCategories.tabSnapshots) static var tabSnapshots + @OSLogWrapper(AppCategories.sync) static var sync + @OSLogWrapper(AppCategories.networkProtection) static var networkProtection + @OSLogWrapper(AppCategories.dbp) static var dbp // Debug->Logging categories will only be enabled for one day @UserDefaultsWrapper(key: .loggingEnabledDate, defaultValue: .distantPast) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 35104d98e3..39eb3062a3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -280,7 +280,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func runCustomJSON() { - let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: Application.appDelegate.subscriptionManager) let viewController = DataBrokerRunCustomJSONViewController(authenticationManager: authenticationManager) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable, .miniaturizable, .resizable], @@ -339,25 +339,13 @@ final class DataBrokerProtectionDebugMenu: NSMenu { settings.showInMenuBar.toggle() } - @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { - let title = menuItem.title - let selectedEnvironment: DataBrokerProtectionSettings.SelectedEnvironment - - if title == EnvironmentTitle.staging.rawValue { - selectedEnvironment = .staging - } else { - selectedEnvironment = .production - } - - settings.selectedEnvironment = selectedEnvironment - } - // MARK: - Utility Functions private func populateDataBrokerProtectionEnvironmentListMenuItems() { environmentMenu.items = [ - NSMenuItem(title: EnvironmentTitle.production.rawValue, action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), - NSMenuItem(title: EnvironmentTitle.staging.rawValue, action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), + NSMenuItem(title: "⚠️ The environment can be set in the Subscription > Environment menu", action: nil, target: nil), + NSMenuItem(title: EnvironmentTitle.production.rawValue, action: nil, target: nil, keyEquivalent: ""), + NSMenuItem(title: EnvironmentTitle.staging.rawValue, action: nil, target: nil, keyEquivalent: ""), ] } @@ -417,9 +405,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment + guard environmentMenu.items.count == 3 else { return } - environmentMenu.items.first?.state = selectedEnvironment == .production ? .on: .off - environmentMenu.items.last?.state = selectedEnvironment == .staging ? .on: .off + environmentMenu.items[1].state = selectedEnvironment == .production ? .on: .off + environmentMenu.items[2].state = selectedEnvironment == .staging ? .on: .off } private func updateShowStatusMenuIconMenu() { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index 111892a5fe..3a181f4594 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -51,7 +51,7 @@ public final class DataBrokerProtectionManager { }() private init() { - self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: Application.appDelegate.subscriptionManager) } public func isUserAuthenticated() -> Bool { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift index 50f8a36d4b..34142380b3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift @@ -23,9 +23,16 @@ import DataBrokerProtection import PixelKit final class DataBrokerProtectionSubscriptionEventHandler { + + private let subscriptionManager: SubscriptionManaging + private let authRepository: AuthenticationRepository private let featureDisabler: DataBrokerProtectionFeatureDisabling - init(featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { + init(subscriptionManager: SubscriptionManaging, + authRepository: AuthenticationRepository = KeychainAuthenticationData(), + featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { + self.subscriptionManager = subscriptionManager + self.authRepository = authRepository self.featureDisabler = featureDisabler } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 1b24150862..3a41e65525 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -540,7 +540,7 @@ import SubscriptionUI toggleBookmarksShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .bookmarks) toggleDownloadsShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .downloads) - if DefaultNetworkProtectionVisibility().isVPNVisible() { + if DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager).isVPNVisible() { toggleNetworkProtectionShortcutMenuItem.isHidden = false toggleNetworkProtectionShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .networkProtection) } else { @@ -620,24 +620,27 @@ import SubscriptionUI NSMenuItem(title: "Trigger Fatal Error", action: #selector(MainViewController.triggerFatalError)) - let currentEnvironmentWrapper = UserDefaultsWrapper(key: .subscriptionEnvironment, defaultValue: SubscriptionPurchaseEnvironment.ServiceEnvironment.default) let isInternalTestingWrapper = UserDefaultsWrapper(key: .subscriptionInternalTesting, defaultValue: false) + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! - SubscriptionDebugMenu( - currentEnvironment: { currentEnvironmentWrapper.wrappedValue.rawValue }, - updateEnvironment: { - guard let newEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment(rawValue: $0) else { return } - currentEnvironmentWrapper.wrappedValue = newEnvironment - SubscriptionPurchaseEnvironment.currentServiceEnvironment = newEnvironment - VPNSettings(defaults: .netP).selectedEnvironment = newEnvironment == .staging ? .staging : .production - }, - isInternalTestingEnabled: { isInternalTestingWrapper.wrappedValue }, - updateInternalTestingFlag: { isInternalTestingWrapper.wrappedValue = $0 }, - currentViewController: { - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - }, - subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs) - ) + var currentEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + let updateServiceEnvironment: (SubscriptionEnvironment.ServiceEnvironment) -> Void = { env in + currentEnvironment.serviceEnvironment = env + SubscriptionManager.save(subscriptionEnvironment: currentEnvironment, userDefaults: subscriptionUserDefaults) + } + let updatePurchasingPlatform: (SubscriptionEnvironment.PurchasePlatform) -> Void = { platform in + currentEnvironment.purchasePlatform = platform + SubscriptionManager.save(subscriptionEnvironment: currentEnvironment, userDefaults: subscriptionUserDefaults) + } + + SubscriptionDebugMenu(currentEnvironment: currentEnvironment, + updateServiceEnvironment: updateServiceEnvironment, + updatePurchasingPlatform: updatePurchasingPlatform, + isInternalTestingEnabled: { isInternalTestingWrapper.wrappedValue }, + updateInternalTestingFlag: { isInternalTestingWrapper.wrappedValue = $0 }, + currentViewController: { WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController }, + subscriptionManager: Application.appDelegate.subscriptionManager) NSMenuItem(title: "Logging").submenu(setupLoggingMenu()) } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 12094bbd16..103b90bc81 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -42,29 +42,41 @@ extension AppDelegate { // MARK: - File @objc func newWindow(_ sender: Any?) { - WindowsManager.openNewWindow() + DispatchQueue.main.async { + WindowsManager.openNewWindow() + } } @objc func newBurnerWindow(_ sender: Any?) { - WindowsManager.openNewWindow(burnerMode: BurnerMode(isBurner: true)) + DispatchQueue.main.async { + WindowsManager.openNewWindow(burnerMode: BurnerMode(isBurner: true)) + } } @objc func newTab(_ sender: Any?) { - WindowsManager.openNewWindow() + DispatchQueue.main.async { + WindowsManager.openNewWindow() + } } @objc func openLocation(_ sender: Any?) { - WindowsManager.openNewWindow() + DispatchQueue.main.async { + WindowsManager.openNewWindow() + } } @objc func closeAllWindows(_ sender: Any?) { - WindowsManager.closeWindows() + DispatchQueue.main.async { + WindowsManager.closeWindows() + } } // MARK: - History @objc func reopenLastClosedTab(_ sender: Any?) { - RecentlyClosedCoordinator.shared.reopenItem() + DispatchQueue.main.async { + RecentlyClosedCoordinator.shared.reopenItem() + } } @objc func recentlyClosedAction(_ sender: Any?) { @@ -73,8 +85,9 @@ extension AppDelegate { assertionFailure("Wrong represented object for recentlyClosedAction()") return } - - RecentlyClosedCoordinator.shared.reopenItem(cacheItem) + DispatchQueue.main.async { + RecentlyClosedCoordinator.shared.reopenItem(cacheItem) + } } @objc func openVisit(_ sender: NSMenuItem) { @@ -83,34 +96,41 @@ extension AppDelegate { assertionFailure("Wrong represented object") return } - - WindowsManager.openNewWindow(with: Tab(content: .contentFromURL(url, source: .historyEntry), shouldLoadInBackground: true)) + DispatchQueue.main.async { + WindowsManager.openNewWindow(with: Tab(content: .contentFromURL(url, source: .historyEntry), shouldLoadInBackground: true)) + } } @objc func clearAllHistory(_ sender: NSMenuItem) { - guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), - let windowController = window.windowController as? MainWindowController else { - assertionFailure("No reference to main window controller") - return - } + DispatchQueue.main.async { + guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), + let windowController = window.windowController as? MainWindowController else { + assertionFailure("No reference to main window controller") + return + } - windowController.mainViewController.clearAllHistory(sender) + windowController.mainViewController.clearAllHistory(sender) + } } @objc func clearThisHistory(_ sender: ClearThisHistoryMenuItem) { - guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), - let windowController = window.windowController as? MainWindowController else { - assertionFailure("No reference to main window controller") - return - } + DispatchQueue.main.async { + guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), + let windowController = window.windowController as? MainWindowController else { + assertionFailure("No reference to main window controller") + return + } - windowController.mainViewController.clearThisHistory(sender) + windowController.mainViewController.clearThisHistory(sender) + } } // MARK: - Window @objc func reopenAllWindowsFromLastSession(_ sender: Any?) { - _=stateRestorationManager.restoreLastSessionState(interactive: true) + DispatchQueue.main.async { + self.stateRestorationManager.restoreLastSessionState(interactive: true) + } } // MARK: - Help @@ -118,7 +138,9 @@ extension AppDelegate { #if FEEDBACK @objc func openFeedback(_ sender: Any?) { - FeedbackPresenter.presentFeedbackForm() + DispatchQueue.main.async { + FeedbackPresenter.presentFeedbackForm() + } } @objc func openReportBrokenSite(_ sender: Any?) { @@ -132,13 +154,15 @@ extension AppDelegate { display: true) privacyDashboardWindow = window - guard let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController, - let tabModel = parentWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel else { - assertionFailure("AppDelegate: Failed to present PrivacyDashboard") - return + DispatchQueue.main.async { + guard let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController, + let tabModel = parentWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel else { + assertionFailure("AppDelegate: Failed to present PrivacyDashboard") + return + } + privacyDashboardViewController.updateTabViewModel(tabModel) + parentWindowController.window?.beginSheet(window) { _ in } } - privacyDashboardViewController.updateTabViewModel(tabModel) - parentWindowController.window?.beginSheet(window) { _ in } } #endif @@ -154,22 +178,27 @@ extension AppDelegate { assertionFailure("Unexpected type of menuItem.representedObject: \(type(of: menuItem.representedObject))") return } - - let tab = Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true) - WindowsManager.openNewWindow(with: tab) + DispatchQueue.main.async { + let tab = Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true) + WindowsManager.openNewWindow(with: tab) + } } @objc func showManageBookmarks(_ sender: Any?) { - let tabCollection = TabCollection(tabs: [Tab(content: .bookmarks)]) - let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + DispatchQueue.main.async { + let tabCollection = TabCollection(tabs: [Tab(content: .bookmarks)]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) - WindowsManager.openNewWindow(with: tabCollectionViewModel) + WindowsManager.openNewWindow(with: tabCollectionViewModel) + } } @objc func openPreferences(_ sender: Any?) { - let tabCollection = TabCollection(tabs: [Tab(content: .anySettingsPane)]) - let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) - WindowsManager.openNewWindow(with: tabCollectionViewModel) + DispatchQueue.main.async { + let tabCollection = TabCollection(tabs: [Tab(content: .anySettingsPane)]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + WindowsManager.openNewWindow(with: tabCollectionViewModel) + } } @objc func openAbout(_ sender: Any?) { @@ -182,9 +211,12 @@ extension AppDelegate { } @objc func openImportBrowserDataWindow(_ sender: Any?) { - DataImportView().show() + DispatchQueue.main.async { + DataImportView().show() + } } + @MainActor @objc func openExportLogins(_ sender: Any?) { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, let window = windowController.window else { return } @@ -223,6 +255,7 @@ extension AppDelegate { } } + @MainActor @objc func openExportBookmarks(_ sender: Any?) { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, let window = windowController.window, @@ -246,16 +279,20 @@ extension AppDelegate { } @objc func fireButtonAction(_ sender: NSButton) { - FireCoordinator.fireButtonAction() + DispatchQueue.main.async { + FireCoordinator.fireButtonAction() + } } @objc func navigateToPrivateEmail(_ sender: Any?) { - guard let window = NSApplication.shared.keyWindow, - let windowController = window.windowController as? MainWindowController else { - assertionFailure("No reference to main window controller") - return + DispatchQueue.main.async { + guard let window = NSApplication.shared.keyWindow, + let windowController = window.windowController as? MainWindowController else { + assertionFailure("No reference to main window controller") + return + } + windowController.mainViewController.browserTabViewController.openNewTab(with: .url(URL.duckDuckGoEmailLogin, source: .ui)) } - windowController.mainViewController.browserTabViewController.openNewTab(with: .url(URL.duckDuckGoEmailLogin, source: .ui)) } } @@ -751,7 +788,7 @@ extension MainViewController { /// Clears the PrivacyPro state to make testing easier. /// private func clearPrivacyProState() { - AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).signOut() + Application.appDelegate.subscriptionManager.accountManager.signOut() resetThankYouModalChecks(nil) UserDefaults.netP.networkProtectionEntitlementsExpired = false diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 943f6a8e66..6acef850b7 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -67,6 +67,10 @@ final class AddressBarTextField: NSTextField { // flag when updating the Value from `handleTextDidChange()` private var currentTextDidChangeEvent: TextDidChangeEventType = .none +// var subscriptionEnvironment: SubscriptionEnvironment { +// Application.appDelegate.subscriptionManager.currentEnvironment +// } + // MARK: - Lifecycle override func awakeFromNib() { @@ -368,7 +372,9 @@ final class AddressBarTextField: NSTextField { #endif if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - if providedUrl.isChild(of: URL.subscriptionBaseURL) || providedUrl.isChild(of: URL.identityTheftRestoration) { + let baseURL = Application.appDelegate.subscriptionManager.url(for: .baseURL) + let identityTheftRestorationURL = Application.appDelegate.subscriptionManager.url(for: .identityTheftRestoration) + if providedUrl.isChild(of: baseURL) || providedUrl.isChild(of: identityTheftRestorationURL) { self.updateValue(selectedTabViewModel: nil, addressBarString: nil) // reset self.window?.makeFirstResponder(nil) return diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index f2092a4641..b4a1e23483 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -57,7 +57,7 @@ final class MoreOptionsMenu: NSMenu { private let passwordManagerCoordinator: PasswordManagerCoordinating private let internalUserDecider: InternalUserDecider private lazy var sharingMenu: NSMenu = SharingMenu(title: UserText.shareMenuItem) - private lazy var accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + private let accountManager: AccountManaging private let networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility @@ -68,15 +68,17 @@ final class MoreOptionsMenu: NSMenu { init(tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), passwordManagerCoordinator: PasswordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility, sharingMenu: NSMenu? = nil, - internalUserDecider: InternalUserDecider) { + internalUserDecider: InternalUserDecider, + accountManager: AccountManaging) { self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager self.passwordManagerCoordinator = passwordManagerCoordinator self.networkProtectionFeatureVisibility = networkProtectionFeatureVisibility self.internalUserDecider = internalUserDecider + self.accountManager = accountManager super.init(title: "") @@ -391,7 +393,9 @@ final class MoreOptionsMenu: NSMenu { } private func makeInactiveSubscriptionItems() -> [NSMenuItem] { - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let subscriptionManager = Application.appDelegate.subscriptionManager + let platform = subscriptionManager.currentEnvironment.purchasePlatform + let shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false if shouldHidePrivacyProDueToNoProducts { return [] } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index cab1a8fa70..02368d3191 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -71,6 +71,10 @@ final class NavigationBarViewController: NSViewController { return progressView }() + private var subscriptionManager: SubscriptionManaging { + Application.appDelegate.subscriptionManager + } + var addressBarViewController: AddressBarViewController? private var tabCollectionViewModel: TabCollectionViewModel @@ -269,7 +273,9 @@ final class NavigationBarViewController: NSViewController { let internalUserDecider = NSApp.delegateTyped.internalUserDecider let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: PasswordManagerCoordinator.shared, - internalUserDecider: internalUserDecider) + networkProtectionFeatureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager), + internalUserDecider: internalUserDecider, + accountManager: subscriptionManager.accountManager) menu.actionDelegate = self let location = NSPoint(x: -menu.size.width + sender.bounds.width, y: sender.bounds.height + 4) menu.popUp(positioning: nil, at: location, in: sender) @@ -894,7 +900,7 @@ extension NavigationBarViewController: NSMenuDelegate { let isPopUpWindow = view.window?.isPopUpWindow ?? false - if !isPopUpWindow && DefaultNetworkProtectionVisibility().isVPNVisible() { + if !isPopUpWindow && DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager).isVPNVisible() { let networkProtectionTitle = LocalPinningManager.shared.shortcutTitle(for: .networkProtection) menu.addItem(withTitle: networkProtectionTitle, action: #selector(toggleNetworkProtectionPanelPinning), keyEquivalent: "N") } @@ -1032,12 +1038,14 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } func optionsButtonMenuRequestedSubscriptionPurchasePage(_ menu: NSMenu) { - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) + let url = subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) } func optionsButtonMenuRequestedIdentityTheftRestoration(_ menu: NSMenu) { - WindowControllersManager.shared.showTab(with: .identityTheftRestoration(.identityTheftRestoration)) + let url = subscriptionManager.url(for: .identityTheftRestoration) + WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index fe1993cb94..f48a2c97fb 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -27,7 +27,7 @@ extension NetworkProtectionDeviceManager { @MainActor static func create() -> NetworkProtectionDeviceManager { - let settings = VPNSettings(defaults: .netP) + let settings = Application.appDelegate.vpnSettings let keyStore = NetworkProtectionKeychainKeyStore() let tokenStore = NetworkProtectionKeychainTokenStore() return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, @@ -40,7 +40,7 @@ extension NetworkProtectionDeviceManager { extension NetworkProtectionCodeRedemptionCoordinator { convenience init() { - let settings = VPNSettings(defaults: .netP) + let settings = Application.appDelegate.vpnSettings self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, @@ -54,7 +54,7 @@ extension NetworkProtectionKeychainTokenStore { } convenience init(isSubscriptionEnabled: Bool) { - let accessTokenProvider: () -> String? = { AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken } + let accessTokenProvider: () -> String? = { Application.appDelegate.subscriptionManager.accountManager.accessToken } self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, isSubscriptionEnabled: isSubscriptionEnabled, @@ -71,7 +71,7 @@ extension NetworkProtectionKeychainKeyStore { extension NetworkProtectionLocationListCompositeRepository { convenience init() { - let settings = VPNSettings(defaults: .netP) + let settings = Application.appDelegate.vpnSettings self.init( environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index aaf0b2de0f..bfe46a5e55 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -50,7 +50,7 @@ final class NetworkProtectionAppEvents { private let uninstaller: VPNUninstalling private let defaults: UserDefaults - init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + init(featureVisibility: NetworkProtectionFeatureVisibility, uninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 0323060f84..3cd6f11e63 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -168,7 +168,9 @@ final class NetworkProtectionDebugMenu: NSMenu { // MARK: - Tunnel Settings - private let settings = VPNSettings(defaults: .netP) + private var settings: VPNSettings { + Application.appDelegate.vpnSettings + } // MARK: - Debug Logic @@ -237,7 +239,7 @@ final class NetworkProtectionDebugMenu: NSMenu { /// @objc func logFeedbackMetadataToConsole(_ sender: Any?) { Task { @MainActor in - let collector = DefaultVPNMetadataCollector() + let collector = DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager) let metadata = await collector.collectMetadata() print(metadata.toPrettyPrintedJSON()!) @@ -319,8 +321,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private func populateNetworkProtectionEnvironmentListMenuItems() { environmentMenu.items = [ - NSMenuItem(title: "Production", action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), - NSMenuItem(title: "Staging", action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), + NSMenuItem(title: "⚠️ The environment can be set in the Subscription > Environment menu", action: nil, target: nil), + NSMenuItem(title: "Production", action: nil, target: nil, keyEquivalent: ""), + NSMenuItem(title: "Staging", action: nil, target: nil, keyEquivalent: ""), ] } @@ -417,15 +420,10 @@ final class NetworkProtectionDebugMenu: NSMenu { private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment + guard environmentMenu.items.count == 3 else { return } - switch selectedEnvironment { - case .production: - environmentMenu.items.first?.state = .on - environmentMenu.items.last?.state = .off - case .staging: - environmentMenu.items.first?.state = .off - environmentMenu.items.last?.state = .on - } + environmentMenu.items[1].state = selectedEnvironment == .production ? .on: .off + environmentMenu.items[2].state = selectedEnvironment == .staging ? .on: .off } private func updatePreferredServerMenu() { @@ -512,28 +510,6 @@ final class NetworkProtectionDebugMenu: NSMenu { } } - // MARK: Environment - - @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { - let title = menuItem.title - let selectedEnvironment: VPNSettings.SelectedEnvironment - - if title == "Staging" { - selectedEnvironment = .staging - } else { - selectedEnvironment = .production - } - - settings.selectedEnvironment = selectedEnvironment - - Task { - _ = try await NetworkProtectionDeviceManager.create().refreshServerList() - try? await populateNetworkProtectionServerListMenuItems() - - settings.selectedServer = .automatic - } - } - // MARK: - Exclusions private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index e4742d3521..de2dd586dd 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -71,7 +71,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { init(popoverManager: NetPPopoverManager, pinningManager: PinningManager = LocalPinningManager.shared, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), statusReporter: NetworkProtectionStatusReporter, iconProvider: IconProvider = NavigationBarIconProvider()) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 9bf6853f9a..44394aa36f 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -74,7 +74,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Subscriptions - private let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + private let accessTokenStorage: SubscriptionTokenKeychainStorage // MARK: - Debug Options Support @@ -164,7 +164,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr defaults: UserDefaults, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), notificationCenter: NotificationCenter = .default, - logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { + logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger(), + accessTokenStorage: SubscriptionTokenKeychainStorage) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID @@ -173,6 +174,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr self.settings = settings self.defaults = defaults self.tokenStore = tokenStore + self.accessTokenStorage = accessTokenStorage subscribeToSettingsChanges() subscribeToStatusChanges() @@ -793,7 +795,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } private func fetchAuthToken() throws -> NSString? { - if let accessToken = accountManager.accessToken { + + if let accessToken = try? accessTokenStorage.getAccessToken() { os_log(.error, log: .networkProtection, "🟢 TunnelController found token: %{public}d", accessToken) return Self.adaptAccessTokenForVPN(accessToken) as NSString? } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index e2e26d0e67..3f9f0f86e4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -190,7 +190,7 @@ extension VPNLocationViewModel { let locationListRepository = NetworkProtectionLocationListCompositeRepository() self.init( locationListRepository: locationListRepository, - settings: VPNSettings(defaults: .netP) + settings: Application.appDelegate.vpnSettings ) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index a39fcb6de4..028b3ba4cc 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -53,7 +53,7 @@ final class NetworkProtectionIPCTunnelController { private let pixelKit: PixelFiring? private let errorRecorder: VPNOperationErrorRecorder - init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, pixelKit: PixelFiring? = PixelKit.shared, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift index 6daeb90569..aef4e46516 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift @@ -55,7 +55,7 @@ final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMess messageStorage: HomePageRemoteMessagingStorage = DefaultHomePageRemoteMessagingStorage.networkProtection(), waitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: "networkprotection", keychainAppGroup: Bundle.main.appGroup(bundle: .netP)), waitlistActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), minimumRefreshInterval: TimeInterval, userDefaults: UserDefaults = .standard ) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 39752c05bd..6e3fa7ea0b 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -25,20 +25,19 @@ import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging private let tunnelController: TunnelController private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let vpnUninstaller: VPNUninstalling private let userDefaults: UserDefaults private var cancellables = Set() - init(accountManager: AccountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), + init(subscriptionManager: SubscriptionManaging, tunnelController: TunnelController, networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), vpnUninstaller: VPNUninstalling, userDefaults: UserDefaults = .netP) { - - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.tunnelController = tunnelController self.networkProtectionTokenStorage = networkProtectionTokenStorage self.vpnUninstaller = vpnUninstaller @@ -49,7 +48,7 @@ final class NetworkProtectionSubscriptionEventHandler { private func subscribeToEntitlementChanges() { Task { - switch await accountManager.hasEntitlement(for: .networkProtection) { + switch await subscriptionManager.accountManager.hasEntitlement(for: .networkProtection) { case .success(let hasEntitlements): Task { await handleEntitlementsChange(hasEntitlements: hasEntitlements) @@ -99,7 +98,7 @@ final class NetworkProtectionSubscriptionEventHandler { } @objc private func handleAccountDidSignIn() { - guard accountManager.accessToken != nil else { + guard subscriptionManager.accountManager.accessToken != nil else { assertionFailure("[NetP Subscription] AccountManager signed in but token could not be retrieved") return } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5623af1f0c..8b4fd7288d 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -141,8 +141,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #else let defaults = UserDefaults.netP #endif - let settings = VPNSettings(defaults: defaults) - switch event { case .userBecameActive: PixelKit.fire( @@ -327,8 +325,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization - @MainActor - @objc public init() { + // swiftlint:disable:next function_body_length + @MainActor @objc public init() { let isSubscriptionEnabled = false #if NETP_SYSTEM_EXTENSION @@ -338,26 +336,45 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #endif NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = AppVersion.shared.versionAndBuildNumber - let settings = VPNSettings(defaults: defaults) - let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) + + // MARK: - Configure Subscription + let subscriptionUserDefaults = UserDefaults(suiteName: MacPacketTunnelProvider.subscriptionsAppGroup)! + let notificationCenter: NetworkProtectionNotificationCenter = DistributedNotificationCenter.default() let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents, isSubscriptionEnabled: isSubscriptionEnabled, accessTokenProvider: { nil } ) + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + // Align Subscription environment to the VPN environment + var subscriptionEnvironment = SubscriptionEnvironment.default + switch settings.selectedEnvironment { + case .production: + subscriptionEnvironment.serviceEnvironment = .production + case .staging: + subscriptionEnvironment.serviceEnvironment = .staging + } - let accountManager = AccountManager(subscriptionAppGroup: Self.subscriptionsAppGroup, accessTokenStorage: tokenStore) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let accountManager = AccountManager(accessTokenStorage: tokenStore, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) - SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging let entitlementsCheck = { await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) } + let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) + let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) + super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift index 497ce84c7b..adc069cb3d 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift @@ -21,7 +21,8 @@ import Subscription import NetworkProtection import Common -extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStorage { +extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStoring { + public func store(accessToken: String) throws { try store(accessToken) } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 29e8333811..7a69e10868 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -61,10 +61,11 @@ struct PreferencesSection: Hashable, Identifiable { ] if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { + let subscriptionManager = Application.appDelegate.subscriptionManager + let platform = subscriptionManager.currentEnvironment.purchasePlatform + var shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false - var shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { + if subscriptionManager.accountManager.isUserAuthenticated { shouldHidePrivacyProDueToNoProducts = false } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 1bcd647e37..db5a483437 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -42,7 +42,7 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent], privacyConfigurationManager: PrivacyConfigurationManaging, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility() + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager) ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs @@ -77,7 +77,7 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + vpnVisibility: NetworkProtectionFeatureVisibility, includeDuckPlayer: Bool, userDefaults: UserDefaults = .netP ) { diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 1316d6cf85..b5e8273c17 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -59,7 +59,7 @@ final class VPNPreferencesModel: ObservableObject { private var onboardingStatus: OnboardingStatus { didSet { - showUninstallVPN = DefaultNetworkProtectionVisibility().isInstalled + showUninstallVPN = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager).isInstalled } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 350eb84f96..55833aea85 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -138,7 +138,8 @@ enum Preferences { WindowControllersManager.shared.showTab(with: .dataBrokerProtection) case .openITR: PixelKit.fire(PrivacyProPixel.privacyProIdentityRestorationSettings) - WindowControllersManager.shared.showTab(with: .identityTheftRestoration(.identityTheftRestoration)) + let url = Application.appDelegate.subscriptionManager.url(for: .identityTheftRestoration) + WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) case .iHaveASubscriptionClick: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseClick) case .activateAddEmailClick: @@ -169,7 +170,8 @@ enum Preferences { return } - await SubscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) + let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: Application.appDelegate.subscriptionManager) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) } } }, @@ -179,7 +181,7 @@ enum Preferences { return PreferencesSubscriptionModel(openURLHandler: openURL, userEventHandler: handleUIEvent, sheetActionHandler: sheetActionHandler, - subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + subscriptionManager: Application.appDelegate.subscriptionManager) } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 37d63c320f..967bc49768 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -34,7 +34,9 @@ final class PreferencesViewController: NSViewController { private var bitwardenManager: BWManagement = BWManager.shared init(syncService: DDGSyncing, duckPlayer: DuckPlayer = DuckPlayer.shared) { - model = PreferencesSidebarModel(syncService: syncService, includeDuckPlayer: duckPlayer.isAvailable) + model = PreferencesSidebarModel(syncService: syncService, + vpnVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + includeDuckPlayer: duckPlayer.isAvailable) super.init(nibName: nil, bundle: nil) } diff --git a/DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift b/DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift new file mode 100644 index 0000000000..48c1c54f2c --- /dev/null +++ b/DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift @@ -0,0 +1,34 @@ +// +// DataBrokerProtectionSettings+Environment.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import DataBrokerProtection +import Subscription + +public extension DataBrokerProtectionSettings { + + /// Align VPN environment to the Subscription environment + func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { + switch subscriptionEnvironment.serviceEnvironment { + case .production: + self.selectedEnvironment = .production + case .staging: + self.selectedEnvironment = .staging + } + } +} diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift index 73e6407c28..24929319ea 100644 --- a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift +++ b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift @@ -17,12 +17,14 @@ // import Foundation +import AppKit import Subscription import BrowserServicesKit extension DefaultSubscriptionFeatureAvailability { + convenience init() { self.init(privacyConfigurationManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, - purchasePlatform: SubscriptionPurchaseEnvironment.current) + purchasePlatform: Application.appDelegate.subscriptionManager.currentEnvironment.purchasePlatform) } } diff --git a/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift b/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift new file mode 100644 index 0000000000..fdb1353662 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift @@ -0,0 +1,48 @@ +// +// SubscriptionEnvironment+Default.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 + +extension SubscriptionEnvironment { + + public static var `default`: SubscriptionEnvironment { +#if APPSTORE || !STRIPE + let platform: SubscriptionEnvironment.PurchasePlatform = .appStore +#else + let platform: SubscriptionEnvironment.PurchasePlatform = .stripe +#endif + +#if ALPHA || DEBUG + let environment: SubscriptionEnvironment.ServiceEnvironment = .staging +#else + let environment: SubscriptionEnvironment.ServiceEnvironment = .production +#endif + return SubscriptionEnvironment(serviceEnvironment: environment, purchasePlatform: platform) + } +} + +extension SubscriptionManager { + + static public func getSavedOrDefaultEnvironment(userDefaults: UserDefaults) -> SubscriptionEnvironment { + if let savedEnvironment = loadEnvironmentFrom(userDefaults: userDefaults) { + return savedEnvironment + } + return SubscriptionEnvironment.default + } +} diff --git a/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift b/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift new file mode 100644 index 0000000000..552b8e28b7 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift @@ -0,0 +1,57 @@ +// +// SubscriptionManager+StandardConfiguration.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription +import Common + +extension SubscriptionManager { + + // Init the SubscriptionManager using the standard dependencies and configuration, to be used only in the dependencies tree root + public convenience init() { + // MARK: - Configure Subscription + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) + + if #available(macOS 12.0, *) { + let storePurchaseManager = StorePurchaseManager() + self.init(storePurchaseManager: storePurchaseManager, + accountManager: accountManager, + subscriptionService: subscriptionService, + authService: authService, + subscriptionEnvironment: subscriptionEnvironment) + } else { + self.init(accountManager: accountManager, + subscriptionService: subscriptionService, + authService: authService, + subscriptionEnvironment: subscriptionEnvironment) + } + } +} diff --git a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift index 66e6aefa04..2e9c3d1f62 100644 --- a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift +++ b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift @@ -25,10 +25,20 @@ protocol SubscriptionRedirectManager: AnyObject { } final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { + private let featureAvailabiltyProvider: () -> Bool + private let subscriptionEnvironment: SubscriptionEnvironment + private let canPurchase: () -> Bool + private let baseURL: URL - init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable) { + init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable, + subscriptionEnvironment: SubscriptionEnvironment, + baseURL: URL, + canPurchase: @escaping () -> Bool) { self.featureAvailabiltyProvider = featureAvailabiltyProvider + self.subscriptionEnvironment = subscriptionEnvironment + self.canPurchase = canPurchase + self.baseURL = baseURL } func redirectURL(for url: URL) -> URL? { @@ -36,15 +46,14 @@ final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { if url.pathComponents == URL.privacyPro.pathComponents { let isFeatureAvailable = featureAvailabiltyProvider() - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let shouldHidePrivacyProDueToNoProducts = subscriptionEnvironment.purchasePlatform == .appStore && canPurchase() == false let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts // Redirect the `/pro` URL to `/subscriptions` URL. If there are any query items in the original URL it appends to the `/subscriptions` URL. - return isPurchasePageRedirectActive ? URL.subscriptionBaseURL.addingQueryItems(from: url) : nil + return isPurchasePageRedirectActive ? baseURL.addingQueryItems(from: url) : nil } return nil } - } private extension URL { diff --git a/DuckDuckGo/Subscription/VPNSettings+Environment.swift b/DuckDuckGo/Subscription/VPNSettings+Environment.swift new file mode 100644 index 0000000000..4e046637ed --- /dev/null +++ b/DuckDuckGo/Subscription/VPNSettings+Environment.swift @@ -0,0 +1,34 @@ +// +// VPNSettings+Environment.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 Subscription + +public extension VPNSettings { + + /// Align VPN environment to the Subscription environment + func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { + switch subscriptionEnvironment.serviceEnvironment { + case .production: + self.selectedEnvironment = .production + case .staging: + self.selectedEnvironment = .staging + } + } +} diff --git a/DuckDuckGo/Sync/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift index c5ff802e84..db74c0125d 100644 --- a/DuckDuckGo/Sync/SyncDebugMenu.swift +++ b/DuckDuckGo/Sync/SyncDebugMenu.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import AppKit import DDGSync import Bookmarks diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index 2b86d68118..5d5ac5606f 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -71,7 +71,7 @@ extension Tab: NavigationResponder { // add extra headers to SERP requests .struct(SerpHeadersNavigationResponder()), - .struct(RedirectNavigationResponder()), + .struct(redirectNavigationResponder), // ensure Content Blocking Rules are applied before navigation .weak(nullable: self.contentBlockingAndSurrogates), @@ -107,4 +107,14 @@ extension Tab: NavigationResponder { } } + var redirectNavigationResponder: RedirectNavigationResponder { + let subscriptionManager = Application.appDelegate.subscriptionManager + let redirectManager = PrivacyProSubscriptionRedirectManager(subscriptionEnvironment: subscriptionManager.currentEnvironment, + baseURL: subscriptionManager.url(for: .baseURL), + canPurchase: { + subscriptionManager.canPurchase + }) + return RedirectNavigationResponder(redirectManager: redirectManager) + } + } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index 369a036ba2..b213cc2ba5 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -117,12 +117,16 @@ extension TabContent { } if let url { - if url.isChild(of: URL.subscriptionBaseURL) { - if SubscriptionPurchaseEnvironment.currentServiceEnvironment == .staging, url.getParameter(named: "environment") == nil { + let subscriptionManager = Application.appDelegate.subscriptionManager + let environment = subscriptionManager.currentEnvironment.serviceEnvironment + let subscriptionBaseURL = subscriptionManager.url(for: .baseURL) + let identityTheftRestorationURL = subscriptionManager.url(for: .identityTheftRestoration) + if url.isChild(of: subscriptionBaseURL) { + if environment == .staging, url.getParameter(named: "environment") == nil { return .subscription(url.appendingParameter(name: "environment", value: "staging")) } return .subscription(url) - } else if url.isChild(of: URL.identityTheftRestoration) { + } else if url.isChild(of: identityTheftRestorationURL) { return .identityTheftRestoration(url) } } diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index 81f400302c..5e57ccab46 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -23,7 +23,7 @@ struct RedirectNavigationResponder: NavigationResponder { private let redirectManager: SubscriptionRedirectManager - init(redirectManager: SubscriptionRedirectManager = PrivacyProSubscriptionRedirectManager()) { + init(redirectManager: SubscriptionRedirectManager) { self.redirectManager = redirectManager } @@ -37,4 +37,19 @@ struct RedirectNavigationResponder: NavigationResponder { } } +// private func redirectURL(for url: URL) -> URL? { +// guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } +// +// if url.pathComponents == URL.privacyPro.pathComponents { +// let isFeatureAvailable = DefaultSubscriptionFeatureAvailability().isFeatureAvailable +// let subscriptionManager = Application.appDelegate.subscriptionManager +// let platform = subscriptionManager.currentEnvironment.purchasePlatform +// let shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false +// let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts +// let url = SubscriptionURL.baseURL.subscriptionURL(environment: subscriptionManager.currentEnvironment.serviceEnvironment) +// return isPurchasePageRedirectActive ? url : nil +// } +// +// return nil +// } } diff --git a/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift index a613d64566..a1ad9e3f9d 100644 --- a/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift @@ -92,7 +92,7 @@ final class IdentityTheftRestorationPagesFeature: Subfeature { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken { + if let accessToken = await Application.appDelegate.subscriptionManager.accountManager.accessToken { return ["token": accessToken] } else { return [String: String]() diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift index 35661ff131..68a02e6372 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import AppKit import Subscription import SubscriptionUI import enum StoreKit.StoreKitError @@ -25,8 +25,16 @@ import PixelKit @available(macOS 12.0, *) struct SubscriptionAppStoreRestorer { - // swiftlint:disable:next cyclomatic_complexity - static func restoreAppStoreSubscription(mainViewController: MainViewController, windowController: MainWindowController) async { + private let subscriptionManager: SubscriptionManaging + @MainActor var window: NSWindow? { WindowControllersManager.shared.lastKeyMainWindowController?.window } + let subscriptionErrorReporter = SubscriptionErrorReporter() + + public init(subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager + } + + // swiftlint:disable:next cyclomatic_complexity function_body_length + func restoreAppStoreSubscription(mainViewController: MainViewController, windowController: MainWindowController) async { let progressViewController = await ProgressViewController(title: UserText.restoringSubscriptionTitle) defer { @@ -39,7 +47,7 @@ struct SubscriptionAppStoreRestorer { mainViewController.presentAsSheet(progressViewController) } - let syncResult = await PurchaseManager.shared.syncAppleIDAccount() + let syncResult = await subscriptionManager.storePurchaseManager().syncAppleIDAccount() switch syncResult { case .success: @@ -63,7 +71,8 @@ struct SubscriptionAppStoreRestorer { } } - let result = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { case .success: @@ -77,15 +86,56 @@ struct SubscriptionAppStoreRestorer { switch error { case .missingAccountOrTransactions: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) - await windowController.showSubscriptionNotFoundAlert() + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + await showSubscriptionNotFoundAlert() case .subscriptionExpired: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionExpired) - await windowController.showSubscriptionInactiveAlert() + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionExpired) + await showSubscriptionInactiveAlert() case .pastTransactionAuthenticationError, .failedToObtainAccessToken, .failedToFetchAccountDetails, .failedToFetchSubscriptionDetails: - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) - await windowController.showSomethingWentWrongAlert() + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) + await showSomethingWentWrongAlert() } } } } + +@available(macOS 12.0, *) +extension SubscriptionAppStoreRestorer { + + /* + WARNING: DUPLICATED CODE + This code will be moved as part of https://app.asana.com/0/0/1207157941206686/f + */ + + // MARK: - UI interactions + + @MainActor + func showSomethingWentWrongAlert() { + PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) + guard let window else { return } + + window.show(.somethingWentWrongAlert()) + } + + @MainActor + func showSubscriptionNotFoundAlert() { + guard let window else { return } + + window.show(.subscriptionNotFoundAlert(), firstButtonAction: { + let url = subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) + PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + }) + } + + @MainActor + func showSubscriptionInactiveAlert() { + guard let window else { return } + + window.show(.subscriptionInactiveAlert(), firstButtonAction: { + let url = subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) + PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + }) + } +} diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift index 438997c0f7..874e5ab9cb 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift @@ -40,7 +40,7 @@ enum SubscriptionError: Error { struct SubscriptionErrorReporter { // swiftlint:disable:next cyclomatic_complexity - static func report(subscriptionActivationError: SubscriptionError) { + func report(subscriptionActivationError: SubscriptionError) { os_log(.error, log: .subscription, "Subscription purchase error: %{public}s", subscriptionActivationError.localizedDescription) diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index f0e7525820..32f819fead 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -31,9 +31,7 @@ public extension Notification.Name { static let subscriptionPageCloseAndOpenPreferences = Notification.Name("com.duckduckgo.subscriptionPage.CloseAndOpenPreferences") } -/// /// The user script that will be the broker for all subscription features -/// public final class SubscriptionPagesUserScript: NSObject, UserScript, UserScriptMessaging { public var source: String = "" @@ -66,16 +64,14 @@ extension SubscriptionPagesUserScript: WKScriptMessageHandlerWithReply { } } -// MARK: - Fallback for macOS 10.15 +/// Fallback for macOS 10.15 extension SubscriptionPagesUserScript: WKScriptMessageHandler { public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { // unsupported } } -/// /// Use Subscription sub-feature -/// final class SubscriptionPagesUseSubscriptionFeature: Subfeature { weak var broker: UserScriptMessageBroker? var featureName = "useSubscription" @@ -84,18 +80,29 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { .exact(hostname: "abrown.duckduckgo.com") ]) - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + @MainActor + var window: NSWindow? { + WindowControllersManager.shared.lastKeyMainWindowController?.window + } + let subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging { subscriptionManager.accountManager } + var subscriptionPlatform: SubscriptionEnvironment.PurchasePlatform { subscriptionManager.currentEnvironment.purchasePlatform } + + let stripePurchaseFlow: StripePurchaseFlow + let subscriptionErrorReporter = SubscriptionErrorReporter() + let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + + public init(subscriptionManager: SubscriptionManaging, + subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { + self.subscriptionManager = subscriptionManager + self.stripePurchaseFlow = StripePurchaseFlow(subscriptionManager: subscriptionManager) + self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + } func with(broker: UserScriptMessageBroker) { self.broker = broker } - private let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler - - init(subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { - self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler - } - struct Handlers { static let getSubscription = "getSubscription" static let setSubscription = "setSubscription" @@ -189,17 +196,13 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard DefaultSubscriptionFeatureAvailability().isSubscriptionPurchaseAllowed else { return SubscriptionOptions.empty } - if SubscriptionPurchaseEnvironment.current == .appStore { + switch subscriptionPlatform { + case .appStore: if #available(macOS 12.0, *) { - switch await AppStorePurchaseFlow.subscriptionOptions() { - case .success(let subscriptionOptions): - return subscriptionOptions - case .failure: - break - } + return await subscriptionManager.storePurchaseManager().subscriptionOptions() } - } else if SubscriptionPurchaseEnvironment.current == .stripe { - switch await StripePurchaseFlow.subscriptionOptions() { + case .stripe: + switch await stripePurchaseFlow.subscriptionOptions() { case .success(let subscriptionOptions): return subscriptionOptions case .failure: @@ -210,8 +213,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return SubscriptionOptions.empty } - let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) - // swiftlint:disable:next function_body_length cyclomatic_complexity func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) @@ -223,8 +224,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // Extract the origin from the webview URL to use for attribution pixel. subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) - - if SubscriptionPurchaseEnvironment.current == .appStore { + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController let progressViewController = await ProgressViewController(title: UserText.purchasingSubscriptionTitle) @@ -237,7 +237,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) return nil } @@ -246,41 +246,44 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.presentAsSheet(progressViewController) // Check for active subscriptions - if await PurchaseManager.hasActiveSubscription() { + if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { PixelKit.fire(PrivacyProPixel.privacyProRestoreAfterPurchaseAttempt) os_log(.info, log: .subscription, "[Purchase] Found active subscription during purchase") - SubscriptionErrorReporter.report(subscriptionActivationError: .hasActiveSubscription) - await WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionFoundAlert(originalMessage: message) + subscriptionErrorReporter.report(subscriptionActivationError: .hasActiveSubscription) + await showSubscriptionFoundAlert(originalMessage: message) return nil } let emailAccessToken = try? EmailManager().getToken() let purchaseTransactionJWS: String + let appStorePurchaseFlow = AppStorePurchaseFlow(subscriptionManager: subscriptionManager) os_log(.info, log: .subscription, "[Purchase] Purchasing") - switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken, subscriptionAppGroup: subscriptionAppGroup) { + switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { case .success(let transactionJWS): purchaseTransactionJWS = transactionJWS case .failure(let error): switch error { case .noProductsFound: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) case .activeSubscriptionAlreadyPresent: - SubscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) + subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) case .authenticatingWithTransactionFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) case .accountCreationFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) case .purchaseFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) case .cancelledByUser: - SubscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) + subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) case .missingEntitlements: - SubscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + case .internalError: + assertionFailure("Internal error") } if error != .cancelledByUser { - await WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() + await showSomethingWentWrongAlert() } await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) return nil @@ -290,7 +293,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { os_log(.info, log: .subscription, "[Purchase] Completing purchase") - switch await AppStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS, subscriptionAppGroup: subscriptionAppGroup) { + switch await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) { case .success(let purchaseUpdate): os_log(.info, log: .subscription, "[Purchase] Purchase complete") PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) @@ -300,44 +303,46 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .failure(let error): switch error { case .noProductsFound: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) case .activeSubscriptionAlreadyPresent: - SubscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) + subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) case .authenticatingWithTransactionFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) case .accountCreationFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) case .purchaseFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) case .cancelledByUser: - SubscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) + subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) case .missingEntitlements: - SubscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) DispatchQueue.main.async { NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) } return nil + case .internalError: + assertionFailure("Internal error") } await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) } } - } else if SubscriptionPurchaseEnvironment.current == .stripe { + } else if subscriptionPlatform == .stripe { let emailAccessToken = try? EmailManager().getToken() - let result = await StripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken, subscriptionAppGroup: subscriptionAppGroup) + let result = await stripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken) switch result { case .success(let success): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: success) case .failure(let error): - await WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() + await showSomethingWentWrongAlert() switch error { case .noProductsFound: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) case .accountCreationFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) } await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) } @@ -358,7 +363,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let actionHandlers = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { @MainActor in - await SubscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) + let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) message.webView?.reload() } } @@ -375,8 +381,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } }) - let vc = await SubscriptionAccessViewController(accountManager: accountManager, actionHandlers: actionHandlers, subscriptionAppGroup: subscriptionAppGroup) - await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(vc) + let subscriptionAccessViewController = await SubscriptionAccessViewController(subscriptionManager: subscriptionManager, actionHandlers: actionHandlers) + await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(subscriptionAccessViewController) return nil } @@ -414,7 +420,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await WindowControllersManager.shared.showTab(with: .dataBrokerProtection) case .identityTheftRestoration: PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .unique) - await WindowControllersManager.shared.showTab(with: .identityTheftRestoration(.identityTheftRestoration)) + let url = subscriptionManager.url(for: .identityTheftRestoration) + await WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) } return nil @@ -425,7 +432,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let progressViewController = await ProgressViewController(title: UserText.completingPurchaseTitle) await mainViewController?.presentAsSheet(progressViewController) - await StripePurchaseFlow.completeSubscriptionPurchase(subscriptionAppGroup: subscriptionAppGroup) + await stripePurchaseFlow.completeSubscriptionPurchase() await mainViewController?.dismiss(progressViewController) PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) @@ -461,7 +468,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken { + if let accessToken = accountManager.accessToken { return ["token": accessToken] } else { return [String: String]() @@ -495,7 +502,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } } -extension MainWindowController { +extension SubscriptionPagesUseSubscriptionFeature { + + /* + WARNING: + This code will be moved as part of https://app.asana.com/0/0/1207157941206686/f + */ + + // MARK: - UI interactions @MainActor func showSomethingWentWrongAlert() { @@ -510,7 +524,8 @@ extension MainWindowController { guard let window else { return } window.show(.subscriptionNotFoundAlert(), firstButtonAction: { - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) + let url = self.subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @@ -520,7 +535,8 @@ extension MainWindowController { guard let window else { return } window.show(.subscriptionInactiveAlert(), firstButtonAction: { - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) + let url = self.subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @@ -532,10 +548,10 @@ extension MainWindowController { window.show(.subscriptionFoundAlert(), firstButtonAction: { if #available(macOS 12.0, *) { Task { - let result = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: self.subscriptionManager) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { - case .success: - PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .dailyAndCount) + case .success: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .dailyAndCount) case .failure: break } originalMessage.webView?.reload() diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 1bc9443977..2668a5fde6 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -50,6 +50,7 @@ final class UserScripts: UserScriptsProvider { clickToLoadScript = ClickToLoadUserScript(scriptSourceProvider: sourceProvider) contentBlockerRulesScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig!) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig!) + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let privacyConfig = sourceProvider.privacyConfigurationManager.privacyConfig let sessionKey = sourceProvider.sessionKey ?? "" @@ -92,7 +93,7 @@ final class UserScripts: UserScriptsProvider { } if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature()) + subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature(subscriptionManager: Application.appDelegate.subscriptionManager)) userScripts.append(subscriptionPagesUserScript) identityTheftRestorationPagesUserScript.registerSubfeature(delegate: IdentityTheftRestorationPagesFeature()) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift index fe8b8258e2..590d958513 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift @@ -40,7 +40,7 @@ final class VPNFeedbackFormViewController: NSViewController { private var cancellables = Set() init() { - self.viewModel = VPNFeedbackFormViewModel() + self.viewModel = VPNFeedbackFormViewModel(metadataCollector: DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager)) super.init(nibName: nil, bundle: nil) self.viewModel.delegate = self } diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift index 7363d0d817..70aeb3e110 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift @@ -67,7 +67,8 @@ final class VPNFeedbackFormViewModel: ObservableObject { private let metadataCollector: VPNMetadataCollector private let feedbackSender: VPNFeedbackSender - init(metadataCollector: VPNMetadataCollector = DefaultVPNMetadataCollector(), feedbackSender: VPNFeedbackSender = DefaultVPNFeedbackSender()) { + init(metadataCollector: VPNMetadataCollector, + feedbackSender: VPNFeedbackSender = DefaultVPNFeedbackSender()) { self.viewState = .feedbackPending self.selectedFeedbackCategory = .landingPage diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 700149aad7..e69c0c4a41 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -121,11 +121,14 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter private let ipcClient: TunnelControllerIPCClient private let defaults: UserDefaults + private let accountManager: AccountManaging + private let settings: VPNSettings - init(defaults: UserDefaults = .netP) { + init(defaults: UserDefaults = .netP, + accountManager: AccountManaging) { let ipcClient = TunnelControllerIPCClient() ipcClient.register() - + self.accountManager = accountManager self.ipcClient = ipcClient self.defaults = defaults @@ -141,6 +144,16 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { // Force refresh just in case. A refresh is requested when the IPC client is created, but distributed notifications don't guarantee delivery // so we'll play it safe and add one more attempt. self.statusReporter.forceRefresh() + + self.settings = VPNSettings(defaults: defaults) + updateSettings() + } + + func updateSettings() { + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + settings.alignTo(subscriptionEnvironment: subscriptionEnvironment) } @MainActor @@ -287,8 +300,6 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectVPNSettingsState() -> VPNMetadata.VPNSettingsState { - let settings = VPNSettings(defaults: defaults) - return .init( connectOnLoginEnabled: settings.connectOnLogin, includeAllNetworksEnabled: settings.includeAllNetworks, @@ -302,10 +313,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false - return .init( hasPrivacyProAccount: accountManager.isUserAuthenticated, hasVPNEntitlement: hasVPNEntitlement diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 753f6c3054..a9b92cdac0 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -44,22 +44,22 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation private let privacyConfigurationManager: PrivacyConfigurationManaging private let defaults: UserDefaults - let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) - let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), vpnUninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP, - log: OSLog = .networkProtection) { + log: OSLog = .networkProtection, + subscriptionManager: SubscriptionManaging) { self.privacyConfigurationManager = privacyConfigurationManager self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.vpnUninstaller = vpnUninstaller self.featureOverrides = featureOverrides self.defaults = defaults - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) + self.subscriptionManager = subscriptionManager } var isInstalled: Bool { @@ -76,7 +76,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { return false } - switch await accountManager.hasEntitlement(for: .networkProtection) { + switch await subscriptionManager.accountManager.hasEntitlement(for: .networkProtection) { case .success(let hasEntitlement): return hasEntitlement case .failure(let error): @@ -93,8 +93,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { guard subscriptionFeatureAvailability.isFeatureAvailable else { return false } - - return accountManager.isUserAuthenticated + return subscriptionManager.accountManager.isUserAuthenticated } /// We've had to add this method because accessing the singleton in app delegate is crashing the integration tests. @@ -106,7 +105,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// Returns whether the VPN should be uninstalled automatically. /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. func shouldUninstallAutomatically() -> Bool { - return subscriptionFeatureAvailability.isFeatureAvailable && !accountManager.isUserAuthenticated && LoginItem.vpnMenu.status.isInstalled + return subscriptionFeatureAvailability.isFeatureAvailable && !subscriptionManager.accountManager.isUserAuthenticated && LoginItem.vpnMenu.status.isInstalled } /// Whether the user is fully onboarded diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift index 594eb0b3d3..b07a7c2796 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift @@ -21,10 +21,10 @@ import DataBrokerProtection import Subscription final public class DataBrokerAuthenticationManagerBuilder { - static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase()) -> DataBrokerProtectionAuthenticationManager { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let subscriptionManager = DataBrokerProtectionSubscriptionManager(accountManager: accountManager, - environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManager()) + + static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase(), + subscriptionManager: SubscriptionManaging) -> DataBrokerProtectionAuthenticationManager { + let subscriptionManager = DataBrokerProtectionSubscriptionManager(subscriptionManager: subscriptionManager) return DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift new file mode 100644 index 0000000000..1098fda56b --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift @@ -0,0 +1,116 @@ +// +// DataBrokerProtectionBackgroundManager.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 Common +import BrowserServicesKit +import DataBrokerProtection +import PixelKit +import Subscription + +public final class DataBrokerProtectionBackgroundManager { + + private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + + private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() + private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() + private let authenticationManager: DataBrokerProtectionAuthenticationManaging + private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() + + private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) + + lazy var dataManager: DataBrokerProtectionDataManager = { + DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) + }() + + lazy var scheduler: DataBrokerProtectionScheduler = { + let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned + let features = ContentScopeFeatureToggles(emailProtection: false, + emailProtectionIncontextSignup: false, + credentialsAutofill: false, + identitiesAutofill: false, + creditCardsAutofill: false, + credentialsSaving: false, + passwordGeneration: false, + inlineIconCredentials: false, + thirdPartyCredentialsProvider: false) + + let sessionKey = UUID().uuidString + let prefs = ContentScopeProperties(gpcEnabled: false, + sessionKey: sessionKey, + featureToggles: features) + + let pixelHandler = DataBrokerProtectionPixelsHandler() + + let userNotificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) + + return DefaultDataBrokerProtectionScheduler(privacyConfigManager: privacyConfigurationManager, + contentScopeProperties: prefs, + dataManager: dataManager, + notificationCenter: NotificationCenter.default, + pixelHandler: pixelHandler, + authenticationManager: authenticationManager, + userNotificationService: userNotificationService) + }() + + public init(subscriptionManager: SubscriptionManaging) { + let redeemUseCase = RedeemUseCase(authenticationService: authenticationService, + authenticationRepository: authenticationRepository) + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager( + redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + _ = ipcServiceManager + } + + public func runOperationsAndStartSchedulerIfPossible() { + pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossible) + + do { + // If there's no saved profile we don't need to start the scheduler + guard (try dataManager.fetchProfile()) != nil else { + pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile) + return + } + } catch { + pixelHandler.fire(.generalError(error: error, + functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) + return + } + + scheduler.runQueuedOperations(showWebView: false) { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), error: %{public}@", + log: .dataBrokerProtection, + oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, + functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + return + } + + self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler) + self?.scheduler.startScheduler() + } + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 7e2bcebbef..70465069d4 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -24,10 +24,12 @@ import DataBrokerProtection import BrowserServicesKit import PixelKit import Networking +import Subscription @objc(Application) final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { - private let _delegate = DuckDuckGoDBPBackgroundAgentAppDelegate() + private let _delegate: DuckDuckGoDBPBackgroundAgentAppDelegate + private let subscriptionManager: SubscriptionManaging override init() { os_log(.error, log: .dbpBackgroundAgent, "🟢 DBP background Agent starting: %{public}d", NSRunningApplication.current.processIdentifier) @@ -65,6 +67,11 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { exit(0) } + // Configure Subscription + subscriptionManager = SubscriptionManager() + + _delegate = DuckDuckGoDBPBackgroundAgentAppDelegate(subscriptionManager: subscriptionManager) + super.init() self.delegate = _delegate } @@ -80,19 +87,28 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele private let settings = DataBrokerProtectionSettings() private var cancellables = Set() private var statusBarMenu: StatusBarMenu? + private let subscriptionManager: SubscriptionManaging private var manager: DataBrokerProtectionAgentManager? + init(subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager + } + @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), authenticationRepository: KeychainAuthenticationData()) - let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase) + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager) manager?.agentFinishedLaunching() setupStatusBarMenu() + + // Aligning the environment with the Subscription one + settings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) } @MainActor diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 3f277460e5..de78d86b57 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -31,7 +31,9 @@ import Subscription @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { - private let _delegate = DuckDuckGoVPNAppDelegate() + + public let accountManager: AccountManaging + private let _delegate: DuckDuckGoVPNAppDelegate override init() { os_log(.error, log: .networkProtection, "🟢 Status Bar Agent starting: %{public}d", NSRunningApplication.current.processIdentifier) @@ -42,14 +44,31 @@ final class DuckDuckGoVPNApplication: NSApplication { exit(0) } + // MARK: - Configure Subscription + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) + + _delegate = DuckDuckGoVPNAppDelegate(bouncer: NetworkProtectionBouncer(accountManager: accountManager), + accountManager: accountManager, + accessTokenStorage: accessTokenStorage, + subscriptionEnvironment: subscriptionEnvironment) super.init() setupPixelKit() self.delegate = _delegate #if DEBUG - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - if let token = accountManager.accessToken { os_log(.error, log: .networkProtection, "🟢 VPN Agent found token: %{public}d", token) } else { @@ -104,7 +123,20 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private static let recentThreshold: TimeInterval = 5.0 private let appLauncher = AppLauncher() - private let bouncer = NetworkProtectionBouncer() + private let bouncer: NetworkProtectionBouncer + private let accountManager: AccountManaging + private let accessTokenStorage: SubscriptionTokenKeychainStorage + + public init(bouncer: NetworkProtectionBouncer, + accountManager: AccountManaging, + accessTokenStorage: SubscriptionTokenKeychainStorage, + subscriptionEnvironment: SubscriptionEnvironment) { + self.bouncer = bouncer + self.accountManager = accountManager + self.accessTokenStorage = accessTokenStorage + self.tunnelSettings = VPNSettings(defaults: .netP) + self.tunnelSettings.alignTo(subscriptionEnvironment: subscriptionEnvironment) + } private var cancellables = Set() @@ -126,7 +158,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { #endif } - private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private let tunnelSettings: VPNSettings private lazy var userDefaults = UserDefaults.netP private lazy var proxySettings = TransparentProxySettings(defaults: .netP) @@ -187,7 +219,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings, - defaults: userDefaults) + defaults: userDefaults, + accessTokenStorage: accessTokenStorage) /// An IPC server that provides access to the tunnel controller. /// @@ -317,9 +350,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { - APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) - SubscriptionPurchaseEnvironment.currentServiceEnvironment = tunnelSettings.selectedEnvironment == .production ? .production : .staging + APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog, type: .info) setupMenuVisibility() @@ -375,10 +407,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() private func setUpSubscriptionMonitoring() { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) guard accountManager.isUserAuthenticated else { return } let entitlementsCheck = { - await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + await self.accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) } Task { diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 302245b270..e9acf969b5 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -27,12 +27,16 @@ import Subscription /// final class NetworkProtectionBouncer { + let accountManager: AccountManaging + + init(accountManager: AccountManaging) { + self.accountManager = accountManager + } + /// Simply verifies that the VPN feature is enabled and if not, takes care of killing the /// current app. /// func requireAuthTokenOrKillApp(controller: TunnelController) async { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - guard !accountManager.isUserAuthenticated else { return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift index 4b94e75351..c2035d4c06 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift @@ -27,27 +27,24 @@ public protocol DataBrokerProtectionSubscriptionManaging { } public final class DataBrokerProtectionSubscriptionManager: DataBrokerProtectionSubscriptionManaging { - private let accountManager: DataBrokerProtectionAccountManaging - private let environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging + + let subscriptionManager: SubscriptionManaging public var isUserAuthenticated: Bool { - accountManager.accessToken != nil + subscriptionManager.accountManager.accessToken != nil } public var accessToken: String? { - accountManager.accessToken + subscriptionManager.accountManager.accessToken } - public init(accountManager: DataBrokerProtectionAccountManaging, - environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging) { - self.accountManager = accountManager - self.environmentManager = environmentManager + public init(subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager } public func hasValidEntitlement() async throws -> Bool { - environmentManager.updateEnvironment() - - switch await accountManager.hasEntitlement(for: .reloadIgnoringLocalCacheData) { + switch await subscriptionManager.accountManager.hasEntitlement(for: .dataBrokerProtection, + cachePolicy: .reloadIgnoringLocalCacheData) { case let .success(result): return result case .failure(let error): diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift deleted file mode 100644 index a18796bace..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Subscription - -/// This protocol exists only as a wrapper on top of the SubscriptionPurchaseEnvironment since it is a concrete type on BSK -public protocol DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { - func updateEnvironment() -} - -public final class DataBrokerProtectionSubscriptionPurchaseEnvironmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { - private let settings: DataBrokerProtectionSettings - - public init(settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings()) { - self.settings = settings - } - - public func updateEnvironment() { - SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging - } -} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift index 556d1d4b9f..c25e2b486e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift @@ -23,17 +23,17 @@ import Subscription @available(macOS 12.0, *) public final class DebugPurchaseModel: ObservableObject { - var purchaseManager: PurchaseManager - var accountManager: AccountManager - private let subscriptionAppGroup: String + var purchaseManager: StorePurchaseManager + let appStorePurchaseFlow: AppStorePurchaseFlow @Published var subscriptions: [SubscriptionRowModel] - init(manager: PurchaseManager, subscriptions: [SubscriptionRowModel] = [], subscriptionAppGroup: String) { + init(manager: StorePurchaseManager, + subscriptions: [SubscriptionRowModel] = [], + appStorePurchaseFlow: AppStorePurchaseFlow) { self.purchaseManager = manager self.subscriptions = subscriptions - self.subscriptionAppGroup = subscriptionAppGroup - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) + self.appStorePurchaseFlow = appStorePurchaseFlow } @MainActor @@ -41,7 +41,7 @@ public final class DebugPurchaseModel: ObservableObject { print("Attempting purchase: \(product.displayName)") Task { - await AppStorePurchaseFlow.purchaseSubscription(with: product.id, emailAccessToken: nil, subscriptionAppGroup: subscriptionAppGroup) + await appStorePurchaseFlow.purchaseSubscription(with: product.id, emailAccessToken: nil) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift index 1573bec52e..8cc9b5a07a 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift @@ -25,7 +25,7 @@ import Subscription @available(macOS 12.0, *) public final class DebugPurchaseViewController: NSViewController { - private let manager: PurchaseManager + private let manager: StorePurchaseManager private let model: DebugPurchaseModel private var cancellables = Set() @@ -34,9 +34,9 @@ public final class DebugPurchaseViewController: NSViewController { fatalError("init(coder:) has not been implemented") } - public init(subscriptionAppGroup: String) { - manager = PurchaseManager.shared - model = DebugPurchaseModel(manager: manager, subscriptionAppGroup: subscriptionAppGroup) + public init(storePurchaseManager: StorePurchaseManager, appStorePurchaseFlow: AppStorePurchaseFlow) { + manager = storePurchaseManager + model = DebugPurchaseModel(manager: manager, appStorePurchaseFlow: appStorePurchaseFlow) super.init(nibName: nil, bundle: nil) } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 4ab7eadd32..fd16741a1d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -21,44 +21,49 @@ import Subscription public final class SubscriptionDebugMenu: NSMenuItem { - var currentEnvironment: () -> String - var updateEnvironment: (String) -> Void + var currentEnvironment: SubscriptionEnvironment + var updateServiceEnvironment: (SubscriptionEnvironment.ServiceEnvironment) -> Void + var updatePurchasingPlatform: (SubscriptionEnvironment.PurchasePlatform) -> Void + var isInternalTestingEnabled: () -> Bool var updateInternalTestingFlag: (Bool) -> Void private var purchasePlatformItem: NSMenuItem? var currentViewController: () -> NSViewController? - private let accountManager: AccountManager - private let subscriptionAppGroup: String + let subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging { + subscriptionManager.accountManager + } private var _purchaseManager: Any? @available(macOS 12.0, *) - fileprivate var purchaseManager: PurchaseManager { + fileprivate var purchaseManager: StorePurchaseManager { if _purchaseManager == nil { - _purchaseManager = PurchaseManager() + _purchaseManager = StorePurchaseManager() } // swiftlint:disable:next force_cast - return _purchaseManager as! PurchaseManager + return _purchaseManager as! StorePurchaseManager } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public init(currentEnvironment: @escaping () -> String, - updateEnvironment: @escaping (String) -> Void, + public init(currentEnvironment: SubscriptionEnvironment, + updateServiceEnvironment: @escaping (SubscriptionEnvironment.ServiceEnvironment) -> Void, + updatePurchasingPlatform: @escaping (SubscriptionEnvironment.PurchasePlatform) -> Void, isInternalTestingEnabled: @escaping () -> Bool, updateInternalTestingFlag: @escaping (Bool) -> Void, currentViewController: @escaping () -> NSViewController?, - subscriptionAppGroup: String) { + subscriptionManager: SubscriptionManaging) { self.currentEnvironment = currentEnvironment - self.updateEnvironment = updateEnvironment + self.updateServiceEnvironment = updateServiceEnvironment + self.updatePurchasingPlatform = updatePurchasingPlatform self.isInternalTestingEnabled = isInternalTestingEnabled self.updateInternalTestingFlag = updateInternalTestingFlag self.currentViewController = currentViewController - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) - self.subscriptionAppGroup = subscriptionAppGroup + self.subscriptionManager = subscriptionManager super.init(title: "Subscription", action: nil, keyEquivalent: "") self.submenu = makeSubmenu() } @@ -105,11 +110,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { private func makePurchasePlatformSubmenu() -> NSMenu { let menu = NSMenu(title: "Select purchase platform:") - - let currentPlatform = SubscriptionPurchaseEnvironment.current - let appStoreItem = NSMenuItem(title: "App Store", action: #selector(setPlatformToAppStore), target: self) - if currentPlatform == .appStore { + if currentEnvironment.purchasePlatform == .appStore { appStoreItem.state = .on appStoreItem.isEnabled = false appStoreItem.action = nil @@ -118,7 +120,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(appStoreItem) let stripeItem = NSMenuItem(title: "Stripe", action: #selector(setPlatformToStripe), target: self) - if currentPlatform == .stripe { + if currentEnvironment.purchasePlatform == .stripe { stripeItem.state = .on stripeItem.isEnabled = false stripeItem.action = nil @@ -128,7 +130,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(.separator()) - let disclaimerItem = NSMenuItem(title: "⚠️ Change not persisted between app restarts", action: nil, target: nil) + let disclaimerItem = NSMenuItem(title: "⚠️ App restart required! The changes are persistent", action: nil, target: nil) menu.addItem(disclaimerItem) return menu @@ -137,11 +139,10 @@ public final class SubscriptionDebugMenu: NSMenuItem { private func makeEnvironmentSubmenu() -> NSMenu { let menu = NSMenu(title: "Select environment:") - let currentEnvironment = currentEnvironment() - let stagingItem = NSMenuItem(title: "Staging", action: #selector(setEnvironmentToStaging), target: self) - stagingItem.state = currentEnvironment == "staging" ? .on : .off - if currentEnvironment == "staging" { + let isStaging = currentEnvironment.serviceEnvironment == .staging + stagingItem.state = isStaging ? .on : .off + if isStaging { stagingItem.isEnabled = false stagingItem.action = nil stagingItem.target = nil @@ -149,14 +150,18 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(stagingItem) let productionItem = NSMenuItem(title: "Production", action: #selector(setEnvironmentToProduction), target: self) - productionItem.state = currentEnvironment == "production" ? .on : .off - if currentEnvironment == "production" { + let isProduction = currentEnvironment.serviceEnvironment == .production + productionItem.state = isProduction ? .on : .off + if isProduction { productionItem.isEnabled = false productionItem.action = nil productionItem.target = nil } menu.addItem(productionItem) + let disclaimerItem = NSMenuItem(title: "⚠️ App restart required! The changes are persistent", action: nil, target: nil) + menu.addItem(disclaimerItem) + return menu } @@ -178,8 +183,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { func showAccountDetails() { let title = accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" let message = accountManager.isUserAuthenticated ? ["AuthToken: \(accountManager.authToken ?? "")", - "AccessToken: \(accountManager.accessToken ?? "")", - "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil + "AccessToken: \(accountManager.accessToken ?? "")", + "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) } @@ -187,7 +192,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func validateToken() { Task { guard let token = accountManager.accessToken else { return } - switch await AuthService.validateToken(accessToken: token) { + switch await subscriptionManager.authService.validateToken(accessToken: token) { case .success(let response): showAlert(title: "Validate token", message: "\(response)") case .failure(let error): @@ -218,7 +223,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func getSubscriptionDetails() { Task { guard let token = accountManager.accessToken else { return } - switch await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { + switch await subscriptionManager.subscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { case .success(let response): showAlert(title: "Subscription info", message: "\(response)") case .failure(let error): @@ -237,10 +242,15 @@ public final class SubscriptionDebugMenu: NSMenuItem { @IBAction func showPurchaseView(_ sender: Any?) { if #available(macOS 12.0, *) { - currentViewController()?.presentAsSheet(DebugPurchaseViewController(subscriptionAppGroup: subscriptionAppGroup)) + let storePurchaseManager = StorePurchaseManager() + let appStorePurchaseFlow = AppStorePurchaseFlow(subscriptionManager: subscriptionManager) + let vc = DebugPurchaseViewController(storePurchaseManager: storePurchaseManager, appStorePurchaseFlow: appStorePurchaseFlow) + currentViewController()?.presentAsSheet(vc) } } + // MARK: - Platform + @IBAction func setPlatformToAppStore(_ sender: Any?) { askAndUpdatePlatform(to: .appStore) } @@ -249,39 +259,46 @@ public final class SubscriptionDebugMenu: NSMenuItem { askAndUpdatePlatform(to: .stripe) } - private func askAndUpdatePlatform(to newPlatform: SubscriptionPurchaseEnvironment.Environment) { + private func askAndUpdatePlatform(to newPlatform: SubscriptionEnvironment.PurchasePlatform) { let alert = makeAlert(title: "Are you sure you want to change the purchase platform to \(newPlatform.rawValue.capitalized)", - message: "This setting is not persisted between app runs. After restarting the app it returns to the default determined on app's distribution method.", + message: "This setting IS persisted between app runs. This action will close the app, do you want to proceed?", buttonNames: ["Yes", "No"]) let response = alert.runModal() - guard case .alertFirstButtonReturn = response else { return } - - SubscriptionPurchaseEnvironment.current = newPlatform - - refreshSubmenu() + updatePurchasingPlatform(newPlatform) + closeTheApp() } + // MARK: - Environment + @IBAction func setEnvironmentToStaging(_ sender: Any?) { - askAndUpdateEnvironment(to: "staging") + askAndUpdateServiceEnvironment(to: SubscriptionEnvironment.ServiceEnvironment.staging) } @IBAction func setEnvironmentToProduction(_ sender: Any?) { - askAndUpdateEnvironment(to: "production") + askAndUpdateServiceEnvironment(to: SubscriptionEnvironment.ServiceEnvironment.production) } - private func askAndUpdateEnvironment(to newEnvironmentString: String) { - let alert = makeAlert(title: "Are you sure you want to change the environment to \(newEnvironmentString.capitalized)", - message: "Please make sure you have manually removed your current active Subscription and reset all related features. \nYou may also need to change environment of related features.", + private func askAndUpdateServiceEnvironment(to newServiceEnvironment: SubscriptionEnvironment.ServiceEnvironment) { + let alert = makeAlert(title: "Are you sure you want to change the environment to \(newServiceEnvironment.description.capitalized)", + message: """ + Please make sure you have manually removed your current active Subscription and reset all related features. + You may also need to change environment of related features. + This setting IS persisted between app runs. This action will close the app, do you want to proceed? + """, buttonNames: ["Yes", "No"]) let response = alert.runModal() - guard case .alertFirstButtonReturn = response else { return } + updateServiceEnvironment(newServiceEnvironment) + closeTheApp() + } - updateEnvironment(newEnvironmentString) - refreshSubmenu() + func closeTheApp() { + NSApp.terminate(self) } + // MARK: - + @objc func postDidSignInNotification(_ sender: Any?) { NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil) @@ -296,7 +313,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { func restorePurchases(_ sender: Any?) { if #available(macOS 12.0, *) { Task { - await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + await appStoreRestoreFlow.restoreAccountFromPastPurchase() } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index f18041dace..406d632e8d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -35,11 +35,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { lazy var sheetModel: SubscriptionAccessModel = makeSubscriptionAccessModel() - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging + private var accountManager: AccountManaging { + subscriptionManager.accountManager + } private let openURLHandler: (URL) -> Void public let userEventHandler: (UserEvent) -> Void private let sheetActionHandler: SubscriptionAccessActionHandlers - private let subscriptionAppGroup: String private var fetchSubscriptionDetailsTask: Task<(), Never>? @@ -89,12 +91,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { public init(openURLHandler: @escaping (URL) -> Void, userEventHandler: @escaping (UserEvent) -> Void, sheetActionHandler: SubscriptionAccessActionHandlers, - subscriptionAppGroup: String) { - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) + subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager self.openURLHandler = openURLHandler self.userEventHandler = userEventHandler self.sheetActionHandler = sheetActionHandler - self.subscriptionAppGroup = subscriptionAppGroup self.isUserAuthenticated = accountManager.isUserAuthenticated @@ -136,9 +137,9 @@ public final class PreferencesSubscriptionModel: ObservableObject { private func makeSubscriptionAccessModel() -> SubscriptionAccessModel { if accountManager.isUserAuthenticated { - ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email, subscriptionAppGroup: subscriptionAppGroup) + ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email, subscriptionManager: subscriptionManager) } else { - ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler, shouldShowRestorePurchase: SubscriptionPurchaseEnvironment.current == .appStore) + ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler, subscriptionManager: subscriptionManager) } } @@ -149,7 +150,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func purchaseAction() { - openURLHandler(.subscriptionPurchase) + openURLHandler(subscriptionManager.url(for: .purchase)) } enum ChangePlanOrBillingAction { @@ -181,14 +182,14 @@ public final class PreferencesSubscriptionModel: ObservableObject { } } - private func changePlanOrBilling(for environment: SubscriptionPurchaseEnvironment.Environment) { + private func changePlanOrBilling(for environment: SubscriptionEnvironment.PurchasePlatform) { switch environment { case .appStore: - NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) + NSWorkspace.shared.open(subscriptionManager.url(for: .manageSubscriptionsInAppStore)) case .stripe: Task { guard let accessToken = accountManager.accessToken, let externalID = accountManager.externalID, - case let .success(response) = await SubscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } + case let .success(response) = await subscriptionManager.subscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } guard let customerPortalURL = URL(string: response.customerPortalUrl) else { return } openURLHandler(customerPortalURL) @@ -198,9 +199,8 @@ public final class PreferencesSubscriptionModel: ObservableObject { private func confirmIfSignedInToSameAccount() async -> Bool { if #available(macOS 12.0, *) { - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return false } - - switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + guard let lastTransactionJWSRepresentation = await subscriptionManager.storePurchaseManager().mostRecentTransaction() else { return false } + switch await subscriptionManager.authService.storeLogin(signature: lastTransactionJWSRepresentation) { case .success(let response): return response.externalID == accountManager.externalID case .failure: @@ -234,15 +234,16 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func openFAQ() { - openURLHandler(.subscriptionFAQ) + openURLHandler(subscriptionManager.url(for: .faq)) } @MainActor func refreshSubscriptionPendingState() { - if SubscriptionPurchaseEnvironment.current == .appStore { + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { Task { - _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + await appStoreRestoreFlow.restoreAccountFromPastPurchase() fetchAndUpdateSubscriptionDetails() } } @@ -270,11 +271,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor private func updateSubscription(with cachePolicy: SubscriptionService.CachePolicy) async { guard let token = accountManager.accessToken else { - SubscriptionService.signOut() + subscriptionManager.subscriptionService.signOut() return } - switch await SubscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) { + switch await subscriptionManager.subscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) { case .success(let subscription): updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod) subscriptionPlatform = subscription.platform @@ -285,7 +286,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { } @MainActor - private func updateAllEntitlement(with cachePolicy: AccountManager.CachePolicy) async { + private func updateAllEntitlement(with cachePolicy: AccountManaging.CachePolicy) async { switch await self.accountManager.hasEntitlement(for: .networkProtection, cachePolicy: cachePolicy) { case let .success(result): hasAccessToVPN = result diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift index 3e14765641..5d7517bb58 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift @@ -20,10 +20,10 @@ import Foundation import Subscription public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, PurchaseRestoringSubscriptionAccessModel { - private var actionHandlers: SubscriptionAccessActionHandlers + private var actionHandlers: SubscriptionAccessActionHandlers public var title = UserText.activateModalTitle - public var description = UserText.activateModalDescription(platform: SubscriptionPurchaseEnvironment.current) + public let description: String public var email: String? public var emailLabel: String { UserText.email } @@ -34,13 +34,18 @@ public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, Pur public var restorePurchaseDescription = UserText.restorePurchaseDescription public var restorePurchaseButtonTitle = UserText.restorePurchaseButton - public init(actionHandlers: SubscriptionAccessActionHandlers, shouldShowRestorePurchase: Bool) { + let subscriptionManager: SubscriptionManaging + + public init(actionHandlers: SubscriptionAccessActionHandlers, + subscriptionManager: SubscriptionManaging) { self.actionHandlers = actionHandlers - self.shouldShowRestorePurchase = shouldShowRestorePurchase + self.shouldShowRestorePurchase = subscriptionManager.currentEnvironment.purchasePlatform == .appStore + self.subscriptionManager = subscriptionManager + self.description = UserText.activateModalDescription(platform: subscriptionManager.currentEnvironment.purchasePlatform) } public func handleEmailAction() { - actionHandlers.openURLHandler(.activateSubscriptionViaEmail) + actionHandlers.openURLHandler(subscriptionManager.url(for: .activateViaEmail)) actionHandlers.uiActionHandler(.activateAddEmailClick) } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index 9e007f4e7e..d67abee49c 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -20,27 +20,28 @@ import Foundation import Subscription public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { - public var title = UserText.shareModalTitle - public var description = UserText.shareModalDescription(platform: SubscriptionPurchaseEnvironment.current) - private let subscriptionAppGroup: String + public var title = UserText.shareModalTitle + public let description: String private var actionHandlers: SubscriptionAccessActionHandlers - public var email: String? public var emailLabel: String { UserText.email } public var emailDescription: String { hasEmail ? UserText.shareModalHasEmailDescription : UserText.shareModalNoEmailDescription } public var emailButtonTitle: String { hasEmail ? UserText.manageEmailButton : UserText.addEmailButton } + private let subscriptionManager: SubscriptionManaging - public init(actionHandlers: SubscriptionAccessActionHandlers, email: String?, subscriptionAppGroup: String) { + public init(actionHandlers: SubscriptionAccessActionHandlers, email: String?, subscriptionManager: SubscriptionManaging) { self.actionHandlers = actionHandlers self.email = email - self.subscriptionAppGroup = subscriptionAppGroup + self.subscriptionManager = subscriptionManager + self.description = UserText.shareModalDescription(platform: subscriptionManager.currentEnvironment.purchasePlatform) } private var hasEmail: Bool { !(email?.isEmpty ?? true) } public func handleEmailAction() { - let url: URL = hasEmail ? .manageSubscriptionEmail : .addEmailToSubscription + let type = hasEmail ? SubscriptionURL.manageEmail : SubscriptionURL.addEmail + let mailURL: URL = subscriptionManager.url(for: type) if hasEmail { actionHandlers.uiActionHandler(.postSubscriptionAddEmailClick) @@ -49,14 +50,15 @@ public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { } Task { - if SubscriptionPurchaseEnvironment.current == .appStore { + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, iOS 15.0, *) { - await AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded(subscriptionAppGroup: subscriptionAppGroup) + let appStoreAccountManagementFlow = AppStoreAccountManagementFlow(subscriptionManager: subscriptionManager) + await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() } } DispatchQueue.main.async { - self.actionHandlers.openURLHandler(url) + self.actionHandlers.openURLHandler(mailURL) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift index 3833c60ff0..f08c95cfb0 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift @@ -22,18 +22,17 @@ import SwiftUI public final class SubscriptionAccessViewController: NSViewController { - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging private var actionHandlers: SubscriptionAccessActionHandlers - private let subscriptionAppGroup: String public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public init(accountManager: AccountManager, actionHandlers: SubscriptionAccessActionHandlers, subscriptionAppGroup: String) { - self.accountManager = accountManager + public init(subscriptionManager: SubscriptionManaging, + actionHandlers: SubscriptionAccessActionHandlers) { + self.subscriptionManager = subscriptionManager self.actionHandlers = actionHandlers - self.subscriptionAppGroup = subscriptionAppGroup super.init(nibName: nil, bundle: nil) } @@ -56,10 +55,10 @@ public final class SubscriptionAccessViewController: NSViewController { } private func makeSubscriptionAccessModel() -> SubscriptionAccessModel { - if accountManager.isUserAuthenticated { - ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email, subscriptionAppGroup: subscriptionAppGroup) + if subscriptionManager.accountManager.isUserAuthenticated { + ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: subscriptionManager.accountManager.email, subscriptionManager: subscriptionManager) } else { - ActivateSubscriptionAccessModel(actionHandlers: actionHandlers, shouldShowRestorePurchase: SubscriptionPurchaseEnvironment.current == .appStore) + ActivateSubscriptionAccessModel(actionHandlers: actionHandlers, subscriptionManager: subscriptionManager) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index f917d58b71..5e470bf2f8 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -101,7 +101,7 @@ enum UserText { // MARK: - Activate subscription modal static let activateModalTitle = NSLocalizedString("subscription.activate.modal.title", value: "Activate your subscription on this device", comment: "Activate subscription modal view title") - static func activateModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { + static func activateModalDescription(platform: SubscriptionEnvironment.PurchasePlatform) -> String { switch platform { case .appStore: NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") @@ -115,7 +115,7 @@ enum UserText { // MARK: - Share subscription modal static let shareModalTitle = NSLocalizedString("subscription.share.modal.title", value: "Use your subscription on other devices", comment: "Share subscription modal view title") - static func shareModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { + static func shareModalDescription(platform: SubscriptionEnvironment.PurchasePlatform) -> String { switch platform { case .appStore: NSLocalizedString("subscription.appstore.share.modal.description", value: "Access your subscription via Apple ID or by adding an email address.", comment: "Share subscription modal view subtitle description") diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index f9680be11d..f6d7e42781 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -21,6 +21,7 @@ import NetworkProtection import NetworkProtectionUI import XCTest import Subscription +import SubscriptionTestingUtilities @testable import DuckDuckGo_Privacy_Browser @@ -29,17 +30,8 @@ final class MoreOptionsMenuTests: XCTestCase { var tabCollectionViewModel: TabCollectionViewModel! var passwordManagerCoordinator: PasswordManagerCoordinator! var capturingActionDelegate: CapturingOptionsButtonMenuDelegate! - @MainActor - lazy var moreOptionMenu: MoreOptionsMenu! = { - let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: networkProtectionVisibilityMock, - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - menu.actionDelegate = capturingActionDelegate - return menu - }() - + var accountManager: AccountManagerMock! + var moreOptionsMenu: MoreOptionsMenu! var internalUserDecider: InternalUserDeciderMock! var networkProtectionVisibilityMock: NetworkProtectionVisibilityMock! @@ -51,8 +43,15 @@ final class MoreOptionsMenuTests: XCTestCase { passwordManagerCoordinator = PasswordManagerCoordinator() capturingActionDelegate = CapturingOptionsButtonMenuDelegate() internalUserDecider = InternalUserDeciderMock() - + accountManager = AccountManagerMock(isUserAuthenticated: true) networkProtectionVisibilityMock = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) + moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, + passwordManagerCoordinator: passwordManagerCoordinator, + networkProtectionFeatureVisibility: networkProtectionVisibilityMock, + sharingMenu: NSMenu(), + internalUserDecider: internalUserDecider, + accountManager: accountManager) + moreOptionsMenu.actionDelegate = capturingActionDelegate } @MainActor @@ -60,51 +59,77 @@ final class MoreOptionsMenuTests: XCTestCase { tabCollectionViewModel = nil passwordManagerCoordinator = nil capturingActionDelegate = nil - moreOptionMenu = nil + moreOptionsMenu = nil + accountManager = nil super.tearDown() } @MainActor - func testThatMoreOptionMenuHasTheExpectedItems() { - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, + func testThatMoreOptionMenuHasTheExpectedItemsAuthenticated() { + moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - - XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) - XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[2].title, UserText.plusButtonNewTabMenuItem) - XCTAssertEqual(moreOptionMenu.items[3].title, UserText.newWindowMenuItem) - XCTAssertEqual(moreOptionMenu.items[4].title, UserText.newBurnerWindowMenuItem) - XCTAssertTrue(moreOptionMenu.items[5].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[6].title, UserText.zoom) - XCTAssertTrue(moreOptionMenu.items[7].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[8].title, UserText.bookmarks) - XCTAssertEqual(moreOptionMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionMenu.items[10].title, UserText.passwordManagement) - XCTAssertTrue(moreOptionMenu.items[11].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[12].title, UserText.emailOptionsMenuItem) - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.networkProtection)) - XCTAssertTrue(moreOptionMenu.items[15].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) - XCTAssertTrue(moreOptionMenu.items[16].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[17].title, UserText.settings) - } else { - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.networkProtection)) - XCTAssertTrue(moreOptionMenu.items[15].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[16].title, UserText.settings) - } + internalUserDecider: internalUserDecider, + accountManager: accountManager) + + XCTAssertEqual(moreOptionsMenu.items[0].title, UserText.sendFeedback) + XCTAssertTrue(moreOptionsMenu.items[1].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[2].title, UserText.plusButtonNewTabMenuItem) + XCTAssertEqual(moreOptionsMenu.items[3].title, UserText.newWindowMenuItem) + XCTAssertEqual(moreOptionsMenu.items[4].title, UserText.newBurnerWindowMenuItem) + XCTAssertTrue(moreOptionsMenu.items[5].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[6].title, UserText.zoom) + 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) + XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) + + XCTAssertTrue(moreOptionsMenu.items[13].isSeparatorItem) + XCTAssertTrue(moreOptionsMenu.items[14].title.hasPrefix(UserText.networkProtection)) + XCTAssertTrue(moreOptionsMenu.items[15].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) + XCTAssertTrue(moreOptionsMenu.items[16].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[17].title, UserText.settings) + } + + @MainActor + func testThatMoreOptionMenuHasTheExpectedItemsNotAuthenticated() { + + accountManager = AccountManagerMock(isUserAuthenticated: false) + moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, + passwordManagerCoordinator: passwordManagerCoordinator, + networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), + sharingMenu: NSMenu(), + internalUserDecider: internalUserDecider, + accountManager: accountManager) + + XCTAssertEqual(moreOptionsMenu.items[0].title, UserText.sendFeedback) + XCTAssertTrue(moreOptionsMenu.items[1].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[2].title, UserText.plusButtonNewTabMenuItem) + XCTAssertEqual(moreOptionsMenu.items[3].title, UserText.newWindowMenuItem) + XCTAssertEqual(moreOptionsMenu.items[4].title, UserText.newBurnerWindowMenuItem) + XCTAssertTrue(moreOptionsMenu.items[5].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[6].title, UserText.zoom) + 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) + XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) + + XCTAssertTrue(moreOptionsMenu.items[13].isSeparatorItem) + XCTAssertTrue(moreOptionsMenu.items[14].title.hasPrefix(UserText.networkProtection)) + XCTAssertTrue(moreOptionsMenu.items[15].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[16].title, UserText.settings) } // MARK: Zoom @MainActor func testWhenClickingDefaultZoomInZoomSubmenuThenTheActionDelegateIsAlerted() { - guard let zoomSubmenu = moreOptionMenu.zoomMenuItem.submenu else { + guard let zoomSubmenu = moreOptionsMenu.zoomMenuItem.submenu else { XCTFail("No zoom submenu available") return } @@ -116,19 +141,16 @@ final class MoreOptionsMenuTests: XCTestCase { } // MARK: Preferences - - @MainActor func testWhenClickingOnPreferenceMenuItemThenTheActionDelegateIsAlerted() { - moreOptionMenu.performActionForItem(at: moreOptionMenu.items.count - 1) + moreOptionsMenu.performActionForItem(at: moreOptionsMenu.items.count - 1) XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedPreferencesCalled) } // MARK: - Bookmarks - @MainActor func testWhenClickingOnBookmarkAllTabsMenuItemThenTheActionDelegateIsAlerted() throws { // GIVEN - let bookmarksMenu = try XCTUnwrap(moreOptionMenu.item(at: 8)?.submenu) + let bookmarksMenu = try XCTUnwrap(moreOptionsMenu.item(at: 8)?.submenu) let bookmarkAllTabsIndex = try XCTUnwrap(bookmarksMenu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) let bookmarkAllTabsMenuItem = try XCTUnwrap(bookmarksMenu.items[bookmarkAllTabsIndex]) bookmarkAllTabsMenuItem.isEnabled = true diff --git a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift index 814abc7a42..3f7d7da067 100644 --- a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift +++ b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift @@ -17,7 +17,8 @@ // import XCTest -import Subscription +@testable import Subscription +import SubscriptionTestingUtilities @testable import DuckDuckGo_Privacy_Browser final class SubscriptionRedirectManagerTests: XCTestCase { @@ -25,8 +26,11 @@ final class SubscriptionRedirectManagerTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true) - SubscriptionPurchaseEnvironment.canPurchase = true + sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true, + subscriptionEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore), + baseURL: SubscriptionURL.baseURL.subscriptionURL(environment: .production), + canPurchase: { true }) } override func tearDownWithError() throws { @@ -37,7 +41,8 @@ final class SubscriptionRedirectManagerTests: XCTestCase { func testWhenURLIsPrivacyProAndHasOriginQueryParameterThenRedirectToSubscriptionBaseURLAndAppendQueryParameter() throws { // GIVEN let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro?origin=test")) - let expectedURL = URL.subscriptionBaseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) + let baseURL = SubscriptionURL.baseURL.subscriptionURL(environment: .production) + let expectedURL = baseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) // WHEN let result = sut.redirectURL(for: url) @@ -49,7 +54,7 @@ final class SubscriptionRedirectManagerTests: XCTestCase { func testWhenURLIsPrivacyProAndDoesNotHaveOriginQueryParameterThenRedirectToSubscriptionBaseURL() throws { // GIVEN let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro")) - let expectedURL = URL.subscriptionBaseURL + let expectedURL = SubscriptionURL.baseURL.subscriptionURL(environment: .production) // WHEN let result = sut.redirectURL(for: url) diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 975116e54b..ea46a83f31 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -17,8 +17,7 @@ // import XCTest -import Subscription - +@testable import Subscription @testable import DuckDuckGo_Privacy_Browser @MainActor @@ -228,7 +227,8 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.closeButton = mouseButton // Update url - let tab = Tab(content: .subscription(.subscriptionPurchase)) + let url = SubscriptionURL.purchase.subscriptionURL(environment: .production) + let tab = Tab(content: .subscription(url)) delegate.mockedCurrentTab = tab let vm = TabViewModel(tab: tab) tabBarViewItem.subscribe(to: vm, tabCollectionViewModel: TabCollectionViewModel()) From c226cf6d1a04a2b1614771d8d91144ac2cc68092 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 23 May 2024 09:13:03 +0200 Subject: [PATCH 13/42] move permanent survey card to first position (#2804) Task/Issue URL: https://app.asana.com/0/1201048563534612/1207354538419159/f **Description**: Make the permanent survey first card --- .../HomePage/Model/HomePageContinueSetUpModel.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index 22ddf43e7e..df7ea99ccb 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -286,12 +286,10 @@ extension HomePage.Models { } var randomisedFeatures: [FeatureType] { - var features = FeatureType.allCases - features.shuffle() - for (index, feature) in features.enumerated() where feature == .defaultBrowser { - features.remove(at: index) - features.insert(feature, at: 0) - } + var features: [FeatureType] = [.permanentSurvey, .defaultBrowser] + var shuffledFeatures = FeatureType.allCases.filter { $0 != .defaultBrowser && $0 != .permanentSurvey } + shuffledFeatures.shuffle() + features.append(contentsOf: shuffledFeatures) return features } From 2104521e66c7b9e872e19268e73a634043d581d9 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 23 May 2024 11:26:49 +0100 Subject: [PATCH 14/42] Check for entitlement in DBP agent (#2802) Task/Issue URL: https://app.asana.com/0/1201011656765697/1206366654222841/f **Description**: Deactivates the DBP agent if entitlements are invalid --- DuckDuckGo/Application/AppDelegate.swift | 13 +- .../DataBrokerProtectionPixelsHandler.swift | 6 +- ...erProtectionSubscriptionEventHandler.swift | 43 +++- ...rokerProtectionEntitlementMonitoring.swift | 56 +++++ .../Pixels/DataBrokerProtectionPixels.swift | 19 +- .../DataBrokerProtectionAgentManager.swift | 43 ++-- .../DataBrokerProtectionAgentStopper.swift | 110 ++++++++++ ...ataBrokerProtectionAgentManagerTests.swift | 98 +++++++-- ...ataBrokerProtectionAgentStopperTests.swift | 198 ++++++++++++++++++ .../DataBrokerProtectionTests/Mocks.swift | 36 +++- 10 files changed, 570 insertions(+), 52 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionEntitlementMonitoring.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index f0811eca7f..2b734be8f9 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -94,7 +94,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler? #if DBP - private var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler? + private lazy var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler = { + let authManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: subscriptionManager) + return DataBrokerProtectionSubscriptionEventHandler(featureDisabler: DataBrokerProtectionFeatureDisabler(), + authenticationManager: authManager, + pixelHandler: DataBrokerProtectionPixelsHandler()) + }() + #endif private var didFinishLaunching = false @@ -216,9 +222,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager, tunnelController: tunnelController, vpnUninstaller: vpnUninstaller) -#if DBP - dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager) -#endif } // swiftlint:disable:next function_body_length @@ -310,7 +313,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { UNUserNotificationCenter.current().delegate = self #if DBP - dataBrokerProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() + dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() #endif #if DBP diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 494d141be5..3632dc549b 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -98,7 +98,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping - init(subscriptionManager: SubscriptionManaging, - authRepository: AuthenticationRepository = KeychainAuthenticationData(), - featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { - self.subscriptionManager = subscriptionManager - self.authRepository = authRepository + init(featureDisabler: DataBrokerProtectionFeatureDisabling, + authenticationManager: DataBrokerProtectionAuthenticationManaging, + pixelHandler: EventMapping) { self.featureDisabler = featureDisabler + self.authenticationManager = authenticationManager + self.pixelHandler = pixelHandler } func registerForSubscriptionAccountManagerEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(handleAccountDidSignOut), + name: .accountDidSignOut, + object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(entitlementsDidChange), + name: .entitlementsDidChange, + object: nil) } @objc private func handleAccountDidSignOut() { featureDisabler.disableAndDelete() } + + @objc private func entitlementsDidChange() { + Task { @MainActor in + do { + if try await authenticationManager.hasValidEntitlement() { + pixelHandler.fire(.entitlementCheckValid) + } else { + pixelHandler.fire(.entitlementCheckInvalid) + featureDisabler.disableAndDelete() + } + } catch { + /// We don't want to disable the agent in case of an error while checking for entitlements. + /// Since this is a destructive action, the only situation that should cause the data to be deleted and the agent to be removed is .success(false) + pixelHandler.fire(.entitlementCheckError) + assertionFailure("Error validating entitlement \(error)") + } + } + } } #endif diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionEntitlementMonitoring.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionEntitlementMonitoring.swift new file mode 100644 index 0000000000..77b6d883b2 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionEntitlementMonitoring.swift @@ -0,0 +1,56 @@ +// +// DataBrokerProtectionEntitlementMonitoring.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol DataBrokerProtectionEntitlementMonitoring { + func start(checkEntitlementFunction: @escaping () async throws -> Bool, interval: TimeInterval, callback: @escaping (DataBrokerProtectionEntitlementMonitorResult) -> Void) + func stop() +} + +public enum DataBrokerProtectionEntitlementMonitorResult { + case enabled + case disabled + case error +} + +final class DataBrokerProtectionEntitlementMonitor: DataBrokerProtectionEntitlementMonitoring { + private var timer: Timer? + + func start(checkEntitlementFunction: @escaping () async throws -> Bool, interval: TimeInterval, callback: @escaping (DataBrokerProtectionEntitlementMonitorResult) -> Void) { + timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in + Task { + do { + switch try await checkEntitlementFunction() { + case true: + callback(.enabled) + case false: + callback(.disabled) + } + } catch { + callback(.error) + } + } + } + } + + func stop() { + timer?.invalidate() + timer = nil + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index f83d3c6aeb..45f954a4d4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -164,6 +164,11 @@ public enum DataBrokerProtectionPixels { case initialScanSiteLoadDuration(duration: Double, hasError: Bool, brokerURL: String, sleepDuration: Double) case initialScanPostLoadingDuration(duration: Double, hasError: Bool, brokerURL: String, sleepDuration: Double) case initialScanPreStartDuration(duration: Double) + + // Entitlements + case entitlementCheckValid + case entitlementCheckInvalid + case entitlementCheckError } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -267,6 +272,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .initialScanSiteLoadDuration: return "m_mac_dbp_scan_broker_site_loaded" case .initialScanPostLoadingDuration: return "m_mac_dbp_initial_scan_broker_post_loading" case .initialScanPreStartDuration: return "m_mac_dbp_initial_scan_pre_start_duration" + + // Entitlements + 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" } } @@ -361,7 +371,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .homeViewShowBadPathError, .homeViewCTAMoveApplicationClicked, .homeViewCTAGrantPermissionClicked, - + .entitlementCheckValid, + .entitlementCheckInvalid, + .entitlementCheckError, .secureVaultInitError, .secureVaultError: return [:] @@ -486,7 +498,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping + private let agentStopper: DataBrokerProtectionAgentStopper // Used for debug functions only, so not injected private lazy var browserWindowManager = BrowserWindowManager() @@ -111,7 +119,9 @@ public final class DataBrokerProtectionAgentManager { queueManager: DataBrokerProtectionQueueManager, dataManager: DataBrokerProtectionDataManaging, operationDependencies: DataBrokerOperationDependencies, - pixelHandler: EventMapping) { + pixelHandler: EventMapping, + agentStopper: DataBrokerProtectionAgentStopper + ) { self.userNotificationService = userNotificationService self.activityScheduler = activityScheduler self.ipcServer = ipcServer @@ -119,6 +129,7 @@ public final class DataBrokerProtectionAgentManager { self.dataManager = dataManager self.operationDependencies = operationDependencies self.pixelHandler = pixelHandler + self.agentStopper = agentStopper self.activityScheduler.delegate = self self.ipcServer.serverDelegate = self @@ -127,20 +138,20 @@ public final class DataBrokerProtectionAgentManager { public func agentFinishedLaunching() { - do { - // If there's no saved profile we don't need to start the scheduler - // Theoretically this should never happen, if there's no data, the agent shouldn't be running - guard (try dataManager.fetchProfile()) != nil else { - return - } - } catch { - os_log("Error during AgentManager.agentFinishedLaunching when trying to fetchProfile, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - return + Task { @MainActor in + // The browser shouldn't start the agent if these prerequisites aren't met. + // However, since the agent can auto-start after a reboot without the browser, we need to validate it again. + // If the agent needs to be stopped, this function will stop it, so the subsequent calls after it will not be made. + await agentStopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + activityScheduler.startScheduler() + didStartActivityScheduler = true + queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + + /// Monitors entitlement changes every 60 minutes to optimize system performance and resource utilization by avoiding unnecessary operations when entitlement is invalid. + /// While keeping the agent active with invalid entitlement has no significant risk, setting the monitoring interval at 60 minutes is a good balance to minimize backend checks. + agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: .minutes(60)) } - - activityScheduler.startScheduler() - didStartActivityScheduler = true - queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift new file mode 100644 index 0000000000..fe6c7734be --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift @@ -0,0 +1,110 @@ +// +// DataBrokerProtectionAgentStopper.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 Common + +protocol DataBrokerProtectionAgentStopper { + /// Validates if the user has profile data, is authenticated, and has valid entitlement. If any of these conditions are not met, the agent will be stopped. + func validateRunPrerequisitesAndStopAgentIfNecessary() async + + /// Monitors the entitlement package. If the entitlement check returns false, the agent will be stopped. + /// This function ensures that the agent is stopped if the user's subscription has expired, even if the browser is not active. Regularly checking for entitlement is required since notifications are not posted to agents. + func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) +} + +struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper { + private let dataManager: DataBrokerProtectionDataManaging + private let entitlementMonitor: DataBrokerProtectionEntitlementMonitoring + private let authenticationManager: DataBrokerProtectionAuthenticationManaging + private let pixelHandler: EventMapping + private let stopAction: DataProtectionStopAction + + init(dataManager: DataBrokerProtectionDataManaging, + entitlementMonitor: DataBrokerProtectionEntitlementMonitoring, + authenticationManager: DataBrokerProtectionAuthenticationManaging, + pixelHandler: EventMapping, + stopAction: DataProtectionStopAction = DefaultDataProtectionStopAction()) { + self.dataManager = dataManager + self.entitlementMonitor = entitlementMonitor + self.authenticationManager = authenticationManager + self.pixelHandler = pixelHandler + self.stopAction = stopAction + } + + public func validateRunPrerequisitesAndStopAgentIfNecessary() async { + do { + guard try dataManager.fetchProfile() != nil, + authenticationManager.isUserAuthenticated else { + os_log("Prerequisites are invalid", log: .dataBrokerProtection) + stopAgent() + return + } + os_log("Prerequisites are valid", log: .dataBrokerProtection) + } catch { + os_log("Error validating prerequisites, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + stopAgent() + } + + do { + let result = try await authenticationManager.hasValidEntitlement() + stopAgentBasedOnEntitlementCheckResult(result ? .enabled : .disabled) + } catch { + stopAgentBasedOnEntitlementCheckResult(.error) + } + } + + public func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) { + entitlementMonitor.start(checkEntitlementFunction: authenticationManager.hasValidEntitlement, + interval: interval) { result in + stopAgentBasedOnEntitlementCheckResult(result) + } + } + + private func stopAgent() { + stopAction.stopAgent() + } + + private func stopAgentBasedOnEntitlementCheckResult(_ result: DataBrokerProtectionEntitlementMonitorResult) { + switch result { + case .enabled: + os_log("Valid entitlement", log: .dataBrokerProtection) + pixelHandler.fire(.entitlementCheckValid) + case .disabled: + os_log("Invalid entitlement", log: .dataBrokerProtection) + pixelHandler.fire(.entitlementCheckInvalid) + stopAgent() + case .error: + os_log("Error when checking entitlement", log: .dataBrokerProtection) + /// We don't want to disable the agent in case of an error while checking for entitlements. + /// Since this is a destructive action, the only situation that should cause the data to be deleted and the agent to be removed is .success(false) + pixelHandler.fire(.entitlementCheckError) + } + } +} + +protocol DataProtectionStopAction { + func stopAgent() +} + +struct DefaultDataProtectionStopAction: DataProtectionStopAction { + func stopAgent() { + os_log("Stopping DataBrokerProtection Agent", log: .dataBrokerProtection) + exit(EXIT_SUCCESS) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift index f4d00a5d5c..185e21f2f8 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift @@ -31,12 +31,14 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { private var mockPixelHandler: MockPixelHandler! private var mockDependencies: DefaultDataBrokerOperationDependencies! private var mockProfile: DataBrokerProtectionProfile! + private var mockAgentStopper: MockAgentStopper! override func setUpWithError() throws { mockPixelHandler = MockPixelHandler() mockActivityScheduler = MockDataBrokerProtectionBackgroundActivityScheduler() mockNotificationService = MockUserNotificationService() + mockAgentStopper = MockAgentStopper() let mockDatabase = MockDatabase() let mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) @@ -75,30 +77,42 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockDataManager.profileToReturn = mockProfile + let schedulerStartedExpectation = XCTestExpectation(description: "Scheduler started") var schedulerStarted = false mockActivityScheduler.startSchedulerCompletion = { schedulerStarted = true + schedulerStartedExpectation.fulfill() } + let scanCalledExpectation = XCTestExpectation(description: "Scan called") var startScheduledScansCalled = false mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true + scanCalledExpectation.fulfill() } // When sut.agentFinishedLaunching() // Then + await fulfillment(of: [scanCalledExpectation, schedulerStartedExpectation], timeout: 1.0) XCTAssertTrue(schedulerStarted) XCTAssertTrue(startScheduledScansCalled) } - func testWhenAgentStart_andProfileDoesNotExist_thenActivityIsNotScheduled_andSheduledOpereationsNotRun() async throws { + func testWhenAgentStart_andProfileDoesNotExist_thenActivityIsNotScheduled_andStopAgentIsCalled() async throws { // Given + let mockStopAction = MockDataProtectionStopAction() + let agentStopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: DataBrokerProtectionEntitlementMonitor(), + authenticationManager: MockAuthenticationManager(), + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, activityScheduler: mockActivityScheduler, @@ -106,26 +120,64 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: agentStopper) mockDataManager.profileToReturn = nil - var schedulerStarted = false - mockActivityScheduler.startSchedulerCompletion = { - schedulerStarted = true + let stopAgentExpectation = XCTestExpectation(description: "Stop agent expectation") + + var stopAgentWasCalled = false + mockStopAction.stopAgentCompletion = { + stopAgentWasCalled = true + stopAgentExpectation.fulfill() } - var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in - startScheduledScansCalled = true + // When + sut.agentFinishedLaunching() + await fulfillment(of: [stopAgentExpectation], timeout: 1.0) + + // Then + XCTAssertTrue(stopAgentWasCalled) + } + + func testWhenAgentStart_thenPrerequisitesAreValidated_andEntitlementsAreMonitored() async { + // Given + let mockAgentStopper = MockAgentStopper() + + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) + + mockDataManager.profileToReturn = nil + + let preRequisitesExpectation = XCTestExpectation(description: "preRequisitesExpectation expectation") + var runPrerequisitesWasCalled = false + mockAgentStopper.validateRunPrerequisitesCompletion = { + runPrerequisitesWasCalled = true + preRequisitesExpectation.fulfill() + } + + let monitorEntitlementExpectation = XCTestExpectation(description: "monitorEntitlement expectation") + var monitorEntitlementWasCalled = false + mockAgentStopper.monitorEntitlementCompletion = { + monitorEntitlementWasCalled = true + monitorEntitlementExpectation.fulfill() } // When sut.agentFinishedLaunching() + await fulfillment(of: [preRequisitesExpectation, monitorEntitlementExpectation], timeout: 1.0) // Then - XCTAssertFalse(schedulerStarted) - XCTAssertFalse(startScheduledScansCalled) + XCTAssertTrue(runPrerequisitesWasCalled) + XCTAssertTrue(monitorEntitlementWasCalled) } func testWhenActivitySchedulerTriggers_thenSheduledOpereationsRun() async throws { @@ -137,7 +189,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockDataManager.profileToReturn = mockProfile @@ -162,7 +215,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockDataManager.profileToReturn = mockProfile @@ -187,7 +241,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockNotificationService.reset() @@ -207,7 +262,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockNotificationService.reset() @@ -227,7 +283,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockNotificationService.reset() mockQueueManager.startImmediateOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) @@ -248,7 +305,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = true @@ -269,7 +327,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = false @@ -290,7 +349,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { queueManager: mockQueueManager, dataManager: mockDataManager, operationDependencies: mockDependencies, - pixelHandler: mockPixelHandler) + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper) var startScheduledScansCalled = false mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift new file mode 100644 index 0000000000..5d8c25ebee --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift @@ -0,0 +1,198 @@ +// +// DataBrokerProtectionAgentStopperTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +import Common + +@testable import DataBrokerProtection + +final class DataBrokerProtectionAgentStopperTests: XCTestCase { + private var mockPixelHandler: EventMapping! + private var mockAuthenticationManager: MockAuthenticationManager! + private var mockEntitlementMonitor: DataBrokerProtectionEntitlementMonitor! + private var mockDataManager: MockDataBrokerProtectionDataManager! + private var mockStopAction: MockDataProtectionStopAction! + + private var fakeProfile: DataBrokerProtectionProfile { + let name = DataBrokerProtectionProfile.Name(firstName: "John", lastName: "Doe") + let address = DataBrokerProtectionProfile.Address(city: "City", state: "State") + + return DataBrokerProtectionProfile(names: [name], addresses: [address], phones: [String](), birthYear: 1900) + } + + override func setUp() { + mockPixelHandler = MockDataBrokerProtectionPixelsHandler() + mockAuthenticationManager = MockAuthenticationManager() + mockPixelHandler = MockPixelHandler() + mockEntitlementMonitor = DataBrokerProtectionEntitlementMonitor() + mockDataManager = MockDataBrokerProtectionDataManager(pixelHandler: mockPixelHandler, + fakeBrokerFlag: DataBrokerDebugFlagFakeBroker()) + mockStopAction = MockDataProtectionStopAction() + } + + override func tearDown() { + mockPixelHandler = nil + mockAuthenticationManager = nil + mockPixelHandler = nil + mockEntitlementMonitor = nil + mockDataManager = nil + mockStopAction = nil + } + + func testNoProfile_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testUserNotAuthenticated_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testErrorEntitlement_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.shouldThrowEntitlementError = true + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + + func testValidEntitlement_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + + func testEntitlementMonitorWithValidResult_thenStopAgentIsNotCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in + XCTAssertFalse(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testEntitlementMonitorWithInValidResult_thenStopAgentIsCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in + XCTAssertTrue(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testEntitlementMonitorWithErrorResult_thenStopAgentIsNotCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.shouldThrowEntitlementError = true + mockDataManager.profileToReturn = fakeProfile + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in + XCTAssertFalse(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index c4b2804e3f..d6bb74149d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -1374,13 +1374,18 @@ final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManagin var shouldAskForInviteCodeValue = false var redeemCodeCalled = false var authHeaderValue: String? = "fake auth header" + var hasValidEntitlementValue = false + var shouldThrowEntitlementError = false var isUserAuthenticated: Bool { isUserAuthenticatedValue } var accessToken: String? { accessTokenValue } func hasValidEntitlement() async throws -> Bool { - return true + if shouldThrowEntitlementError { + throw NSError(domain: "duck.com", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error"]) + } + return hasValidEntitlementValue } func shouldAskForInviteCode() -> Bool { shouldAskForInviteCodeValue } @@ -1397,5 +1402,34 @@ final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManagin shouldAskForInviteCodeValue = false redeemCodeCalled = false authHeaderValue = "fake auth header" + hasValidEntitlementValue = false + shouldThrowEntitlementError = false + } +} + +final class MockAgentStopper: DataBrokerProtectionAgentStopper { + var validateRunPrerequisitesCompletion: (() -> Void)? + var monitorEntitlementCompletion: (() -> Void)? + + func validateRunPrerequisitesAndStopAgentIfNecessary() async { + validateRunPrerequisitesCompletion?() + } + + func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) { + monitorEntitlementCompletion?() + } +} + +final class MockDataProtectionStopAction: DataProtectionStopAction { + var wasStopCalled = false + var stopAgentCompletion: (() -> Void)? + + func stopAgent() { + wasStopCalled = true + stopAgentCompletion?() + } + + func reset() { + wasStopCalled = false } } From 8ab6aaddfb1c225994164b484f135e7263a61c97 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Thu, 23 May 2024 12:32:42 +0200 Subject: [PATCH 15/42] Subscription refactoring, BSK update (#2809) Task/Issue URL: https://app.asana.com/0/72649045549333/1206805455884775/f Tech Design URL: https://app.asana.com/0/481882893211075/1207147511614062/f BSK update --- DuckDuckGo.xcodeproj/project.pbxproj | 78 +++++++++---------- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 42 insertions(+), 50 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4c1f200d8..08e3fe56ea 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -169,7 +169,6 @@ 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 3158B14D2B0BF74D00AF130C /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.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 */; }; 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; @@ -221,7 +220,6 @@ 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; - 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */; }; 31EF1E832B63FFCA00E6DB17 /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; }; 31EF1E842B63FFD100E6DB17 /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */; }; @@ -2611,10 +2609,18 @@ F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; F1B33DF72BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; + F1C70D792BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */; }; + F1C70D7A2BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */; }; + F1C70D7C2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D7D2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D7E2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D7F2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D802BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D812BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D822BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1C70D832BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428D2BFB9F9C00A31506 /* Subscription */; }; F1D042902BFB9FA300A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428F2BFB9FA300A31506 /* Subscription */; }; - F1D042912BFB9FD700A31506 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; - F1D042922BFB9FD800A31506 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; @@ -2655,12 +2661,6 @@ F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 537FC71EA5115A983FAF3170 /* Crashes */; }; F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = DC3F73D49B2D44464AFEFCD8 /* Subscription */; }; F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = F1DF95E62BD188B60045E591 /* LoginItems */; }; - F1FDC9292BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; - F1FDC92A2BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; - F1FDC92B2BF4DFEC006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; - F1FDC92C2BF4DFED006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; - F1FDC92D2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; - F1FDC92E2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; F1FDC9382BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; @@ -3392,7 +3392,6 @@ 7B4D8A202BDA857300852966 /* VPNOperationErrorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOperationErrorRecorder.swift; sourceTree = ""; }; 7B5291882A1697680022E406 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeveloperID.xcconfig; sourceTree = ""; }; - 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemInterface.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 = ""; }; @@ -4138,12 +4137,13 @@ F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppStoreRestorer.swift; sourceTree = ""; }; F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionErrorReporter.swift; sourceTree = ""; }; + F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemInterface.swift; sourceTree = ""; }; + F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataBrokerProtectionSettings+Environment.swift"; sourceTree = ""; }; F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionManager+StandardConfiguration.swift"; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; - F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VPNSettings+Environment.swift"; sourceTree = ""; }; F41D174025CB131900472416 /* NSColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSColorExtension.swift; sourceTree = ""; }; F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAppearanceExtension.swift; sourceTree = ""; }; @@ -4625,21 +4625,21 @@ 3192EC862A4DCF0E001E97A5 /* DBP */ = { isa = PBXGroup; children = ( - 3169132B2BD2C7960051B46D /* ErrorView */, - 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */, + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */, + 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */, 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, - 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, - 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, - 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemInterface.swift */, - 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */, - 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, + BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, - 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */, + 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, + F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */, 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */, - BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, + 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */, BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */, + 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, + 3169132B2BD2C7960051B46D /* ErrorView */, + 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */, 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */, - 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */, ); path = DBP; sourceTree = ""; @@ -8292,10 +8292,10 @@ F118EA7B2BEA2B8700F77634 /* Subscription */ = { isa = PBXGroup; children = ( + F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */, F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */, F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */, - F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */, F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */, F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */, F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */, @@ -9836,7 +9836,6 @@ 3706FB43293F65D500E42796 /* DuckPlayer.swift in Sources */, 3706FB44293F65D500E42796 /* Favicon.swift in Sources */, 3706FB45293F65D500E42796 /* SuggestionContainerViewModel.swift in Sources */, - F1FDC92A2BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 3706FB46293F65D500E42796 /* FirePopoverWrapperViewController.swift in Sources */, 3706FB47293F65D500E42796 /* NSPasteboardItemExtension.swift in Sources */, 3706FB48293F65D500E42796 /* AutofillPreferencesModel.swift in Sources */, @@ -9893,6 +9892,7 @@ 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, + F1C70D7A2BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, @@ -10185,6 +10185,7 @@ 4B9DB03F2A983B24000927DB /* JoinWaitlistView.swift in Sources */, 987799F22999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, 37A6A8F72AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, + F1C70D7D2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 3706FC2C293F65D500E42796 /* BookmarkHTMLReader.swift in Sources */, 3706FC2D293F65D500E42796 /* Tab+NSSecureCoding.swift in Sources */, 3706FC2E293F65D500E42796 /* NSNotificationName+EmailManager.swift in Sources */, @@ -10201,7 +10202,6 @@ 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, B6ABD0CB2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, - 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, B6B140892ABDBCC1004F8E85 /* HoverTrackingArea.swift in Sources */, 3706FC36293F65D500E42796 /* LongPressButton.swift in Sources */, 3706FC37293F65D500E42796 /* CoreDataStore.swift in Sources */, @@ -10735,7 +10735,6 @@ EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, F1DA51932BF6081D00CF29FA /* AttributionPixelHandler.swift in Sources */, - F1FDC92C2BF4DFED006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, F1DA51892BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, @@ -10749,6 +10748,7 @@ 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, + F1C70D7F2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, @@ -10782,7 +10782,7 @@ 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, - F1FDC92D2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, + F1C70D802BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -10825,7 +10825,7 @@ 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, - F1FDC92E2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, + F1C70D812BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -10878,6 +10878,7 @@ B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, + F1C70D7E2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, F1DA51922BF6081C00CF29FA /* AttributionPixelHandler.swift in Sources */, @@ -10885,7 +10886,6 @@ 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, - F1FDC92B2BF4DFEC006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, F1DA51882BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, ); @@ -10939,10 +10939,7 @@ files = ( 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, - 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - F1D042912BFB9FD700A31506 /* SubscriptionEnvironment+Default.swift in Sources */, - 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, - 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, + F1C70D822BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, @@ -10957,10 +10954,7 @@ files = ( 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, - 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, - F1D042922BFB9FD800A31506 /* SubscriptionEnvironment+Default.swift in Sources */, - 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, - 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, + F1C70D832BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, @@ -11107,6 +11101,7 @@ 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */, 4B8AC93926B48A5100879451 /* FirefoxLoginReader.swift in Sources */, F18826902BC0105800D9AC4F /* PixelDataRecord.swift in Sources */, + F1C70D792BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift in Sources */, F18826912BC0105800D9AC4F /* PixelDataStore.swift in Sources */, B69B503E2726A12500758A2B /* AtbParser.swift in Sources */, 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, @@ -11160,10 +11155,7 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, - 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, - 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, @@ -11537,6 +11529,7 @@ 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, + F1C70D7C2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */, @@ -11645,7 +11638,6 @@ AAC82C60258B6CB5009B6B42 /* TabPreviewWindowController.swift in Sources */, AAC5E4E425D6BA9C007F5990 /* NSSizeExtension.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, - F1FDC9292BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, @@ -13034,8 +13026,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - branch = fcappelli/subscription_refactoring_2; - kind = branch; + kind = exactVersion; + version = 146.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 981f226c05..7c7e84edb5 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" : { - "branch" : "fcappelli/subscription_refactoring_2", - "revision" : "874ae4269db821797742655e134e72199c2813c8" + "revision" : "b01a7ba359b650f0c5c3ab00a756e298b1ae650c", + "version" : "146.0.0" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "46989693916f56d1186bd59ac15124caef896560", - "version" : "1.3.1" + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index b199cd1256..203c5dda21 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6bdacb0bff..9b10b4534f 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.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 cd969a3ce6..210d28971a 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 00c70b4aadcc14f352d8abc4b2bd3911a215dc10 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 23 May 2024 12:28:52 +0100 Subject: [PATCH 16/42] Increase test timeout (#2810) Task/Issue URL: https://app.asana.com/0/1205237866452338/1207390952710955/f Tech Design URL: CC: **Description**: Increase test timeout --- .../DataBrokerProtectionAgentStopperTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift index 5d8c25ebee..3271a31f69 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift @@ -144,12 +144,12 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { let expectation = XCTestExpectation(description: "Wait for monitor") stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertFalse(mockStopAction.wasStopCalled) expectation.fulfill() } - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 3) } func testEntitlementMonitorWithInValidResult_thenStopAgentIsCalled() { @@ -166,12 +166,12 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { let expectation = XCTestExpectation(description: "Wait for monitor") stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertTrue(mockStopAction.wasStopCalled) expectation.fulfill() } - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 3) } func testEntitlementMonitorWithErrorResult_thenStopAgentIsNotCalled() { @@ -188,11 +188,11 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { let expectation = XCTestExpectation(description: "Wait for monitor") stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertFalse(mockStopAction.wasStopCalled) expectation.fulfill() } - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 3) } } From 03bc5dc325b13f5f2a355fc45481ec42d0b117a9 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Thu, 23 May 2024 07:20:11 -0500 Subject: [PATCH 17/42] Add mylife data broker (#2786) Task/Issue URL: https://app.asana.com/0/1206873150423133/1207316296802490/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Resources/JSON/mylife.com.json | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/mylife.com.json diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/mylife.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/mylife.com.json new file mode 100644 index 0000000000..5d26c83a07 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/mylife.com.json @@ -0,0 +1,140 @@ +{ + "name": "mylife", + "url": "mylife.com", + "version": "0.0.1", + "addedDatetime": 1715797497496, + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "31285970-27bd-4ec6-a4c1-afc5fb501624", + "url": "https://www.mylife.com/pub-multisearch.pubview?searchFirstName=${firstName}&searchLastName=${lastName}&searchLocation=${city}%2C+${state|upcase}&whyReg=peoplesearch&whySub=Member+Profile+Sub&pageType=ps" + }, + { + "actionType": "extract", + "id": "9a08be56-d596-48e5-8745-0574b541e9df", + "selector": ".ais-InfiniteHits-item", + "profile": { + "name": { + "selector": ".hit-name", + "beforeText": "," + }, + "alternativeNamesList": { + "selector": ".hit-akas .hit-values", + "findElements": true + }, + "age": { + "selector": ".hit-name", + "afterText": "," + }, + "addressCityState": { + "selector": ".hit-location" + }, + "addressCityStateList": { + "selector": ".hit-pastAddresses .hit-values", + "findElements": true + }, + "profileUrl": { + "selector": ".hit-btn-lg", + "identifierType": "path", + "identifier": "https://www.mylife.com/${firstName}-${lastName}/${id}" + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "formOptOut", + "actions": [ + { + "actionType": "navigate", + "id": "8c7e8f10-5bd2-4dba-b99a-d198b3c0bbc9", + "url": "https://www.mylife.com/ccpa/index.pubview" + }, + { + "actionType": "fillForm", + "id": "1cd0b2b7-7203-4ae1-9d22-d76aa6de8a26", + "selector": "//form", + "dataSource": "userProfile", + "elements": [ + { + "type": "firstName", + "selector": ".//input[@name='firstname']" + }, + { + "type": "lastName", + "selector": ".//input[@name='lastname']" + }, + { + "type": "city", + "selector": ".//input[@name='city']" + }, + { + "type": "state", + "selector": ".//input[@name='state']" + } + ] + }, + { + "actionType": "fillForm", + "id": "ed570894-ebbe-4f9c-a9f7-9d58e81bdc28", + "selector": "//form", + "elements": [ + { + "type": "email", + "selector": ".//input[@name='emailAddress']" + }, + { + "type": "$generated_zip_code$", + "selector": ".//input[@name='zipcode']" + }, + { + "type": "profileUrl", + "selector": ".//input[@name='profileUrl']" + } + ] + }, + { + "actionType": "getCaptchaInfo", + "id": "aeda8b17-92cf-43ce-8974-12a13fb9bcfd", + "selector": ".g-recaptcha" + }, + { + "actionType": "solveCaptcha", + "id": "6b8a962e-19ed-4f33-8c56-4f4a1f17cad3", + "selector": ".g-recaptcha" + }, + { + "actionType": "click", + "id": "6cb0e6f4-e881-4937-872e-29627223bdb8", + "elements": [ + { + "type": "button", + "selector": ".//input[@type='submit']" + } + ] + }, + { + "actionType": "expectation", + "id": "fe7201a7-4e92-4ad3-90fb-d836019d71e0", + "expectations": [ + { + "type": "text", + "selector": "#successRequest", + "expect": "Your request has been received." + } + ] + } + ] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } +} From d826ec2899d752267e4b5fde918be052121d041d Mon Sep 17 00:00:00 2001 From: amddg44 Date: Thu, 23 May 2024 16:45:42 +0200 Subject: [PATCH 18/42] New autofill save & update password prompt pixels for alignment with iOS (#2801) Task/Issue URL: https://app.asana.com/0/72649045549333/1207361449520263/f Tech Design URL: CC: Description: New pixels to bring macOS password management pixel reporting up to date with iOS --- .../View/SaveCredentialsViewController.swift | 104 ++++++++++++++++++ DuckDuckGo/Statistics/GeneralPixel.swift | 44 ++++++++ .../Tab/ViewModel/TabViewModelTests.swift | 2 +- 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index 02a5595240..e9f565bb9b 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -63,6 +63,12 @@ final class SaveCredentialsViewController: NSViewController { @IBOutlet var fireproofCheck: NSButton! @IBOutlet weak var fireproofCheckDescription: NSTextFieldCell! + private enum Action { + case displayed + case confirmed + case dismissed + } + weak var delegate: SaveCredentialsDelegate? private var credentials: SecureVaultModels.WebsiteCredentials? @@ -139,6 +145,9 @@ final class SaveCredentialsViewController: NSViewController { // Only use the non-editable state if a credential was automatically saved and it didn't already exist. let condition = credentials.account.id != nil && !(credentials.account.username?.isEmpty ?? true) && automaticallySaved updateViewState(editable: !condition) + + let existingCredentials = getExistingCredentialsFrom(credentials) + evaluateCredentialsAndFirePixels(for: .displayed, credentials: existingCredentials) } private func updateViewState(editable: Bool) { @@ -208,6 +217,7 @@ final class SaveCredentialsViewController: NSViewController { domain: domainLabel.stringValue) account.id = credentials?.account.id let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) + let existingCredentials = getExistingCredentialsFrom(credentials) do { if passwordManagerCoordinator.isEnabled { @@ -231,6 +241,8 @@ final class SaveCredentialsViewController: NSViewController { PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) } + evaluateCredentialsAndFirePixels(for: .confirmed, credentials: existingCredentials) + PixelKit.fire(GeneralPixel.autofillItemSaved(kind: .password)) if passwordManagerCoordinator.isEnabled { @@ -250,6 +262,9 @@ final class SaveCredentialsViewController: NSViewController { @IBAction func onDontUpdateClicked(_ sender: Any) { delegate?.shouldCloseSaveCredentialsViewController(self) + + let existingCredentials = getExistingCredentialsFrom(credentials) + evaluateCredentialsAndFirePixels(for: .dismissed, credentials: existingCredentials) } @IBAction func onNotNowSegmentedControlClicked(_ sender: Any) { @@ -280,6 +295,9 @@ final class SaveCredentialsViewController: NSViewController { delegate?.shouldCloseSaveCredentialsViewController(self) } + let existingCredentials = getExistingCredentialsFrom(credentials) + evaluateCredentialsAndFirePixels(for: .dismissed, credentials: existingCredentials) + guard DataClearingPreferences.shared.isLoginDetectionEnabled else { notifyDelegate() return @@ -365,4 +383,90 @@ final class SaveCredentialsViewController: NSViewController { } } + private func getExistingCredentialsFrom(_ credentials: SecureVaultModels.WebsiteCredentials?) -> SecureVaultModels.WebsiteCredentials? { + guard let credentials = credentials, let id = credentials.account.id else { + return nil + } + + var existingCredentials: SecureVaultModels.WebsiteCredentials? + + if passwordManagerCoordinator.isEnabled { + guard !passwordManagerCoordinator.isLocked else { + os_log("Failed to access credentials: Password manager is locked") + return existingCredentials + } + + passwordManagerCoordinator.websiteCredentialsFor(accountId: id) { credentials, _ in + existingCredentials = credentials + } + } else { + if let idInt = Int64(id) { + existingCredentials = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).websiteCredentialsFor(accountId: idInt) + } + } + + return existingCredentials + } + + private func isUsernameUpdated(credentials: SecureVaultModels.WebsiteCredentials) -> Bool { + if credentials.account.username != self.usernameField.stringValue.trimmingWhitespace() { + return true + } + return false + } + + private func isPasswordUpdated(credentials: SecureVaultModels.WebsiteCredentials) -> Bool { + if credentials.password != self.passwordData { + return true + } + return false + } + + private func evaluateCredentialsAndFirePixels(for action: Action, credentials: SecureVaultModels.WebsiteCredentials?) { + switch action { + case .displayed: + if let credentials = credentials { + if isPasswordUpdated(credentials: credentials) { + PixelKit.fire(GeneralPixel.autofillLoginsUpdatePasswordInlineDisplayed) + } else { + PixelKit.fire(GeneralPixel.autofillLoginsUpdateUsernameInlineDisplayed) + } + } else { + if usernameField.stringValue.trimmingWhitespace().isEmpty { + PixelKit.fire(GeneralPixel.autofillLoginsSavePasswordInlineDisplayed) + } else { + PixelKit.fire(GeneralPixel.autofillLoginsSaveLoginInlineDisplayed) + } + } + case .confirmed, .dismissed: + if let credentials = credentials { + if isUsernameUpdated(credentials: credentials) { + firePixel(for: action, + confirmedPixel: GeneralPixel.autofillLoginsUpdateUsernameInlineConfirmed, + dismissedPixel: GeneralPixel.autofillLoginsUpdateUsernameInlineDismissed) + } + if isPasswordUpdated(credentials: credentials) { + firePixel(for: action, + confirmedPixel: GeneralPixel.autofillLoginsUpdatePasswordInlineConfirmed, + dismissedPixel: GeneralPixel.autofillLoginsUpdatePasswordInlineDismissed) + } + } else { + if usernameField.stringValue.trimmingWhitespace().isEmpty { + firePixel(for: action, + confirmedPixel: GeneralPixel.autofillLoginsSavePasswordInlineConfirmed, + dismissedPixel: GeneralPixel.autofillLoginsSavePasswordInlineDismissed) + } else { + firePixel(for: action, + confirmedPixel: GeneralPixel.autofillLoginsSaveLoginInlineConfirmed, + dismissedPixel: GeneralPixel.autofillLoginsSaveLoginInlineDismissed) + } + } + } + } + + private func firePixel(for action: Action, confirmedPixel: PixelKitEventV2, dismissedPixel: PixelKitEventV2) { + let pixel = action == .confirmed ? confirmedPixel : dismissedPixel + PixelKit.fire(pixel) + } + } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index d5846e38a7..fd7e8dafa3 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -40,11 +40,27 @@ enum GeneralPixel: PixelKitEventV2 { case formAutofilled(kind: FormAutofillKind) case autofillItemSaved(kind: FormAutofillKind) + case autofillLoginsSaveLoginInlineDisplayed + case autofillLoginsSaveLoginInlineConfirmed + case autofillLoginsSaveLoginInlineDismissed + + case autofillLoginsSavePasswordInlineDisplayed + case autofillLoginsSavePasswordInlineConfirmed + case autofillLoginsSavePasswordInlineDismissed + case autofillLoginsSaveLoginModalExcludeSiteConfirmed case autofillLoginsSettingsResetExcludedDisplayed case autofillLoginsSettingsResetExcludedConfirmed case autofillLoginsSettingsResetExcludedDismissed + case autofillLoginsUpdatePasswordInlineDisplayed + case autofillLoginsUpdatePasswordInlineConfirmed + case autofillLoginsUpdatePasswordInlineDismissed + + case autofillLoginsUpdateUsernameInlineDisplayed + case autofillLoginsUpdateUsernameInlineConfirmed + case autofillLoginsUpdateUsernameInlineDismissed + case bitwardenPasswordAutofilled case bitwardenPasswordSaved @@ -361,6 +377,20 @@ enum GeneralPixel: PixelKitEventV2 { case .autofillItemSaved(kind: let kind): return "m_mac_save_\(kind)" + case .autofillLoginsSaveLoginInlineDisplayed: + return "m_mac_autofill_logins_save_login_inline_displayed" + case .autofillLoginsSaveLoginInlineConfirmed: + return "m_mac_autofill_logins_save_login_inline_confirmed" + case .autofillLoginsSaveLoginInlineDismissed: + return "m_mac_autofill_logins_save_login_inline_dismissed" + + case .autofillLoginsSavePasswordInlineDisplayed: + return "m_mac_autofill_logins_save_password_inline_displayed" + case .autofillLoginsSavePasswordInlineConfirmed: + return "m_mac_autofill_logins_save_password_inline_confirmed" + case .autofillLoginsSavePasswordInlineDismissed: + return "m_mac_autofill_logins_save_password_inline_dismissed" + case .autofillLoginsSaveLoginModalExcludeSiteConfirmed: return "m_mac_autofill_logins_save_login_exclude_site_confirmed" case .autofillLoginsSettingsResetExcludedDisplayed: @@ -370,6 +400,20 @@ enum GeneralPixel: PixelKitEventV2 { case .autofillLoginsSettingsResetExcludedDismissed: return "m_mac_autofill_settings_reset_excluded_dismissed" + case .autofillLoginsUpdatePasswordInlineDisplayed: + return "m_mac_autofill_logins_update_password_inline_displayed" + case .autofillLoginsUpdatePasswordInlineConfirmed: + return "m_mac_autofill_logins_update_password_inline_confirmed" + case .autofillLoginsUpdatePasswordInlineDismissed: + return "m_mac_autofill_logins_update_password_inline_dismissed" + + case .autofillLoginsUpdateUsernameInlineDisplayed: + return "m_mac_autofill_logins_update_username_inline_displayed" + case .autofillLoginsUpdateUsernameInlineConfirmed: + return "m_mac_autofill_logins_update_username_inline_confirmed" + case .autofillLoginsUpdateUsernameInlineDismissed: + return "m_mac_autofill_logins_update_username_inline_dismissed" + case .bitwardenPasswordAutofilled: return "m_mac_bitwarden_autofill_password" diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index c2e83d6fbb..4828527900 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -294,7 +294,7 @@ final class TabViewModelTests: XCTestCase { let filteredCases = DefaultZoomValue.allCases.filter { $0 != AccessibilityPreferences.shared.defaultPageZoom } let randomZoomLevel = filteredCases.randomElement()! AccessibilityPreferences.shared.updateZoomPerWebsite(zoomLevel: randomZoomLevel, url: hostURL) - var tab = Tab(url: url) + let tab = Tab(url: url) var tabVM = TabViewModel(tab: tab) // WHEN From 04bb8e2b1799f0b23fbc94f3466372d62d61d9c5 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 23 May 2024 15:51:35 +0100 Subject: [PATCH 19/42] Add History to iOS (updated UI and rollout) (#2770) Task/Issue URL: https://app.asana.com/0/72649045549333/1205122649889514/f Tech Design URL: CC: **Description**: Tweak suggestions for iOS **Steps to test this PR**: 1. Build and run the app 2. Use history and ensure that it works as before --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../xcschemes/sandbox-test-tool.xcscheme | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Model/HistoryCoordinatorTests.swift | 272 ------------------ 7 files changed, 8 insertions(+), 286 deletions(-) delete mode 100644 UnitTests/History/Model/HistoryCoordinatorTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 08e3fe56ea..5d81a1af21 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -864,7 +864,6 @@ 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99D0C26FE1A83001E4761 /* ChromiumBookmarksReaderTests.swift */; }; 3706FE6E293F661700E42796 /* FirefoxBookmarksReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99D0D26FE1A83001E4761 /* FirefoxBookmarksReaderTests.swift */; }; 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B117F7C276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift */; }; - 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B32642C69300C2EFBC /* HistoryCoordinatorTests.swift */; }; 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F52837CBA800D1D4AA /* SavedStateMock.swift */; }; 3706FE72293F661700E42796 /* ClickToLoadTDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA8AE769279FBDB20078943E /* ClickToLoadTDSTests.swift */; }; 3706FE73293F661700E42796 /* PermissionManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0D926AE7AF400A9DAD1 /* PermissionManagerMock.swift */; }; @@ -1981,7 +1980,6 @@ AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */; }; - AAEC74B42642C69300C2EFBC /* HistoryCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B32642C69300C2EFBC /* HistoryCoordinatorTests.swift */; }; AAEC74B62642CC6A00C2EFBC /* HistoryStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B52642CC6A00C2EFBC /* HistoryStoringMock.swift */; }; AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B72642E43800C2EFBC /* HistoryStoreTests.swift */; }; AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAECA41F24EEA4AC00EFA63A /* IndexPathExtension.swift */; }; @@ -3754,7 +3752,6 @@ AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewViewController.swift; sourceTree = ""; }; AAE99B8827088A19008B6BD9 /* FirePopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopover.swift; sourceTree = ""; }; AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatingMock.swift; sourceTree = ""; }; - AAEC74B32642C69300C2EFBC /* HistoryCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatorTests.swift; sourceTree = ""; }; AAEC74B52642CC6A00C2EFBC /* HistoryStoringMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStoringMock.swift; sourceTree = ""; }; AAEC74B72642E43800C2EFBC /* HistoryStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStoreTests.swift; sourceTree = ""; }; AAECA41F24EEA4AC00EFA63A /* IndexPathExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexPathExtension.swift; sourceTree = ""; }; @@ -7626,7 +7623,6 @@ isa = PBXGroup; children = ( AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */, - AAEC74B32642C69300C2EFBC /* HistoryCoordinatorTests.swift */, ); path = Model; sourceTree = ""; @@ -10598,7 +10594,6 @@ 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, - 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, 9F3344632BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, 3706FE72293F661700E42796 /* ClickToLoadTDSTests.swift in Sources */, @@ -12012,7 +12007,6 @@ 4BB99D1026FE1A84001E4761 /* FirefoxBookmarksReaderTests.swift in Sources */, 9FBD84702BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, 4B117F7D276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift in Sources */, - AAEC74B42642C69300C2EFBC /* HistoryCoordinatorTests.swift in Sources */, 378205F62837CBA800D1D4AA /* SavedStateMock.swift in Sources */, 3783F92329432E1800BCA897 /* WebViewTests.swift in Sources */, EA8AE76A279FBDB20078943E /* ClickToLoadTDSTests.swift in Sources */, @@ -13027,7 +13021,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 146.0.0; + version = 146.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 7c7e84edb5..913e081687 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" : "b01a7ba359b650f0c5c3ab00a756e298b1ae650c", - "version" : "146.0.0" + "revision" : "d18f97300d105ec5b8fb5fbe54659c661dea6158", + "version" : "146.0.1" } }, { @@ -174,7 +174,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", "state" : { "revision" : "c01e6a59d000356b58ec77053e0a99d538be56a5", "version" : "2.1.1" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index eb7e5e26bb..41730d7069 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> Date: Thu, 23 May 2024 16:00:12 +0100 Subject: [PATCH 20/42] Make profile selector optional (#2811) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207389625725611/f **Description**: Make profile selector optional --- .../Sources/DataBrokerProtection/Model/ExtractedProfile.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index aa6158bd24..5bf40900ae 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -19,7 +19,7 @@ import Foundation struct ProfileSelector: Codable { - let selector: String + let selector: String? let findElements: Bool? let afterText: String? let beforeText: String? From 41a9790206f610fc6fb57d0ac8199ac52f45979b Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Thu, 23 May 2024 10:54:56 -0500 Subject: [PATCH 21/42] Bump BSK (#2807) Task/Issue URL: https://app.asana.com/0/608920331025329/1207360142555446/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5d81a1af21..ca8c019e43 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13021,7 +13021,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 146.0.1; + version = 146.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 913e081687..4a74ac2fd1 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" : "d18f97300d105ec5b8fb5fbe54659c661dea6158", - "version" : "146.0.1" + "revision" : "65f3ccadd0118bcef112e3f5fff03e491b3261cd", + "version" : "146.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "bb8e7e62104ed6506c7bfd3ef7aa4aca3686ed4f", - "version" : "5.15.0" + "revision" : "fa861c4eccb21d235e34070b208b78bdc32ece08", + "version" : "5.17.0" } }, { @@ -174,7 +174,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "c01e6a59d000356b58ec77053e0a99d538be56a5", "version" : "2.1.1" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 9b62cd2cb8..5c788ad63f 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: "146.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 517322718a..24722fef2b 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: "146.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 925a70c283..a33802799a 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: "146.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From a3d04ea917dab3ded23019cd85cb3d9a5798ec14 Mon Sep 17 00:00:00 2001 From: Sean Barag Date: Thu, 23 May 2024 10:28:44 -0700 Subject: [PATCH 22/42] autofill: don't prefix autofill email pixels with `m.mac.` (#2808) Task/Issue URL: https://app.asana.com/0/1206682621538333/1207380134103391/f Tech Design URL: N/A Pixels fired for email autofill use-cases are intentionally not prefixed with `m.mac.`, though the historical reason isn't clear to me. The migration to PixelKit for pixel management[^1] caused those email-related pixels to be prefixed. Wrap email-related JS pixels in `NonStandardEvent`s to prevent that prefixing, to restore their original intended names. [^1]: d885fd02d (PixelKit adoption (#2557), 2024-04-17) --- DuckDuckGo/Autofill/ContentOverlayViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 63a6a88368..5033f96ac5 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -325,7 +325,7 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { self.emailManager.updateLastUseDate() - PixelKit.fire(GeneralPixel.jsPixel(pixel), withAdditionalParameters: pixelParameters) + PixelKit.fire(NonStandardEvent(GeneralPixel.jsPixel(pixel)), withAdditionalParameters: pixelParameters) } else { PixelKit.fire(GeneralPixel.jsPixel(pixel), withAdditionalParameters: pixel.pixelParameters) } From 22905cde55fb3cb9359046f0d3a5ebebebb4f141 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Thu, 23 May 2024 21:45:08 +0200 Subject: [PATCH 23/42] Autofill engagement KPIs for pixel reporting (#2806) Task/Issue URL: https://app.asana.com/0/72649045549333/1207357107981852/f Tech Design URL: CC: Description: New Autofill engagement KPI pixels --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/Application/AppDelegate.swift | 24 +++++++++++++++++++ .../ContentOverlayViewController.swift | 5 ++++ .../SecureVaultLoginImporter.swift | 4 ++++ DuckDuckGo/Menus/MainMenuActions.swift | 3 +++ .../PasswordManagementViewController.swift | 1 + .../View/SaveCredentialsViewController.swift | 2 ++ .../Statistics/ATB/StatisticsLoader.swift | 1 + DuckDuckGo/Statistics/GeneralPixel.swift | 17 +++++++++++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- UnitTests/DataExport/MockSecureVault.swift | 8 +++++++ 14 files changed, 71 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ca8c019e43..6143c3a653 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13021,7 +13021,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 146.1.0; + version = 146.2.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4a74ac2fd1..6dd57e77a0 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" : "65f3ccadd0118bcef112e3f5fff03e491b3261cd", - "version" : "146.1.0" + "revision" : "e1e436422bc167933baa0f90838958f2ac7119f3", + "version" : "146.2.0" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 2b734be8f9..4ef995e23e 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -76,6 +76,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let featureFlagger: FeatureFlagger private var appIconChanger: AppIconChanger! private var autoClearHandler: AutoClearHandler! + private(set) var autofillPixelReporter: AutofillPixelReporter? private(set) var syncDataProviders: SyncDataProviders! private(set) var syncService: DDGSyncing? @@ -321,6 +322,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif setUpAutoClearHandler() + + setUpAutofillPixelReporter() } func applicationDidBecomeActive(_ notification: Notification) { @@ -587,6 +590,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } } + + private func setUpAutofillPixelReporter() { + autofillPixelReporter = AutofillPixelReporter( + userDefaults: .standard, + eventMapping: EventMapping {event, _, params, _ in + switch event { + case .autofillActiveUser: + PixelKit.fire(GeneralPixel.autofillActiveUser) + case .autofillEnabledUser: + PixelKit.fire(GeneralPixel.autofillEnabledUser) + case .autofillOnboardedUser: + PixelKit.fire(GeneralPixel.autofillOnboardedUser) + case .autofillLoginsStacked: + PixelKit.fire(GeneralPixel.autofillLoginsStacked, withAdditionalParameters: params) + case .autofillCreditCardsStacked: + PixelKit.fire(GeneralPixel.autofillCreditCardsStacked, withAdditionalParameters: params) + } + }, + passwordManager: PasswordManagerCoordinator.shared, + installDate: AppDelegate.firstLaunchDate) + } } extension AppDelegate: UNUserNotificationCenterDelegate { diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 5033f96ac5..12a087e0ad 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -296,6 +296,7 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { public func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: String) { PixelKit.fire(GeneralPixel.formAutofilled(kind: type.formAutofillKind)) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) if type.formAutofillKind == .password && passwordManagerCoordinator.isEnabled { @@ -326,7 +327,11 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { self.emailManager.updateLastUseDate() PixelKit.fire(NonStandardEvent(GeneralPixel.jsPixel(pixel)), withAdditionalParameters: pixelParameters) + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) } else { + if pixel.isIdentityPixel { + NotificationCenter.default.post(name: .autofillFillEvent, object: nil) + } PixelKit.fire(GeneralPixel.jsPixel(pixel), withAdditionalParameters: pixel.pixelParameters) } } diff --git a/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift b/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift index 193806cef0..eb3cdd03d9 100644 --- a/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift +++ b/DuckDuckGo/DataImport/Logins/SecureVault/SecureVaultLoginImporter.swift @@ -60,6 +60,10 @@ final class SecureVaultLoginImporter: LoginImporter { } } + if successful.count > 0 { + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) + } + return .init(successful: successful.count, duplicate: duplicates.count, failed: failed.count) } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 103b90bc81..cef3e05a90 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -743,6 +743,9 @@ extension MainViewController { try? vault?.deleteNoteFor(noteId: noteID) } UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageContinueSetUpImport.rawValue) + + let autofillPixelReporter = AutofillPixelReporter(userDefaults: .standard, eventMapping: EventMapping { _, _, _, _ in }, installDate: nil) + autofillPixelReporter.resetStoreDefaults() } @objc func resetBookmarks(_ sender: Any?) { diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index f18a2478de..bb8bd46046 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -539,6 +539,7 @@ final class PasswordManagementViewController: NSViewController { refetchWithText(searchField.stringValue) { [weak self] in self?.syncModelsOnCredentials(savedCredentials, select: true) } + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) } else { syncModelsOnCredentials(savedCredentials) } diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index e9f565bb9b..0d7bc4bc6d 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -241,6 +241,8 @@ final class SaveCredentialsViewController: NSViewController { PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) } + NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil) + evaluateCredentialsAndFirePixels(for: .confirmed, credentials: existingCredentials) PixelKit.fire(GeneralPixel.autofillItemSaved(kind: .password)) diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 19e7c70108..1d3cfb1be3 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -167,6 +167,7 @@ final class StatisticsLoader { if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.statisticsStore.searchRetentionAtb = atb.version self.storeUpdateVersionIfPresent(atb) + NotificationCenter.default.post(name: .searchDAU, object: nil, userInfo: nil) } completion() diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index fd7e8dafa3..6184263d85 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -61,6 +61,12 @@ enum GeneralPixel: PixelKitEventV2 { case autofillLoginsUpdateUsernameInlineConfirmed case autofillLoginsUpdateUsernameInlineDismissed + case autofillActiveUser + case autofillEnabledUser + case autofillOnboardedUser + case autofillLoginsStacked + case autofillCreditCardsStacked + case bitwardenPasswordAutofilled case bitwardenPasswordSaved @@ -414,6 +420,17 @@ enum GeneralPixel: PixelKitEventV2 { case .autofillLoginsUpdateUsernameInlineDismissed: return "m_mac_autofill_logins_update_username_inline_dismissed" + case .autofillActiveUser: + return "m_mac_autofill_activeuser" + case .autofillEnabledUser: + return "m_mac_autofill_enableduser" + case .autofillOnboardedUser: + return "m_mac_autofill_onboardeduser" + case .autofillLoginsStacked: + return "m_mac_autofill_logins_stacked" + case .autofillCreditCardsStacked: + return "m_mac_autofill_creditcards_stacked" + case .bitwardenPasswordAutofilled: return "m_mac_bitwarden_autofill_password" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5c788ad63f..1fa39e2681 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: "146.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.2.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 24722fef2b..041e3d6cd1 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: "146.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.2.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index a33802799a..57655ffe08 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: "146.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "146.2.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/DataExport/MockSecureVault.swift b/UnitTests/DataExport/MockSecureVault.swift index 5f07ac42f2..97cacafde5 100644 --- a/UnitTests/DataExport/MockSecureVault.swift +++ b/UnitTests/DataExport/MockSecureVault.swift @@ -161,6 +161,10 @@ final class MockSecureVault: AutofillSecureVault { return storedCards } + func creditCardsCount() throws -> Int { + return storedCards.count + } + func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? { return storedCards.first { $0.id == id } } @@ -408,6 +412,10 @@ class MockDatabaseProvider: AutofillDatabaseProvider { return Array(_creditCards.values) } + func creditCardsCount() throws -> Int { + return _creditCards.count + } + func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { return _creditCards[cardId] } From f47861498fb4e65893464353ee69de82b4b8b13d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 23 May 2024 22:19:31 -0700 Subject: [PATCH 24/42] BSK bump for iOS RMF updates (#2798) Task/Issue URL: https://app.asana.com/0/1193060753475688/1207234800675206/f Tech Design URL: CC: Description: This PR updates macOS for the BSK RMF changes. These changes are not used by any component of the macOS app. --- 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 6143c3a653..ba8f275f98 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13021,7 +13021,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 146.2.0; + version = 147.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 6dd57e77a0..bd2dc2a20d 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" : "e1e436422bc167933baa0f90838958f2ac7119f3", - "version" : "146.2.0" + "revision" : "610a58a77fefe82f8541d4a7f998ef2a4609a068", + "version" : "147.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 1fa39e2681..f55ac724a6 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: "146.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "147.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 041e3d6cd1..e9ad0dbc90 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: "146.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "147.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 57655ffe08..3dcac72550 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: "146.2.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "147.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 89627db1fe109339c7b5507d53d9c02260893c7c Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 24 May 2024 14:07:16 +0200 Subject: [PATCH 25/42] Privacy Pro macOS quick follow ups (#2813) Task/Issue URL: https://app.asana.com/0/1204099484721401/1207013882115696/f Description: Small copy fixes --- .../SubscriptionUI/Sources/SubscriptionUI/UserText.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 5e470bf2f8..6321b3cc4f 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -76,7 +76,7 @@ enum UserText { // MARK: Preferences when subscription activation is pending static let preferencesSubscriptionPendingHeader = NSLocalizedString("subscription.preferences.subscription.pending.header", value: "Your subscription is being activated", comment: "Header for the subscription preferences pane when the subscription activation is pending") - static let preferencesSubscriptionPendingCaption = NSLocalizedString("subscription.preferences.subscription.pending.caption", value: "This is taking longer than usual, please check back later.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") + static let preferencesSubscriptionPendingCaption = NSLocalizedString("subscription.preferences.subscription.pending.caption", value: "This is taking longer than usual. Please check back later.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") // MARK: Preferences when subscription is expired static let preferencesSubscriptionExpiredCaption = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Subscribe again to continue using Privacy Pro.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") @@ -87,7 +87,7 @@ enum UserText { // MARK: - Change plan or billing dialogs static let changeSubscriptionDialogTitle = NSLocalizedString("subscription.dialog.change.title", value: "Change Plan or Billing", comment: "Change plan or billing dialog title") static let changeSubscriptionGoogleDialogDescription = NSLocalizedString("subscription.dialog.change.google.description", value: "Your subscription was purchased through the Google Play Store. To change your plan or billing settings, please open Google Play Store subscription settings on a device signed in to the same Google Account used to purchase your subscription.", comment: "Change plan or billing dialog subtitle description for subscription purchased via Google") - static let changeSubscriptionAppleDialogDescription = NSLocalizedString("subscription.dialog.change.apple.description", value: "Your subscription was purchased through the Apple App Store. To change your plan or billing settings, please go to Settings > Apple ID > Subscriptions on a device signed in to the same Apple ID used to purchase your subscription.", comment: "Change plan or billing dialog subtitle description for subscription purchased via Apple") + static let changeSubscriptionAppleDialogDescription = NSLocalizedString("subscription.dialog.change.apple.description", value: "Your subscription was purchased through the Apple App Store. To change your plan or billing settings, please go to System Settings > Apple ID > Media and Purchases > Subscriptions > Manage on a device signed in to the same Apple ID used to purchase your subscription.", comment: "Change plan or billing dialog subtitle description for subscription purchased via Apple") static let changeSubscriptionDialogDone = NSLocalizedString("subscription.dialog.change.done.button", value: "Done", comment: "Button to close the change subscription dialog") // MARK: - Remove from this device dialog @@ -124,7 +124,7 @@ enum UserText { } } - static let shareModalHasEmailDescription = NSLocalizedString("subscription.share.modal.has.email.description", value: "Use this email to activate your subscription on other devices. Open the DuckDuckGo app on another device and find Privacy Pro in browser settings.", comment: "Share subscription modal description for email address channel") + static let shareModalHasEmailDescription = NSLocalizedString("subscription.share.modal.has.email.description", value: "Use this email to activate your subscription from browser settings in the DuckDuckGo app on other devices", comment: "Share subscription modal description for email address channel") static let shareModalNoEmailDescription = NSLocalizedString("subscription.share.modal.no.email.description", value: "Add an email address to access your subscription in DuckDuckGo on other devices. We’ll only use this address to verify your subscription.", comment: "Share subscription modal description for email address channel") static let restorePurchasesDescription = NSLocalizedString("subscription.share.modal.restore.purchases.description", value: "Your subscription is automatically available in DuckDuckGo on any device signed in to your Apple ID.", comment: "Share subscription modal description for restoring Apple ID purchases") From 36da9593f30040463c955244069d2670ed454bfe Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 24 May 2024 14:08:33 +0200 Subject: [PATCH 26/42] Scroll address bar to caret when using arrows (#2799) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207043866291151/f **Description**: When using arrows in the address bar it will scroll to the caret position --- .../View/AddressBarTextEditor.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextEditor.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextEditor.swift index bf5c1c6c7f..c235f193d1 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextEditor.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextEditor.swift @@ -292,12 +292,14 @@ final class AddressBarTextEditor: NSTextView { guard let index = nextWordSelectionIndex(backwards: false) else { return } self.selectedRange = NSRange(location: index, length: 0) + scrollToCaret() } override func moveWordLeft(_ sender: Any?) { guard let index = nextWordSelectionIndex(backwards: true) else { return } self.selectedRange = NSRange(location: index, length: 0) + scrollToCaret() } override func moveWordRightAndModifySelection(_ sender: Any?) { @@ -311,6 +313,7 @@ final class AddressBarTextEditor: NSTextView { let range = NSRange(location: selectedRange.location, length: index - selectedRange.location) self.setSelectedRange(range, affinity: .downstream, stillSelecting: false) + self.scrollToSelectionEnd() } override func moveWordLeftAndModifySelection(_ sender: Any?) { @@ -324,6 +327,7 @@ final class AddressBarTextEditor: NSTextView { let range = NSRange(location: index, length: selectedRange.upperBound - index) self.setSelectedRange(range, affinity: .upstream, stillSelecting: false) + self.scrollToSelectionStart() } override func deleteForward(_ sender: Any?) { @@ -422,6 +426,22 @@ final class AddressBarTextEditor: NSTextView { breakUndoCoalescing() } + private func scrollToCaret() { + guard let layoutManager = layoutManager, let textContainer = textContainer else { return } + let caretRect = layoutManager.boundingRect(forGlyphRange: selectedRange(), in: textContainer) + scrollToVisible(caretRect) + } + + private func scrollToSelectionStart() { + let startRange = NSRange(location: selectedRange().location, length: 0) + self.scrollRangeToVisible(startRange) + } + + private func scrollToSelectionEnd() { + let endRange = NSRange(location: selectedRange.location + selectedRange.length, length: 0) + self.scrollRangeToVisible(endRange) + } + } final class AddressBarTextFieldCell: NSTextFieldCell { From 6a23246daef74c0651913f01eba1ef33e4b19431 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 24 May 2024 18:25:32 +0200 Subject: [PATCH 27/42] Update BSK due to iOS changes. (#2776) Task/Issue URL: https://app.asana.com/0/414235014887631/1207178910368620/f iOS: duckduckgo/iOS#2795 BSK: duckduckgo/BrowserServicesKit#801 Description Updating BSK due to iOS changes to ensure it integrates well with 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 ba8f275f98..02e94cdf9a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13021,7 +13021,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 147.0.0; + version = 148.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 bd2dc2a20d..b6b11e25d1 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" : "610a58a77fefe82f8541d4a7f998ef2a4609a068", - "version" : "147.0.0" + "revision" : "7c235d29fc446436734612e81dd486b7c52aa577", + "version" : "148.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index f55ac724a6..583ef141ef 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: "147.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "148.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index e9ad0dbc90..626fd972c0 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: "147.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "148.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 3dcac72550..269c7f0143 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: "147.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "148.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 4d1917b6028b7a65edea572efcb32f9033051cb9 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Fri, 24 May 2024 14:38:43 -0500 Subject: [PATCH 28/42] Update PeopleWhiz Broker Files to use hash id (#2814) Task/Issue URL: https://app.asana.com/0/1203581873609357/1207395866477309/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Resources/JSON/people-wizard.com.json | 5 ++++- .../Resources/JSON/peopleswhizr.com.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peopleswiz.com.json | 5 ++++- .../Resources/JSON/peopleswizard.com.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peoplewhiz.com.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peoplewhiz.net.json | 5 ++++- .../Resources/JSON/peoplewhized.com.json | 5 ++++- .../Resources/JSON/peoplewhized.net.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peoplewhizr.com.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peoplewhizr.net.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peoplewiz.com.json | 5 ++++- .../Resources/JSON/peoplewizard.net.json | 5 ++++- .../DataBrokerProtection/Resources/JSON/peoplewizr.com.json | 5 ++++- 13 files changed, 52 insertions(+), 13 deletions(-) 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 33b35506fb..c387594263 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswhizr.com.json index c51c669302..39bee21f02 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswiz.com.json index 7c3aa7d2c8..18ac36c128 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peopleswizard.com.json index 9d288252ab..28e592c7cb 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.com.json index 861ede1a91..9560a08e76 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.6", + "version": "0.1.7", "addedDatetime": 1676160000000, "steps": [ { @@ -31,6 +31,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhiz.net.json index 697a7a94e1..af5e43e553 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709424000000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.com.json index 03a649fb82..0e5c402651 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709424000000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhized.net.json index 7ff6ee6216..66b7eec1d9 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.com.json index ff7542026a..6efb536b08 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewhizr.net.json index a4c3be5697..6faef5be13 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewiz.com.json index 3c4309d416..5e3459100f 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizard.net.json index 7715592055..c912e7886e 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplewizr.com.json index db79a59baf..e6a7d1638c 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.6", + "version": "0.1.7", "parent": "peoplewhiz.com", "addedDatetime": 1709445600000, "steps": [ @@ -32,6 +32,9 @@ "relativesList": { "selector": ".MuiGrid-item:nth-child(4) [class^='ResultsTable__AddressAndLocation-sc']", "findElements": true + }, + "profileUrl": { + "identifierType": "hash" } } } From 86fd337d347f47c30e6ccaa9abb6224e24cdddfb Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Sun, 26 May 2024 09:55:55 -0700 Subject: [PATCH 29/42] Privacy Pro survey support (#2816) Task/Issue URL: https://app.asana.com/0/72649045549333/1206971614154218/f Tech Design URL: CC: Description: This PR adds support for showing surveys to Privacy Pro users. --- DuckDuckGo.xcodeproj/project.pbxproj | 98 ++--- .../PrivacyProSurvey.imageset/Contents.json | 12 + .../Privacy-Pro-128.pdf | Bin 0 -> 14556 bytes .../HomePageRemoteMessagingRequest.swift | 64 ++- .../HomePageRemoteMessagingStorage.swift | 42 +- .../Surveys/SurveyRemoteMessage.swift} | 37 +- .../Surveys/SurveyRemoteMessaging.swift | 270 +++++++++++++ .../Common/Surveys/SurveyURLBuilder.swift | 105 ++++- .../DataBrokerProtectionRemoteMessaging.swift | 180 --------- .../Model/HomePageContinueSetUpModel.swift | 159 +++----- .../View/HomePageViewController.swift | 4 +- .../MainWindow/MainViewController.swift | 25 +- DuckDuckGo/Menus/MainMenu.swift | 4 + DuckDuckGo/Menus/MainMenuActions.swift | 5 + .../NetworkProtectionDebugMenu.swift | 8 - .../NetworkProtectionDebugUtilities.swift | 2 - .../NetworkProtectionRemoteMessage.swift | 71 ---- .../NetworkProtectionRemoteMessaging.swift | 179 --------- .../Model/AutofillPreferencesModel.swift | 7 +- DuckDuckGo/Statistics/GeneralPixel.swift | 45 +-- .../InputFilesChecker/InputFilesChecker.swift | 5 +- .../HomePage/ContinueSetUpModelTests.swift | 90 +---- .../HomePage/Resources/survey-messages.json | 20 + .../SurveyRemoteMessageTests.swift} | 87 ++-- .../HomePage/SurveyRemoteMessagingTests.swift | 296 ++++++++++++++ ...etworkProtectionRemoteMessagingTests.swift | 376 ------------------ .../Resources/dbp-messages.json | 37 -- .../network-protection-messages.json | 37 -- 28 files changed, 923 insertions(+), 1342 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf rename DuckDuckGo/{DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift => Common/Surveys/SurveyRemoteMessage.swift} (59%) create mode 100644 DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift delete mode 100644 DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift create mode 100644 UnitTests/HomePage/Resources/survey-messages.json rename UnitTests/{NetworkProtection/NetworkProtectionRemoteMessageTests.swift => HomePage/SurveyRemoteMessageTests.swift} (52%) create mode 100644 UnitTests/HomePage/SurveyRemoteMessagingTests.swift delete mode 100644 UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift delete mode 100644 UnitTests/NetworkProtection/Resources/dbp-messages.json delete mode 100644 UnitTests/NetworkProtection/Resources/network-protection-messages.json diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 02e94cdf9a..c18322e0a6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1129,14 +1129,9 @@ 4B37EE5F2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */; }; 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */; }; 4B37EE632B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */; }; - 4B37EE6F2B4CFE8500A89A61 /* dbp-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B37EE6E2B4CFE8500A89A61 /* dbp-messages.json */; }; 4B37EE722B4CFEE400A89A61 /* SurveyURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */; }; 4B37EE732B4CFF0800A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */; }; 4B37EE742B4CFF0A00A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */; }; - 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */; }; - 4B37EE762B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */; }; - 4B37EE772B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */; }; - 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */; }; 4B39AAF627D9B2C700A73FD5 /* NSStackViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */; }; 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */; }; 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F641D27A8D3BD00E0C118 /* BrowserProfileTests.swift */; }; @@ -1353,6 +1348,8 @@ 4BBDEE9328FC14760092FAA6 /* ConnectBitwardenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBDEE8F28FC14760092FAA6 /* ConnectBitwardenViewModel.swift */; }; 4BBDEE9428FC14760092FAA6 /* ConnectBitwardenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBDEE9028FC14760092FAA6 /* ConnectBitwardenViewController.swift */; }; 4BBE0AA727B9B027003B37A8 /* PopUpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE0AA627B9B027003B37A8 /* PopUpButton.swift */; }; + 4BBEE8DE2BFEDE3E00E5E111 /* SurveyRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */; }; + 4BBEE8DF2BFEE07D00E5E111 /* survey-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BCF15E92ABB99470083F6DF /* survey-messages.json */; }; 4BBF0915282DD40100EE1418 /* TemporaryFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0914282DD40100EE1418 /* TemporaryFileHandler.swift */; }; 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF0916282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift */; }; 4BBF09232830812900EE1418 /* FileSystemDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF09222830812900EE1418 /* FileSystemDSL.swift */; }; @@ -1362,16 +1359,16 @@ 4BCBE4582BA7E17800FC75A1 /* SubscriptionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4572BA7E17800FC75A1 /* SubscriptionUI */; }; 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE4592BA7E17800FC75A1 /* Subscription */; }; 4BCBE45C2BA7E18500FC75A1 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 4BCBE45B2BA7E18500FC75A1 /* Subscription */; }; - 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; - 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; - 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */; }; - 4BCF15ED2ABB9B180083F6DF /* network-protection-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BCF15E92ABB99470083F6DF /* network-protection-messages.json */; }; - 4BCF15EE2ABBDBFD0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; - 4BCF15EF2ABBDBFF0083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */; }; + 4BCF15D72ABB8A110083F6DF /* SurveyRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */; }; + 4BCF15D92ABB8A7F0083F6DF /* SurveyRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */; }; + 4BCF15EC2ABB9AF80083F6DF /* SurveyRemoteMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */; }; + 4BCF15ED2ABB9B180083F6DF /* survey-messages.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BCF15E92ABB99470083F6DF /* survey-messages.json */; }; + 4BCF15EE2ABBDBFD0083F6DF /* SurveyRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */; }; + 4BCF15EF2ABBDBFF0083F6DF /* SurveyRemoteMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */; }; 4BD18F01283F0BC500058124 /* BookmarksBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD18EFF283F0BC500058124 /* BookmarksBarViewController.swift */; }; 4BD18F05283F151F00058124 /* BookmarksBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */; }; - 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; - 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; + 4BD57C042AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */; }; + 4BD57C052AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */; }; 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 */; }; @@ -3120,9 +3117,6 @@ 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageRemoteMessagingStorage.swift; sourceTree = ""; }; 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyURLBuilder.swift; sourceTree = ""; }; 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePageRemoteMessagingRequest.swift; sourceTree = ""; }; - 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionRemoteMessage.swift; sourceTree = ""; }; - 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionRemoteMessaging.swift; sourceTree = ""; }; - 4B37EE6E2B4CFE8500A89A61 /* dbp-messages.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dbp-messages.json"; sourceTree = ""; }; 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStackViewExtension.swift; sourceTree = ""; }; 4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManagerExtension.swift; sourceTree = ""; }; 4B3F641D27A8D3BD00E0C118 /* BrowserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserProfileTests.swift; sourceTree = ""; }; @@ -3300,13 +3294,13 @@ 4BBF0916282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryFileHandlerTests.swift; sourceTree = ""; }; 4BBF09222830812900EE1418 /* FileSystemDSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemDSL.swift; sourceTree = ""; }; 4BBF0924283083EC00EE1418 /* FileSystemDSLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemDSLTests.swift; sourceTree = ""; }; - 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessaging.swift; sourceTree = ""; }; - 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessage.swift; sourceTree = ""; }; - 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessageTests.swift; sourceTree = ""; }; - 4BCF15E92ABB99470083F6DF /* network-protection-messages.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "network-protection-messages.json"; sourceTree = ""; }; + 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessaging.swift; sourceTree = ""; }; + 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessage.swift; sourceTree = ""; }; + 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyRemoteMessageTests.swift; sourceTree = ""; }; + 4BCF15E92ABB99470083F6DF /* survey-messages.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "survey-messages.json"; sourceTree = ""; }; 4BD18EFF283F0BC500058124 /* BookmarksBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarViewController.swift; sourceTree = ""; }; 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; - 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; sourceTree = ""; }; + 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 = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; @@ -4636,7 +4630,6 @@ 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3169132B2BD2C7960051B46D /* ErrorView */, 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */, - 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */, ); path = DBP; sourceTree = ""; @@ -5064,22 +5057,15 @@ 4B37EE5B2B4CFC3C00A89A61 /* Surveys */ = { isa = PBXGroup; children = ( - 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */, - 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */, 4B37EE5E2B4CFC3C00A89A61 /* HomePageRemoteMessagingRequest.swift */, + 4B37EE5C2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift */, + 4BCF15D82ABB8A7F0083F6DF /* SurveyRemoteMessage.swift */, + 4BCF15D62ABB8A110083F6DF /* SurveyRemoteMessaging.swift */, + 4B37EE5D2B4CFC3C00A89A61 /* SurveyURLBuilder.swift */, ); path = Surveys; sourceTree = ""; }; - 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */ = { - isa = PBXGroup; - children = ( - 4B37EE662B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessage.swift */, - 4B37EE672B4CFC9500A89A61 /* DataBrokerProtectionRemoteMessaging.swift */, - ); - path = RemoteMessaging; - sourceTree = ""; - }; 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */ = { isa = PBXGroup; children = ( @@ -5172,7 +5158,6 @@ 4B4D60612A0B29FA00BCD287 /* DeveloperIDTarget */ = { isa = PBXGroup; children = ( - 4BCF15D52ABB83D70083F6DF /* NetworkProtectionRemoteMessaging */, 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */, 4B2F565B2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift */, ); @@ -5732,22 +5717,18 @@ path = View; sourceTree = ""; }; - 4BCF15D52ABB83D70083F6DF /* NetworkProtectionRemoteMessaging */ = { + 4BBEE8E12BFEE54100E5E111 /* Resources */ = { isa = PBXGroup; children = ( - 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */, - 4BCF15D62ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift */, + 4BCF15E92ABB99470083F6DF /* survey-messages.json */, ); - path = NetworkProtectionRemoteMessaging; + path = Resources; sourceTree = ""; }; 4BCF15E32ABB987F0083F6DF /* NetworkProtection */ = { isa = PBXGroup; children = ( BDA7648F2BC4E56200D0400C /* Mocks */, - 4BCF15E62ABB98A20083F6DF /* Resources */, - 4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */, - 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */, 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */, @@ -5755,15 +5736,6 @@ path = NetworkProtection; sourceTree = ""; }; - 4BCF15E62ABB98A20083F6DF /* Resources */ = { - isa = PBXGroup; - children = ( - 4B37EE6E2B4CFE8500A89A61 /* dbp-messages.json */, - 4BCF15E92ABB99470083F6DF /* network-protection-messages.json */, - ); - path = Resources; - sourceTree = ""; - }; 4BD18F02283F0F1000058124 /* View */ = { isa = PBXGroup; children = ( @@ -5790,11 +5762,14 @@ 4BF6961B28BE90E800D402D4 /* HomePage */ = { isa = PBXGroup; children = ( + 4BBEE8E12BFEE54100E5E111 /* Resources */, 56534DEB29DF251C00121467 /* Mocks */, 4BF6961C28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift */, 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */, 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */, 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */, + 4BD57C032AC112DF00B580EE /* SurveyRemoteMessagingTests.swift */, + 4BCF15E42ABB98990083F6DF /* SurveyRemoteMessageTests.swift */, ); path = HomePage; sourceTree = ""; @@ -9033,6 +9008,7 @@ 3706FE8C293F661700E42796 /* atb-with-update.json in Resources */, 9FBD84622BB3BC6400220859 /* Origin-empty.txt in Resources */, 3706FE8D293F661700E42796 /* DataImportResources in Resources */, + 4BBEE8DF2BFEE07D00E5E111 /* survey-messages.json in Resources */, 3706FE8E293F661700E42796 /* atb.json in Resources */, 9FBD845E2BB3B80300220859 /* Origin.txt in Resources */, 3706FE8F293F661700E42796 /* DuckDuckGo-ExampleCrash.ips in Resources */, @@ -9226,10 +9202,9 @@ B69B50542726CD8100758A2B /* atb-with-update.json in Resources */, 37A803DB27FD69D300052F4C /* DataImportResources in Resources */, B65CD8D52B316FCA00A595BB /* __Snapshots__ in Resources */, - 4B37EE6F2B4CFE8500A89A61 /* dbp-messages.json in Resources */, B69B50522726CD8100758A2B /* atb.json in Resources */, 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */, - 4BCF15ED2ABB9B180083F6DF /* network-protection-messages.json in Resources */, + 4BCF15ED2ABB9B180083F6DF /* survey-messages.json in Resources */, B67C6C422654BF49006C872E /* DuckDuckGo-Symbol.jpg in Resources */, B69B50552726CD8100758A2B /* invalid.json in Resources */, 9FBD84612BB3BC6400220859 /* Origin-empty.txt in Resources */, @@ -9910,7 +9885,6 @@ 3706FB7E293F65D500E42796 /* FireCoordinator.swift in Sources */, 3706FB7F293F65D500E42796 /* GeolocationProvider.swift in Sources */, B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */, - 4B37EE762B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, 3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, B677FC552B064A9C0099EB04 /* DataImportViewModel.swift in Sources */, D64A5FF92AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, @@ -10007,7 +9981,7 @@ 3706FBB6293F65D500E42796 /* ChromiumPreferences.swift in Sources */, 3706FBB7293F65D500E42796 /* FirePopoverViewController.swift in Sources */, 3706FBB8293F65D500E42796 /* SavePaymentMethodPopover.swift in Sources */, - 4BCF15EF2ABBDBFF0083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, + 4BCF15EF2ABBDBFF0083F6DF /* SurveyRemoteMessaging.swift in Sources */, B6CC266D2BAD9CD800F53F8D /* FileProgressPresenter.swift in Sources */, 3706FBB9293F65D500E42796 /* FindInPageViewController.swift in Sources */, 3706FBBA293F65D500E42796 /* Cryptography.swift in Sources */, @@ -10156,7 +10130,6 @@ 1DC669712B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, 3706FC19293F65D500E42796 /* NSNotificationName+Favicons.swift in Sources */, 3706FC1A293F65D500E42796 /* PinningManager.swift in Sources */, - 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, 3706FC1B293F65D500E42796 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */, 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, @@ -10319,7 +10292,7 @@ B6BCC53C2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3706FC8A293F65D500E42796 /* AutoconsentUserScript.swift in Sources */, 3706FC8B293F65D500E42796 /* BookmarksExporter.swift in Sources */, - 4BCF15EE2ABBDBFD0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, + 4BCF15EE2ABBDBFD0083F6DF /* SurveyRemoteMessage.swift in Sources */, 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */, 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, @@ -10479,6 +10452,7 @@ B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, + 4BBEE8DE2BFEDE3E00E5E111 /* SurveyRemoteMessageTests.swift in Sources */, 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, @@ -10598,7 +10572,7 @@ 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, 3706FE72293F661700E42796 /* ClickToLoadTDSTests.swift in Sources */, 3706FE73293F661700E42796 /* PermissionManagerMock.swift in Sources */, - 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, + 4BD57C052AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */, 3706FE74293F661700E42796 /* WebsiteDataStoreMock.swift in Sources */, 3706FE75293F661700E42796 /* WebsiteBreakageReportTests.swift in Sources */, 56D145EF29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, @@ -11222,7 +11196,6 @@ 4BBC16A227C485BC00E00A38 /* DeviceIdleStateDetector.swift in Sources */, 4B379C2427BDE1B0008A968E /* FlatButton.swift in Sources */, 37054FC92873301700033B6F /* PinnedTabView.swift in Sources */, - 4B37EE772B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, 4BA1A6A0258B079600F6F690 /* DataEncryption.swift in Sources */, B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */, B626A76D29928B1600053070 /* TestsClosureNavigationResponder.swift in Sources */, @@ -11317,7 +11290,6 @@ B66260E029AC6EBD00E9E3EE /* HistoryTabExtension.swift in Sources */, B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, - 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11497,7 +11469,7 @@ 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, - 4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */, + 4BCF15D92ABB8A7F0083F6DF /* SurveyRemoteMessage.swift in Sources */, 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, 4B9DB0292A983B24000927DB /* WaitlistStorage.swift in Sources */, AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */, @@ -11521,7 +11493,7 @@ 85707F31276A7DCA00DC0649 /* OnboardingViewModel.swift in Sources */, 85AC3B0525D6B1D800C7D2AA /* ScriptSourceProviding.swift in Sources */, 4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */, - 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, + 4BCF15D72ABB8A110083F6DF /* SurveyRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, F1C70D7C2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, @@ -11765,7 +11737,7 @@ 9833913327AAAEEE00DAF119 /* EmbeddedTrackerDataTests.swift in Sources */, 3776583127F8325B009A6B35 /* AutofillPreferencesTests.swift in Sources */, B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, - 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, + 4BD57C042AC112DF00B580EE /* SurveyRemoteMessagingTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, @@ -11856,7 +11828,7 @@ AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 4B723E0726B0003E00E14D75 /* CSVImporterTests.swift in Sources */, - 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */, + 4BCF15EC2ABB9AF80083F6DF /* SurveyRemoteMessageTests.swift in Sources */, B62EB47C25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift in Sources */, 9F0FFFBB2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, 4BBC16A527C488C900E00A38 /* DeviceAuthenticatorTests.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json new file mode 100644 index 0000000000..85ed0dc199 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Privacy-Pro-128.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf b/DuckDuckGo/Assets.xcassets/Images/PrivacyProSurvey.imageset/Privacy-Pro-128.pdf new file mode 100644 index 0000000000000000000000000000000000000000..18085b7a093a947728b9d5bf2e5100cac4e649b1 GIT binary patch literal 14556 zcmeI3U5_P4k%sTjuc#YKfaEioRayBVAxmHzv;wr=@!p}mXxfb#)HBUaH^}hodEUr8 zQFZzpGd&0{*rhGm<*2I6_>MOs>wNOn7eD(n9?Rhnr!YPI=J$tb`tgsaS6_bj^z*Nu zZl3P?|7H8n?adU9bI{-P*=Ia?@%QS>^x`My7eBea_{rtPPsW!#`PpZZ^X}a@Qp{mK z)6g&P-hKP@yJ>;=T2DPby}!G8dwBK5-Jjmv-o5_$Pp8kn-v8h2u=8LReayQa!eQb^G=Zj_aa7tnvKf@%G(eJ+5pU&TtZm}-_K=QC ziD^mDYSZ6z4=t_`E3}q)ql}AGrg2M4*y90;7MSaFd+5u?F!hbf%v#HF1ODwZ-mXHo5a>3vAG0;27@PdT(EKB zLde#O5%=P+UCUjtx(>Br3*S%g4Zm@|l|+hhXcTVlo6_4AA0Butdhs|)i+ALh84D6mFlf@%_=0_=sUrN7*3IWTDOnW*5iU}m_j6&s|{QD ze)5swH_n%QL~e#BM|e|CM{LzA0XDVAU?>JBsn%X^C3XZO?)th-s2 z^(7cu_Wj%+&O8R}Ly{3gB5vs=Ev4&~6wriC2B0^{-d z#F0Pl6v6KCxYPXN@|G_kbTq{DZTxX(*dKSwd5X+0S8&99U9Btoyj;QdXoKZUZ$Elx_;gbh!kCAwnY(qFY+iu+FSzBq@>p zBBWA4PxfkV+BE`ji|jtO0A+%aT3q0`^U;LvTUaa4ocI;^jWptd>_f1M!PGeNSjI5` zHGw%#)%Ibj;6!c>qQ_>u~ki#4oID)3Z+|Mg5kf6y;RDwvtV5ga%&aS1#o72q}N z3>EF7KW0uSYbG!&@yL^+VevN2cuOv`p2I{aF3HfiYfe$ENLn#TYABdso=d3>r87v0 zB~`*P;>{tWRRuT&53LdsxP6QTM@7RVfTjW1ir(VTdZyiLbV_&toYAK6Hcpq595gYp z!F&tJX(9Q{go|`dux1hscf+!#m9?QYYBj-3lES%?r2_b^SuTWRk)Z0{yB{ zgW&RDM4)Yy?U$wG*@Bq_v4)i~Vj-)REXL^#GmZrlB$<@(YocT>ZM8wtik1l`R7T^o zhJwi$hs0t7H)1U{e7%#%qmwf9OuRH^P`sKL%bA~L@)zw*-vSkr-V+I}bv}wRH3KJF zx_rhj8)US>Y9?6AeV|)&L!7Pv+`0#IIcbUuMI;f9Bg^HbQp=ubqLzSHnC3}ckzihv z0L?IwT1iFZLdMJ%etFJ|QliStoK-y3Lt%f@EWwHj^zss_I525Tz^F#0+EJyZs7x4e zIkW0g6aI)u7k>RuiYf%K&KMpF`RMP9DtNz$DwKOAs-XT0MU^~M{GLY@@rT2oM-|a| z5mkz4O2}DMY2rVkia`9hsER6)e^*r1c8aQciYgrVSybT$c<8gJ!fmZY?xIR5!b;>n zF{)tr9}`vNS90Y?N0sE$-|mdaOA)1}k{v-nu*lf2k_?`eDvCF#L?ihMgtVYi1p(&R zR4%TK6rt({&|1jXV|;DrEi*`yLnodGWO4SvpeZF4HUMSRv`}6}!dd#zk9-lyo|K)y z8IYE5nZ&5cn_vyR!8ovntfEO(Da5%3b%hMfd(`*s_5=Em zHbIdAT%=@3fgD(vN&=+BDJh0Xr8-2P&|;&oq5DKt3|1CNsnl<95CAgMfNDV?K0{5( z%uufZY6x1mp$kxAr(9DL@r{@(NQ__r+RPlyVBH9-9hqxeVN^01X_?}MGAa5X1@#Fb zt}4}pGpAZ8X(Oq^P_Y8hz7Y|e6PQ5Q>s63J4hD`OqW zg-Q!ZA%Pz7Q?d_`f$9lfGh&ejMHFP>&(KII>7>2?89- z(koB~EzDp^LWLNRrvlBaEul5U<`b|O*HF2|KLJr-tBDq0Sc`=TE%lMBx-9D`7exeK zXR!$U!9O`gP-jK(5@(w5;v94Ces0uALnSI0f-X%SPh*M=4D@MpWP*#8!$88DZVbSP zoRk{UPR?0J3JsVW8~bpTR=O6RH0V)vR?2}i*aH{Qb8SXt;+#PRux_lG>Cw1P_5kPs zIP?xjnj{-_rt~O^Pz6ksgBk2y=EOhZEzc29X()=VXA3A?9rqEC4)neF5y68VSM*~D zaHcy_SAs4{Q#iNvbI3!Oi8g1rgvN%n>Gn3UiYMc?(g8^hP1EgWYK_67OzTH)-*u&q zon$rB1nJllE2Szx%G>mfbS0t7xB`%8HA&TQwA2eS$IqC{>1g((GkJv_Zzez_MwD_} zMa?P~IB0(^kYx!(Rb4gXW-R3~UPTW9ylA_!rILuUY88ew?$QJfdn($6T_RZs9!xmcVsye_jR5|#W8=Mv6u8r?!>Yd z7kDi1LgZDYR6#J(H7yXHy^`yd9u#Fb1tfISC^(8nv@OpxB91Rz5Tfc^2vNF8B3s=G zF-2aYi6+mNRxOl8BBXkBpM|{g&6RF|Oh8s6^}(1m66zXw-Fgd^Q|m28vAO6$)V^DK zrH3l&!Bmu(6KJ5I$pV!D??qLGiiP@+m*Q)k4*{x3C8;wFAxJDY(1?-pQ7MtjDV^N# zO4oFW1o5B?#E9C^@0E@X>P2tYdqDLA-UH%ySGvWkedV?O^LnSMI`FKgJ@vluD;Kfe z_j|fz;RgC?xZIHcmA?CWCv&yuN%W`Bdm~Erl`iT^zjdW8yV}0>HcvQ2%C$D{N;7yB z-d^HhtHNhZC~1QLxolP|7-42u(A^2Jqg+O4DdNrPKv@d?z}GGSB6CiG)9~ zPuSRw3`ZU`WE%-3zS>fjlbfg=RzIhDJh@LHH&|hZuGQTZmmDXmCLxpgPzEoeJzOK&*9uT>(yk z-AG~Ez%JG?uv=Dr*hFe$GvPOZ-3lb9oEg}yVoI|ya)=7Cs0&z_Vkq4-%`Y(!U>CHZ z5(jco$~CYHALJHI=s@qPD+YEcwYaD(PQkMY1GR9`gaOxJc!Ia6c2lQ2dSKAt6c!H{dd4%SuDbxs*%{hevS4)F0?Mdn(utvtEpNg2mk~e@; zC79lcD4?$s6&Bc9F4pKha5iW*jl*3?_zcppsX+%MA(XA8#o39r6^$15yR1 zYz!@$-W81)g;m8oc%KoUPF1+4yinNPkOKZLD?s5MYLsiBN17;Q9gJa38pGBIGGs|~ zfcY-d)_EJ`nQNP1>s|BU(XW%%wlhHxwBf49GAuSfYq)8f7~f&7IE$STALaCp77Y%T!2u2FDPNYGXQiP@w!0(wN7-# zbj0w?S_;H&RI|x5k<22UHUuX}cZ=^bqx0b+x-G1{c6t1!ujCY8U}uGmv7H$<0eq27 zOkZUqompawh|L1qmD^$^FLaKTaQc`e0N_MH1RN^R9{8-WjC(&2fww5D{zq*Nai0QzB5g(@+7RMJN9<7LX2D;3VmX@d3-B>5YoHK-QFdUk-(*#=cs3 z?7ERanb&t)C>W;Q7<)~kj4tQ$0CjP*(NlCb5vP0 z#R$1_uGZ@(5E_&~Rs?CmvY}lgX*gyqD>8t9Eb~f4-(}-VILUY z4H6PTqFIQ1G@^*6g%gf}$V7rq70e;90bmsDL{|-Jk&kqezGmT|Sg>Uy(rYlhL8bSw zf-CcZu^N;s#Qr+)Z~r9jrXvV($Rv-^x|jhYCnH_RD`jH~zP(_bu??~-WC#;AIwIn} z2BavEIvi%~rcw#CL~nxx?!tFW7AK+)I3aUeQwwlZ4@|Rz3+WhxQR})16>*gSPzAM4R~cbW(KH_WN;hsK3o8NX=RC9&TDwx zqLr^YaM5ut!#LAkRQq7*gX4dOlsg;hV;v1<-GIg@^P{~{O(4;F^}*?E96*;~w4RIU zG5k4?3R9uJ0&xlnLq3KkcBRKGLZ70rRw2`rtNV-`nSy6qZX|2BpBw#NKey;(1O51F z{IcrwGb~WZ_@NpdWh(yjEPmUv`vKce-o1NxdYpd!KmBKFum1bbf4G}oeRcEg-5~tx z?wdC^KYjRa`t>G~%hvo0%I*8G(@L-J9v}Yr{`T&1!d%nqvE1hm@1E}7v7>$^b|QXt z^B&s7z`;nn-;#;6{o3vqZ@z)mNfNxCPWycR@JHT_7yLg$*{y^{b}LE1N8ga&=MBL> zzj?a3fB5$N)$iWl{q_*1a_EIh|DM#UtP|w%$N1B(CWb8iPoKJ+k)nR1c0ppG{Ol<^ zi4pCzmcDdH=0SRTfAi-4?)^bOKm3m`P{$7*AKu>maQeZUZ+_U7^6K@&!_y>zO~J3e de)E?*xVInQ-MoJ?ap)eG(completion: @escaping (Result<[T], Error>) -> Void) + func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error> } final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingRequest { - enum NetworkProtectionEndpoint { + enum SurveysEndpoint { case debug case production var url: URL { switch self { - case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/network-protection/messages-v2-debug.json")! - case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/network-protection/messages-v2.json")! - } - } - } - - enum DataBrokerProtectionEndpoint { - case debug - case production - - var url: URL { - switch self { - case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/dbp/messages-debug.json")! - case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/dbp/messages.json")! + case .debug: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/surveys/surveys-debug.json")! + case .production: return URL(string: "https://staticcdn.duckduckgo.com/macos-desktop-browser/surveys/surveys.json")! } } } @@ -56,19 +44,11 @@ final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingReques case requestCompletedWithoutErrorOrResponse } - static func networkProtectionMessagesRequest() -> HomePageRemoteMessagingRequest { + static func surveysRequest() -> HomePageRemoteMessagingRequest { #if DEBUG || REVIEW - return DefaultHomePageRemoteMessagingRequest(endpointURL: NetworkProtectionEndpoint.debug.url) + return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.debug.url) #else - return DefaultHomePageRemoteMessagingRequest(endpointURL: NetworkProtectionEndpoint.production.url) -#endif - } - - static func dataBrokerProtectionMessagesRequest() -> HomePageRemoteMessagingRequest { -#if DEBUG || REVIEW - return DefaultHomePageRemoteMessagingRequest(endpointURL: DataBrokerProtectionEndpoint.debug.url) -#else - return DefaultHomePageRemoteMessagingRequest(endpointURL: DataBrokerProtectionEndpoint.production.url) + return DefaultHomePageRemoteMessagingRequest(endpointURL: SurveysEndpoint.production.url) #endif } @@ -78,25 +58,27 @@ final class DefaultHomePageRemoteMessagingRequest: HomePageRemoteMessagingReques self.endpointURL = endpointURL } - func fetchHomePageRemoteMessages(completion: @escaping (Result<[T], Error>) -> Void) { + func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], Error> { let httpMethod = APIRequest.HTTPMethod.get let configuration = APIRequest.Configuration(url: endpointURL, method: httpMethod, body: nil) let request = APIRequest(configuration: configuration) - request.fetch { response, error in - if let error { - completion(Result.failure(error)) - } else if let responseData = response?.data { - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode([T].self, from: responseData) - completion(Result.success(decoded)) - } catch { - completion(.failure(HomePageRemoteMessagingRequestError.failedToDecodeMessages)) - } - } else { - completion(.failure(HomePageRemoteMessagingRequestError.requestCompletedWithoutErrorOrResponse)) + do { + let response = try await request.fetch() + + guard let data = response.data else { + return .failure(HomePageRemoteMessagingRequestError.requestCompletedWithoutErrorOrResponse) + } + + do { + let decoder = JSONDecoder() + let decoded = try decoder.decode([SurveyRemoteMessage].self, from: data) + return .success(decoded) + } catch { + return .failure(HomePageRemoteMessagingRequestError.failedToDecodeMessages) } + } catch { + return .failure(error) } } diff --git a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift index d9b1fd01ab..7057913dad 100644 --- a/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift +++ b/DuckDuckGo/Common/Surveys/HomePageRemoteMessagingStorage.swift @@ -18,26 +18,21 @@ import Foundation -protocol HomePageRemoteMessagingStorage { +protocol SurveyRemoteMessagingStorage { - func store(messages: [Message]) throws - func storedMessages() -> [Message] + func store(messages: [SurveyRemoteMessage]) throws + func storedMessages() -> [SurveyRemoteMessage] func dismissRemoteMessage(with id: String) func dismissedMessageIDs() -> [String] } -final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorage { +final class DefaultSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage { - enum NetworkProtectionConstants { - static let dismissedMessageIdentifiersKey = "home.page.network-protection.dismissed-message-identifiers" - static let networkProtectionMessagesFileName = "network-protection-messages.json" - } - - enum DataBrokerProtectionConstants { - static let dismissedMessageIdentifiersKey = "home.page.dbp.dismissed-message-identifiers" - static let networkProtectionMessagesFileName = "dbp-messages.json" + enum SurveyConstants { + static let dismissedMessageIdentifiersKey = "home.page.survey.dismissed-message-identifiers" + static let surveyMessagesFileName = "survey-messages.json" } private let userDefaults: UserDefaults @@ -48,23 +43,16 @@ final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorag URL.sandboxApplicationSupportURL } - static func networkProtection() -> DefaultHomePageRemoteMessagingStorage { - return DefaultHomePageRemoteMessagingStorage( - messagesFileName: NetworkProtectionConstants.networkProtectionMessagesFileName, - dismissedMessageIdentifiersKey: NetworkProtectionConstants.dismissedMessageIdentifiersKey - ) - } - - static func dataBrokerProtection() -> DefaultHomePageRemoteMessagingStorage { - return DefaultHomePageRemoteMessagingStorage( - messagesFileName: DataBrokerProtectionConstants.networkProtectionMessagesFileName, - dismissedMessageIdentifiersKey: DataBrokerProtectionConstants.dismissedMessageIdentifiersKey + static func surveys() -> DefaultSurveyRemoteMessagingStorage { + return DefaultSurveyRemoteMessagingStorage( + messagesFileName: SurveyConstants.surveyMessagesFileName, + dismissedMessageIdentifiersKey: SurveyConstants.dismissedMessageIdentifiersKey ) } init( userDefaults: UserDefaults = .standard, - messagesDirectoryURL: URL = DefaultHomePageRemoteMessagingStorage.applicationSupportURL, + messagesDirectoryURL: URL = DefaultSurveyRemoteMessagingStorage.applicationSupportURL, messagesFileName: String, dismissedMessageIdentifiersKey: String ) { @@ -73,15 +61,15 @@ final class DefaultHomePageRemoteMessagingStorage: HomePageRemoteMessagingStorag self.dismissedMessageIdentifiersKey = dismissedMessageIdentifiersKey } - func store(messages: [Message]) throws { + func store(messages: [SurveyRemoteMessage]) throws { let encoded = try JSONEncoder().encode(messages) try encoded.write(to: messagesURL) } - func storedMessages() -> [Message] { + func storedMessages() -> [SurveyRemoteMessage] { do { let messagesData = try Data(contentsOf: messagesURL) - let messages = try JSONDecoder().decode([Message].self, from: messagesData) + let messages = try JSONDecoder().decode([SurveyRemoteMessage].self, from: messagesData) return messages } catch { diff --git a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift similarity index 59% rename from DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift rename to DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift index a49c997ca0..b9fb348f24 100644 --- a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessage.swift +++ b/DuckDuckGo/Common/Surveys/SurveyRemoteMessage.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionRemoteMessage.swift +// SurveyRemoteMessage.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,10 +18,10 @@ import Foundation import Common +import Subscription -struct DataBrokerRemoteMessageAction: Codable, Equatable, Hashable { +struct SurveyRemoteMessageAction: Codable, Equatable, Hashable { enum Action: String, Codable { - case openDataBrokerProtection case openSurveyURL case openURL } @@ -31,23 +31,31 @@ struct DataBrokerRemoteMessageAction: Codable, Equatable, Hashable { let actionURL: String? } -struct DataBrokerProtectionRemoteMessage: Codable, Equatable, Hashable { +struct SurveyRemoteMessage: Codable, Equatable, Identifiable, Hashable { + + struct Attributes: Codable, Equatable, Hashable { + let subscriptionStatus: String? + let subscriptionBillingPeriod: String? + let minimumDaysSinceSubscriptionStarted: Int? + let maximumDaysUntilSubscriptionExpirationOrRenewal: Int? + let daysSinceVPNEnabled: Int? + let daysSincePIREnabled: Int? + } let id: String let cardTitle: String let cardDescription: String - /// If this is set, the message won't be displayed if DBP hasn't been used, even if the usage and access booleans are false - let daysSinceDataBrokerProtectionEnabled: Int? - let requiresDataBrokerProtectionUsage: Bool - let requiresDataBrokerProtectionAccess: Bool - let action: DataBrokerRemoteMessageAction + let attributes: Attributes + let action: SurveyRemoteMessageAction func presentableSurveyURL( statisticsStore: StatisticsStore = LocalStatisticsStore(), - activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), + vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), + pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description, appVersion: String = AppVersion.shared.versionNumber, - hardwareModel: String? = HardwareModel.model + hardwareModel: String? = HardwareModel.model, + subscription: Subscription? ) -> URL? { if let actionType = action.actionType, actionType == .openURL, let urlString = action.actionURL, let url = URL(string: urlString) { return url @@ -62,8 +70,11 @@ struct DataBrokerProtectionRemoteMessage: Codable, Equatable, Hashable { operatingSystemVersion: operatingSystemVersion, appVersion: appVersion, hardwareModel: hardwareModel, - daysSinceActivation: activationDateStore.daysSinceActivation(), - daysSinceLastActive: activationDateStore.daysSinceLastActive() + subscription: subscription, + daysSinceVPNActivated: vpnActivationDateStore.daysSinceActivation(), + daysSinceVPNLastActive: vpnActivationDateStore.daysSinceLastActive(), + daysSincePIRActivated: pirActivationDateStore.daysSinceActivation(), + daysSincePIRLastActive: pirActivationDateStore.daysSinceLastActive() ) return surveyURLBuilder.buildSurveyURL(from: surveyURL) diff --git a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift new file mode 100644 index 0000000000..51c1496884 --- /dev/null +++ b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift @@ -0,0 +1,270 @@ +// +// SurveyRemoteMessaging.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 Networking +import PixelKit +import Subscription + +protocol SurveyRemoteMessaging { + + func fetchRemoteMessages() async + func presentableRemoteMessages() -> [SurveyRemoteMessage] + func dismiss(message: SurveyRemoteMessage) + +} + +protocol SurveyRemoteMessageSubscriptionFetching { + + func getSubscription(accessToken: String) async -> Result + +} + +final class DefaultSurveyRemoteMessaging: SurveyRemoteMessaging { + + enum Constants { + static let lastRefreshDateKey = "surveys.remote-messaging.last-refresh-date" + } + + private let messageRequest: HomePageRemoteMessagingRequest + private let messageStorage: SurveyRemoteMessagingStorage + private let accountManager: AccountManaging + private let subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching + private let vpnActivationDateStore: WaitlistActivationDateStore + private let pirActivationDateStore: WaitlistActivationDateStore + private let minimumRefreshInterval: TimeInterval + private let userDefaults: UserDefaults + + convenience init(subscriptionManager: SubscriptionManaging) { + #if DEBUG || REVIEW + self.init( + accountManager: subscriptionManager.accountManager, + subscriptionFetcher: subscriptionManager.subscriptionService, + minimumRefreshInterval: .seconds(30) + ) + #else + self.init( + accountManager: subscriptionManager.accountManager, + subscriptionFetcher: subscriptionManager.subscriptionService, + minimumRefreshInterval: .hours(1) + ) + #endif + } + + init( + messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.surveysRequest(), + messageStorage: SurveyRemoteMessagingStorage = DefaultSurveyRemoteMessagingStorage.surveys(), + accountManager: AccountManaging, + subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching, + vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), + pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), + networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + minimumRefreshInterval: TimeInterval, + userDefaults: UserDefaults = .standard + ) { + self.messageRequest = messageRequest + self.messageStorage = messageStorage + self.accountManager = accountManager + self.subscriptionFetcher = subscriptionFetcher + self.vpnActivationDateStore = vpnActivationDateStore + self.pirActivationDateStore = pirActivationDateStore + self.minimumRefreshInterval = minimumRefreshInterval + self.userDefaults = userDefaults + } + + func fetchRemoteMessages() async { + if let lastRefreshDate = lastRefreshDate(), lastRefreshDate.addingTimeInterval(minimumRefreshInterval) > Date() { + return + } + + let messageFetchResult = await self.messageRequest.fetchHomePageRemoteMessages() + + switch messageFetchResult { + case .success(let messages): + do { + let processedMessages = await self.process(messages: messages) + try self.messageStorage.store(messages: processedMessages) + self.updateLastRefreshDate() + } catch { + PixelKit.fire(DebugEvent(GeneralPixel.surveyRemoteMessageStorageFailed, error: error)) + } + case .failure(let error): + // Ignore 403 errors, those happen when a file can't be found on S3 + if case APIRequest.Error.invalidStatusCode(403) = error { + self.updateLastRefreshDate() + return + } + + PixelKit.fire(DebugEvent(GeneralPixel.surveyRemoteMessageFetchingFailed, error: error)) + } + } + + // swiftlint:disable cyclomatic_complexity function_body_length + + /// Processes the messages received from S3 and returns those which the user is eligible for. This is done by checking each of the attributes against the user's local state. + /// Because the result of the message fetch is cached, it means that they won't be immediately updated if the user suddenly qualifies, but the refresh interval for remote messages is only 1 hour so it + /// won't take long for the message to appear to the user. + private func process(messages: [SurveyRemoteMessage]) async -> [SurveyRemoteMessage] { + guard let token = accountManager.accessToken else { + return [] + } + + guard case let .success(subscription) = await subscriptionFetcher.getSubscription(accessToken: token) else { + return [] + } + + return messages.filter { message in + + var attributeMatchStatus = false + + // Check subscription status: + if let messageSubscriptionStatus = message.attributes.subscriptionStatus { + if let subscriptionStatus = Subscription.Status(rawValue: messageSubscriptionStatus) { + if subscription.status == subscriptionStatus { + attributeMatchStatus = true + } else { + return false + } + } else { + // If we received a subscription status but can't map it to a valid type, don't show the message. + return false + } + } + + // Check subscription billing period: + if let messageSubscriptionBillingPeriod = message.attributes.subscriptionBillingPeriod { + if let subscriptionBillingPeriod = Subscription.BillingPeriod(rawValue: messageSubscriptionBillingPeriod) { + if subscription.billingPeriod == subscriptionBillingPeriod { + attributeMatchStatus = true + } else { + return false + } + } else { + // If we received a subscription billing period but can't map it to a valid type, don't show the message. + return false + } + } + + // Check subscription start date: + if let messageDaysSinceSubscriptionStarted = message.attributes.minimumDaysSinceSubscriptionStarted { + guard let daysSinceSubscriptionStartDate = Calendar.current.dateComponents( + [.day], from: subscription.startedAt, to: Date() + ).day else { + return false + } + + if daysSinceSubscriptionStartDate >= messageDaysSinceSubscriptionStarted { + attributeMatchStatus = true + } else { + return false + } + } + + // Check subscription end/expiration date: + if let messageDaysUntilSubscriptionExpiration = message.attributes.maximumDaysUntilSubscriptionExpirationOrRenewal { + guard let daysUntilSubscriptionExpiration = Calendar.current.dateComponents( + [.day], from: subscription.expiresOrRenewsAt, to: Date() + ).day else { + return false + } + + if daysUntilSubscriptionExpiration <= messageDaysUntilSubscriptionExpiration { + attributeMatchStatus = true + } else { + return false + } + } + + // Check VPN usage: + if let requiredDaysSinceVPNActivation = message.attributes.daysSinceVPNEnabled { + if let daysSinceActivation = vpnActivationDateStore.daysSinceActivation(), requiredDaysSinceVPNActivation <= daysSinceActivation { + attributeMatchStatus = true + } else { + return false + } + } + + // Check PIR usage: + if let requiredDaysSincePIRActivation = message.attributes.daysSincePIREnabled { + if let daysSinceActivation = pirActivationDateStore.daysSinceActivation(), requiredDaysSincePIRActivation <= daysSinceActivation { + attributeMatchStatus = true + } else { + return false + } + } + + return attributeMatchStatus + + } + } + + // swiftlint:enable cyclomatic_complexity function_body_length + + func presentableRemoteMessages() -> [SurveyRemoteMessage] { + let dismissedMessageIDs = messageStorage.dismissedMessageIDs() + let possibleMessages: [SurveyRemoteMessage] = messageStorage.storedMessages() + + let filteredMessages = possibleMessages.filter { message in + if dismissedMessageIDs.contains(message.id) { + return false + } + + return true + + } + + return filteredMessages + } + + func dismiss(message: SurveyRemoteMessage) { + messageStorage.dismissRemoteMessage(with: message.id) + } + + func resetLastRefreshTimestamp() { + userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) + } + + // MARK: - Private + + private func lastRefreshDate() -> Date? { + guard let object = userDefaults.object(forKey: Constants.lastRefreshDateKey) else { + return nil + } + + guard let date = object as? Date else { + assertionFailure("Got rate limited date, but couldn't convert it to Date") + resetLastRefreshTimestamp() + return nil + } + + return date + } + + private func updateLastRefreshDate() { + userDefaults.setValue(Date(), forKey: Constants.lastRefreshDateKey) + } + +} + +extension SubscriptionService: SurveyRemoteMessageSubscriptionFetching { + + func getSubscription(accessToken: String) async -> Result { + return await self.getSubscription(accessToken: accessToken, cachePolicy: .returnCacheDataElseLoad) + } + +} diff --git a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift index a431276800..7392cb7f75 100644 --- a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift +++ b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift @@ -19,41 +19,60 @@ import Foundation import Common import BrowserServicesKit +import Subscription final class SurveyURLBuilder { enum SurveyURLParameters: String, CaseIterable { case atb = "atb" case atbVariant = "var" - case daysSinceActivated = "delta" - case macOSVersion = "mv" + case macOSVersion = "osv" case appVersion = "ddgv" case hardwareModel = "mo" - case lastDayActive = "da" + + case privacyProStatus = "ppro_status" + case privacyProPurchasePlatform = "ppro_platform" + case privacyProBillingPeriod = "ppro_billing" + case privacyProDaysSincePurchase = "ppro_days_since_purchase" + case privacyProDaysUntilExpiration = "ppro_days_until_exp" + + case vpnFirstUsed = "vpn_first_used" + case vpnLastUsed = "vpn_last_used" + case pirFirstUsed = "pir_first_used" + case pirLastUsed = "pir_last_used" } private let statisticsStore: StatisticsStore private let operatingSystemVersion: String private let appVersion: String private let hardwareModel: String? - private let daysSinceActivation: Int? - private let daysSinceLastActive: Int? + private let subscription: Subscription? + private let daysSinceVPNActivated: Int? + private let daysSinceVPNLastActive: Int? + private let daysSincePIRActivated: Int? + private let daysSincePIRLastActive: Int? init(statisticsStore: StatisticsStore, operatingSystemVersion: String, appVersion: String, hardwareModel: String?, - daysSinceActivation: Int?, - daysSinceLastActive: Int?) { + subscription: Subscription?, + daysSinceVPNActivated: Int?, + daysSinceVPNLastActive: Int?, + daysSincePIRActivated: Int?, + daysSincePIRLastActive: Int?) { self.statisticsStore = statisticsStore self.operatingSystemVersion = operatingSystemVersion self.appVersion = appVersion self.hardwareModel = hardwareModel - self.daysSinceActivation = daysSinceActivation - self.daysSinceLastActive = daysSinceLastActive + self.subscription = subscription + self.daysSinceVPNActivated = daysSinceVPNActivated + self.daysSinceVPNLastActive = daysSinceVPNLastActive + self.daysSincePIRActivated = daysSincePIRActivated + self.daysSincePIRLastActive = daysSincePIRLastActive } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length func buildSurveyURL(from originalURLString: String) -> URL? { guard var components = URLComponents(string: originalURLString) else { assertionFailure("Could not build components from survey URL") @@ -72,10 +91,6 @@ final class SurveyURLBuilder { if let variant = statisticsStore.variant { queryItems.append(queryItem(parameter: parameter, value: variant)) } - case .daysSinceActivated: - if let daysSinceActivation { - queryItems.append(queryItem(parameter: parameter, value: daysSinceActivation)) - } case .macOSVersion: queryItems.append(queryItem(parameter: parameter, value: operatingSystemVersion)) case .appVersion: @@ -84,9 +99,57 @@ final class SurveyURLBuilder { if let hardwareModel = hardwareModel { queryItems.append(queryItem(parameter: parameter, value: hardwareModel)) } - case .lastDayActive: - if let daysSinceLastActive { - queryItems.append(queryItem(parameter: parameter, value: daysSinceLastActive)) + case .vpnFirstUsed: + if let daysSinceVPNActivated { + queryItems.append(queryItem(parameter: parameter, value: daysSinceVPNActivated)) + } + case .vpnLastUsed: + if let daysSinceVPNLastActive { + queryItems.append(queryItem(parameter: parameter, value: daysSinceVPNLastActive)) + } + case .pirFirstUsed: + if let daysSincePIRActivated { + queryItems.append(queryItem(parameter: parameter, value: daysSincePIRActivated)) + } + case .pirLastUsed: + if let daysSincePIRLastActive { + queryItems.append(queryItem(parameter: parameter, value: daysSincePIRLastActive)) + } + case .privacyProStatus: + if let privacyProStatus = subscription?.status { + switch privacyProStatus { + case .autoRenewable: queryItems.append(queryItem(parameter: parameter, value: "auto_renewable")) + case .notAutoRenewable: queryItems.append(queryItem(parameter: parameter, value: "not_auto_renewable")) + case .gracePeriod: queryItems.append(queryItem(parameter: parameter, value: "grace_period")) + case .inactive: queryItems.append(queryItem(parameter: parameter, value: "inactive")) + case .expired: queryItems.append(queryItem(parameter: parameter, value: "expired")) + case .unknown: queryItems.append(queryItem(parameter: parameter, value: "unknown")) + } + } + case .privacyProPurchasePlatform: + if let privacyProPurchasePlatform = subscription?.platform { + switch privacyProPurchasePlatform { + case .apple: queryItems.append(queryItem(parameter: parameter, value: "apple")) + case .google: queryItems.append(queryItem(parameter: parameter, value: "google")) + case .stripe: queryItems.append(queryItem(parameter: parameter, value: "stripe")) + case .unknown: queryItems.append(queryItem(parameter: parameter, value: "unknown")) + } + } + case .privacyProBillingPeriod: + if let privacyProBillingPeriod = subscription?.billingPeriod { + switch privacyProBillingPeriod { + case .monthly: queryItems.append(queryItem(parameter: parameter, value: "monthly")) + case .yearly: queryItems.append(queryItem(parameter: parameter, value: "yearly")) + case .unknown: queryItems.append(queryItem(parameter: parameter, value: "unknown")) + } + } + case .privacyProDaysSincePurchase: + if let startedAt = subscription?.startedAt, let daysSincePurchase = daysSince(date: startedAt) { + queryItems.append(queryItem(parameter: parameter, value: daysSincePurchase)) + } + case .privacyProDaysUntilExpiration: + if let expiresOrRenewsAt = subscription?.expiresOrRenewsAt, let daysUntilExpiry = daysSince(date: expiresOrRenewsAt) { + queryItems.append(queryItem(parameter: parameter, value: daysUntilExpiry)) } } } @@ -132,4 +195,12 @@ final class SurveyURLBuilder { return bucket } + private func daysSince(date storedDate: Date) -> Int? { + if let days = Calendar.current.dateComponents([.day], from: storedDate, to: Date()).day { + return abs(days) + } + + return nil + } + } diff --git a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift b/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift deleted file mode 100644 index b43994834b..0000000000 --- a/DuckDuckGo/DBP/RemoteMessaging/DataBrokerProtectionRemoteMessaging.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// DataBrokerProtectionRemoteMessaging.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 Networking -import PixelKit - -#if DBP - -protocol DataBrokerProtectionRemoteMessaging { - - func fetchRemoteMessages(completion: (() -> Void)?) - func presentableRemoteMessages() -> [DataBrokerProtectionRemoteMessage] - func dismiss(message: DataBrokerProtectionRemoteMessage) - -} - -final class DefaultDataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging { - - enum Constants { - static let lastRefreshDateKey = "data-broker-protection.remote-messaging.last-refresh-date" - } - - private let messageRequest: HomePageRemoteMessagingRequest - private let messageStorage: HomePageRemoteMessagingStorage - private let waitlistStorage: WaitlistStorage - private let waitlistActivationDateStore: WaitlistActivationDateStore - private let dataBrokerProtectionVisibility: DataBrokerProtectionFeatureVisibility - private let minimumRefreshInterval: TimeInterval - private let userDefaults: UserDefaults - - convenience init() { - #if DEBUG || REVIEW - self.init(minimumRefreshInterval: .seconds(30)) - #else - self.init(minimumRefreshInterval: .hours(1)) - #endif - } - - init( - messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.dataBrokerProtectionMessagesRequest(), - messageStorage: HomePageRemoteMessagingStorage = DefaultHomePageRemoteMessagingStorage.dataBrokerProtection(), - waitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: "dbp", keychainAppGroup: Bundle.main.appGroup(bundle: .dbp)), - waitlistActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), - dataBrokerProtectionVisibility: DataBrokerProtectionFeatureVisibility = DefaultDataBrokerProtectionFeatureVisibility(), - minimumRefreshInterval: TimeInterval, - userDefaults: UserDefaults = .standard - ) { - self.messageRequest = messageRequest - self.messageStorage = messageStorage - self.waitlistStorage = waitlistStorage - self.waitlistActivationDateStore = waitlistActivationDateStore - self.dataBrokerProtectionVisibility = dataBrokerProtectionVisibility - self.minimumRefreshInterval = minimumRefreshInterval - self.userDefaults = userDefaults - } - - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - if let lastRefreshDate = lastRefreshDate(), lastRefreshDate.addingTimeInterval(minimumRefreshInterval) > Date() { - fetchCompletion?() - return - } - - self.messageRequest.fetchHomePageRemoteMessages { [weak self] result in - defer { - fetchCompletion?() - } - - guard let self else { return } - - // Cast the generic parameter to a concrete type: - let result: Result<[DataBrokerProtectionRemoteMessage], Error> = result - - switch result { - case .success(let messages): - do { - try self.messageStorage.store(messages: messages) - self.updateLastRefreshDate() // Update last refresh date on success, otherwise let the app try again next time - } catch { - PixelKit.fire(DebugEvent(GeneralPixel.dataBrokerProtectionRemoteMessageStorageFailed, error: error)) - } - case .failure(let error): - // Ignore 403 errors, those happen when a file can't be found on S3 - if case APIRequest.Error.invalidStatusCode(403) = error { - self.updateLastRefreshDate() // Avoid refreshing constantly when the file isn't available - return - } - - PixelKit.fire(DebugEvent(GeneralPixel.dataBrokerProtectionRemoteMessageFetchingFailed, error: error)) - } - } - } - - /// Uses the "days since DBP activated" count combined with the set of dismissed messages to determine which messages should be displayed to the user. - func presentableRemoteMessages() -> [DataBrokerProtectionRemoteMessage] { - let dismissedMessageIDs = messageStorage.dismissedMessageIDs() - let possibleMessages: [DataBrokerProtectionRemoteMessage] = messageStorage.storedMessages() - - // Only show messages that haven't been dismissed, and check whether they have a requirement on how long the user has used DBP for. - let filteredMessages = possibleMessages.filter { message in - - // Don't show messages that have already been dismissed. If you need to show the same message to a user again, - // it should get a new message ID. - if dismissedMessageIDs.contains(message.id) { - return false - } - - // First, check messages that require a number of days of DBP usage - if let requiredDaysSinceActivation = message.daysSinceDataBrokerProtectionEnabled, - let daysSinceActivation = waitlistActivationDateStore.daysSinceActivation() { - if requiredDaysSinceActivation <= daysSinceActivation { - return true - } else { - return false - } - } - - // Next, check if the message requires access to DBP but it's not visible: - if message.requiresDataBrokerProtectionAccess, !dataBrokerProtectionVisibility.isFeatureVisible() { - return false - } - - // Finally, check if the message requires DBP usage, and check if the user has used it at all: - if message.requiresDataBrokerProtectionUsage, waitlistActivationDateStore.daysSinceActivation() == nil { - return false - } - - return true - - } - - return filteredMessages - } - - func dismiss(message: DataBrokerProtectionRemoteMessage) { - messageStorage.dismissRemoteMessage(with: message.id) - } - - func resetLastRefreshTimestamp() { - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - } - - // MARK: - Private - - private func lastRefreshDate() -> Date? { - guard let object = userDefaults.object(forKey: Constants.lastRefreshDateKey) else { - return nil - } - - guard let date = object as? Date else { - assertionFailure("Got rate limited date, but couldn't convert it to Date") - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - return nil - } - - return date - } - - private func updateLastRefreshDate() { - userDefaults.setValue(Date(), forKey: Constants.lastRefreshDateKey) - } - -} - -#endif diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index df7ea99ccb..9522183014 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Common import Foundation import PixelKit +import Subscription import NetworkProtection import NetworkProtectionUI @@ -39,7 +40,7 @@ extension HomePage.Models { let gridWidth = FeaturesGridDimensions.width let deleteActionTitle = UserText.newTabSetUpRemoveItemAction let privacyConfigurationManager: PrivacyConfigurationManaging - let homePageRemoteMessaging: HomePageRemoteMessaging + let surveyRemoteMessaging: SurveyRemoteMessaging let permanentSurveyManager: SurveyManager var duckPlayerURL: String { @@ -53,6 +54,7 @@ extension HomePage.Models { private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager private let duckPlayerPreferences: DuckPlayerPreferencesPersistor + private let subscriptionManager: SubscriptionManaging @UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) var shouldShowAllFeatures: Bool { @@ -109,18 +111,20 @@ extension HomePage.Models { tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), duckPlayerPreferences: DuckPlayerPreferencesPersistor, - homePageRemoteMessaging: HomePageRemoteMessaging, + surveyRemoteMessaging: SurveyRemoteMessaging, privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, - permanentSurveyManager: SurveyManager = PermanentSurveyManager()) { + permanentSurveyManager: SurveyManager = PermanentSurveyManager(), + subscriptionManager: SubscriptionManaging = Application.appDelegate.subscriptionManager) { self.defaultBrowserProvider = defaultBrowserProvider self.dockCustomizer = dockCustomizer self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager self.duckPlayerPreferences = duckPlayerPreferences - self.homePageRemoteMessaging = homePageRemoteMessaging + self.surveyRemoteMessaging = surveyRemoteMessaging self.privacyConfigurationManager = privacyConfigurationManager self.permanentSurveyManager = permanentSurveyManager + self.subscriptionManager = subscriptionManager refreshFeaturesMatrix() @@ -142,9 +146,7 @@ extension HomePage.Models { performEmailProtectionAction() case .permanentSurvey: visitSurvey() - case .networkProtectionRemoteMessage(let message): - handle(remoteMessage: message) - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): handle(remoteMessage: message) case .dataBrokerProtectionWaitlistInvited: performDataBrokerProtectionWaitlistInvitedAction() @@ -205,14 +207,9 @@ extension HomePage.Models { shouldShowEmailProtectionSetting = false case .permanentSurvey: shouldShowPermanentSurvey = false - case .networkProtectionRemoteMessage(let message): - homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: message) - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDismissed(messageID: message.id)) - case .dataBrokerProtectionRemoteMessage(let message): -#if DBP - homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: message) - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageDismissed(messageID: message.id)) -#endif + case .surveyRemoteMessage(let message): + surveyRemoteMessaging.dismiss(message: message) + PixelKit.fire(GeneralPixel.surveyRemoteMessageDismissed(messageID: message.id)) case .dataBrokerProtectionWaitlistInvited: shouldShowDBPWaitlistInvitedCardUI = false } @@ -221,20 +218,16 @@ extension HomePage.Models { func refreshFeaturesMatrix() { var features: [FeatureType] = [] -#if DBP + if shouldDBPWaitlistCardBeVisible { features.append(.dataBrokerProtectionWaitlistInvited) } - for message in homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.presentableRemoteMessages() { - features.append(.dataBrokerProtectionRemoteMessage(message)) - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) + for message in surveyRemoteMessaging.presentableRemoteMessages() { + features.append(.surveyRemoteMessage(message)) + PixelKit.fire(GeneralPixel.surveyRemoteMessageDisplayed(messageID: message.id), frequency: .daily) } -#endif - for message in homePageRemoteMessaging.networkProtectionRemoteMessaging.presentableRemoteMessages() { - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) - } appendFeatureCards(&features) featuresMatrix = features.chunked(into: itemsPerRow) @@ -260,8 +253,7 @@ extension HomePage.Models { return shouldEmailProtectionCardBeVisible case .permanentSurvey: return shouldPermanentSurveyBeVisible - case .networkProtectionRemoteMessage, - .dataBrokerProtectionRemoteMessage, + case .surveyRemoteMessage, .dataBrokerProtectionWaitlistInvited: return false // These are handled separately } @@ -347,7 +339,8 @@ extension HomePage.Models { private var shouldPermanentSurveyBeVisible: Bool { return shouldShowPermanentSurvey && - permanentSurveyManager.isSurveyAvailable + permanentSurveyManager.isSurveyAvailable && + surveyRemoteMessaging.presentableRemoteMessages().isEmpty // When Privacy Pro survey is visible, ensure we do not show multiple at once } @MainActor private func visitSurvey() { @@ -358,55 +351,44 @@ extension HomePage.Models { shouldShowPermanentSurvey = false } - @MainActor private func handle(remoteMessage: NetworkProtectionRemoteMessage) { + @MainActor private func handle(remoteMessage: SurveyRemoteMessage) { guard let actionType = remoteMessage.action.actionType else { - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDismissed(messageID: remoteMessage.id)) - homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: remoteMessage) + PixelKit.fire(GeneralPixel.surveyRemoteMessageDismissed(messageID: remoteMessage.id)) + surveyRemoteMessaging.dismiss(message: remoteMessage) refreshFeaturesMatrix() return } switch actionType { - case .openNetworkProtection: - NotificationCenter.default.post(name: .ToggleNetworkProtectionInMainWindow, object: nil) case .openSurveyURL, .openURL: - if let surveyURL = remoteMessage.presentableSurveyURL() { - let tab = Tab(content: .url(surveyURL, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageOpened(messageID: remoteMessage.id)) - - // Dismiss the message after the user opens the URL, even if they just close the tab immediately afterwards. - homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() + Task { @MainActor in + var subscription: Subscription? + + if let token = subscriptionManager.accountManager.accessToken { + switch await subscriptionManager.subscriptionService.getSubscription( + accessToken: token, + cachePolicy: .returnCacheDataElseLoad + ) { + case .success(let fetchedSubscription): + subscription = fetchedSubscription + case .failure: + break + } + } + + if let surveyURL = remoteMessage.presentableSurveyURL(subscription: subscription) { + let tab = Tab(content: .url(surveyURL, source: .ui), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + PixelKit.fire(GeneralPixel.surveyRemoteMessageOpened(messageID: remoteMessage.id)) + + // Dismiss the message after the user opens the URL, even if they just close the tab immediately afterwards. + surveyRemoteMessaging.dismiss(message: remoteMessage) + refreshFeaturesMatrix() + } } } } - @MainActor private func handle(remoteMessage: DataBrokerProtectionRemoteMessage) { -#if DBP - guard let actionType = remoteMessage.action.actionType else { - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageDismissed(messageID: remoteMessage.id)) - homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() - return - } - - switch actionType { - case .openDataBrokerProtection: - break // Not used currently - case .openSurveyURL, .openURL: - if let surveyURL = remoteMessage.presentableSurveyURL() { - let tab = Tab(content: .url(surveyURL, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - PixelKit.fire(GeneralPixel.dataBrokerProtectionRemoteMessageOpened(messageID: remoteMessage.id)) - - // Dismiss the message after the user opens the URL, even if they just close the tab immediately afterwards. - homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: remoteMessage) - refreshFeaturesMatrix() - } - } -#endif - } } // MARK: Feature Type @@ -429,8 +411,7 @@ extension HomePage.Models { case dock case importBookmarksAndPasswords case permanentSurvey - case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage) - case dataBrokerProtectionRemoteMessage(DataBrokerProtectionRemoteMessage) + case surveyRemoteMessage(SurveyRemoteMessage) case dataBrokerProtectionWaitlistInvited var title: String { @@ -447,9 +428,7 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionCardTitle case .permanentSurvey: return PermanentSurveyManager.title - case .networkProtectionRemoteMessage(let message): - return message.cardTitle - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): return message.cardTitle case .dataBrokerProtectionWaitlistInvited: return "Personal Information Removal" @@ -470,9 +449,7 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionSummary case .permanentSurvey: return PermanentSurveyManager.body - case .networkProtectionRemoteMessage(let message): - return message.cardDescription - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): return message.cardDescription case .dataBrokerProtectionWaitlistInvited: return "You're invited to try Personal Information Removal beta!" @@ -493,9 +470,7 @@ extension HomePage.Models { return UserText.newTabSetUpEmailProtectionAction case .permanentSurvey: return PermanentSurveyManager.actionTitle - case .networkProtectionRemoteMessage(let message): - return message.action.actionTitle - case .dataBrokerProtectionRemoteMessage(let message): + case .surveyRemoteMessage(let message): return message.action.actionTitle case .dataBrokerProtectionWaitlistInvited: return "Get Started" @@ -527,10 +502,8 @@ extension HomePage.Models { return .inbox128.resized(to: iconSize)! case .permanentSurvey: return .survey128.resized(to: iconSize)! - case .networkProtectionRemoteMessage: - return .vpnEnded.resized(to: iconSize)! - case .dataBrokerProtectionRemoteMessage: - return .dbpInformationRemover.resized(to: iconSize)! + case .surveyRemoteMessage: + return .privacyProSurvey.resized(to: iconSize)! case .dataBrokerProtectionWaitlistInvited: return .dbpInformationRemover.resized(to: iconSize)! } @@ -553,34 +526,6 @@ extension HomePage.Models { // MARK: - Remote Messaging -struct HomePageRemoteMessaging { - - static func defaultMessaging() -> HomePageRemoteMessaging { -#if DBP - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: .netP, - dataBrokerProtectionRemoteMessaging: DefaultDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: .dbp - ) -#else - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: .netP - ) -#endif - } - - let networkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging - let networkProtectionUserDefaults: UserDefaults - -#if DBP - let dataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging - let dataBrokerProtectionUserDefaults: UserDefaults -#endif - -} - extension AppVersion { public var majorAndMinorOSVersion: String { let components = osVersion.split(separator: ".") diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 092a33384c..224f0f93f4 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -154,7 +154,9 @@ final class HomePageViewController: NSViewController { dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), - homePageRemoteMessaging: .defaultMessaging() + surveyRemoteMessaging: DefaultSurveyRemoteMessaging( + subscriptionManager: Application.appDelegate.subscriptionManager + ) ) } diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 113e525dca..8d4c5852a9 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -192,13 +192,8 @@ final class MainViewController: NSViewController { updateReloadMenuItem() updateStopMenuItem() browserTabViewController.windowDidBecomeKey() - - refreshNetworkProtectionMessages() - -#if DBP + refreshSurveyMessages() DataBrokerProtectionAppEvents().windowDidBecomeMain() - refreshDataBrokerProtectionMessages() -#endif } func windowDidResignKey() { @@ -220,19 +215,15 @@ final class MainViewController: NSViewController { } } - private let networkProtectionMessaging = DefaultNetworkProtectionRemoteMessaging() - - func refreshNetworkProtectionMessages() { - networkProtectionMessaging.fetchRemoteMessages() - } - -#if DBP - private let dataBrokerProtectionMessaging = DefaultDataBrokerProtectionRemoteMessaging() + private lazy var surveyMessaging: DefaultSurveyRemoteMessaging = { + return DefaultSurveyRemoteMessaging(subscriptionManager: Application.appDelegate.subscriptionManager) + }() - func refreshDataBrokerProtectionMessages() { - dataBrokerProtectionMessaging.fetchRemoteMessages() + func refreshSurveyMessages() { + Task { + await surveyMessaging.fetchRemoteMessages() + } } -#endif override func encodeRestorableState(with coder: NSCoder) { fatalError("Default AppKit State Restoration should not be used") diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 3a41e65525..c7e3b6d6ae 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -642,6 +642,10 @@ import SubscriptionUI currentViewController: { WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController }, subscriptionManager: Application.appDelegate.subscriptionManager) + NSMenuItem(title: "Privacy Pro Survey") { + NSMenuItem(title: "Reset Remote Message Cache", action: #selector(MainViewController.resetSurveyRemoteMessages)) + } + NSMenuItem(title: "Logging").submenu(setupLoggingMenu()) } debugMenu.addItem(internalUserItem) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index cef3e05a90..898f51ea74 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -927,6 +927,11 @@ extension MainViewController { setConfigurationUrl(nil) } + @objc func resetSurveyRemoteMessages(_ sender: Any?) { + DefaultSurveyRemoteMessagingStorage.surveys().removeStoredAndDismissedMessages() + DefaultSurveyRemoteMessaging(subscriptionManager: Application.appDelegate.subscriptionManager).resetLastRefreshTimestamp() + } + // MARK: - Developer Tools @objc func toggleDeveloperTools(_ sender: Any?) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 3cd6f11e63..23fb71b7ab 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -76,9 +76,6 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Remove Network Extension and Login Items", action: #selector(NetworkProtectionDebugMenu.removeSystemExtensionAndAgents)) .targetting(self) - - NSMenuItem(title: "Reset Remote Messages", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionRemoteMessages)) - .targetting(self) } NSMenuItem.separator() @@ -483,11 +480,6 @@ final class NetworkProtectionDebugMenu: NSMenu { overrideNetworkProtectionActivationDate(to: nil) } - @objc func resetNetworkProtectionRemoteMessages(_ sender: Any?) { - DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() - DefaultNetworkProtectionRemoteMessaging(minimumRefreshInterval: 0).resetLastRefreshTimestamp() - } - @objc func overrideNetworkProtectionActivationDateToNow(_ sender: Any?) { overrideNetworkProtectionActivationDate(to: Date()) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index cf810d9225..aa2c99a004 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -59,8 +59,6 @@ final class NetworkProtectionDebugUtilities { settings.resetToDefaults() - DefaultHomePageRemoteMessagingStorage.networkProtection().removeStoredAndDismissedMessages() - UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.networkProtectionTermsAndConditionsAccepted.rawValue) NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) UserDefaults.netP.networkProtectionEntitlementsExpired = false diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift deleted file mode 100644 index 2f5f6ce6fc..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessage.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// NetworkProtectionRemoteMessage.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 Common - -struct NetworkProtectionRemoteMessageAction: Codable, Equatable, Hashable { - enum Action: String, Codable { - case openNetworkProtection - case openSurveyURL - case openURL - } - - let actionTitle: String - let actionType: Action? - let actionURL: String? -} - -struct NetworkProtectionRemoteMessage: Codable, Equatable, Identifiable, Hashable { - - let id: String - let cardTitle: String - let cardDescription: String - /// If this is set, the message won't be displayed if NetP hasn't been used, even if the usage and access booleans are false - let daysSinceNetworkProtectionEnabled: Int? - let requiresNetworkProtectionUsage: Bool - let requiresNetworkProtectionAccess: Bool - let action: NetworkProtectionRemoteMessageAction - - func presentableSurveyURL( - statisticsStore: StatisticsStore = LocalStatisticsStore(), - activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description, - appVersion: String = AppVersion.shared.versionNumber, - hardwareModel: String? = HardwareModel.model - ) -> URL? { - if let actionType = action.actionType, actionType == .openURL, let urlString = action.actionURL, let url = URL(string: urlString) { - return url - } - - guard let actionType = action.actionType, actionType == .openSurveyURL, let surveyURL = action.actionURL else { - return nil - } - - let surveyURLBuilder = SurveyURLBuilder( - statisticsStore: statisticsStore, - operatingSystemVersion: operatingSystemVersion, - appVersion: appVersion, - hardwareModel: hardwareModel, - daysSinceActivation: activationDateStore.daysSinceActivation(), - daysSinceLastActive: activationDateStore.daysSinceLastActive() - ) - - return surveyURLBuilder.buildSurveyURL(from: surveyURL) - } -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift deleted file mode 100644 index aef4e46516..0000000000 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// NetworkProtectionRemoteMessaging.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 Networking -import PixelKit - -protocol NetworkProtectionRemoteMessaging { - - func fetchRemoteMessages(completion: (() -> Void)?) - func presentableRemoteMessages() -> [NetworkProtectionRemoteMessage] - func dismiss(message: NetworkProtectionRemoteMessage) - -} - -final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging { - - enum Constants { - static let lastRefreshDateKey = "network-protection.remote-messaging.last-refresh-date" - } - - private let messageRequest: HomePageRemoteMessagingRequest - private let messageStorage: HomePageRemoteMessagingStorage - private let waitlistStorage: WaitlistStorage - private let waitlistActivationDateStore: WaitlistActivationDateStore - private let networkProtectionVisibility: NetworkProtectionFeatureVisibility - private let minimumRefreshInterval: TimeInterval - private let userDefaults: UserDefaults - - convenience init() { - #if DEBUG || REVIEW - self.init(minimumRefreshInterval: .seconds(30)) - #else - self.init(minimumRefreshInterval: .hours(1)) - #endif - } - - init( - messageRequest: HomePageRemoteMessagingRequest = DefaultHomePageRemoteMessagingRequest.networkProtectionMessagesRequest(), - messageStorage: HomePageRemoteMessagingStorage = DefaultHomePageRemoteMessagingStorage.networkProtection(), - waitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: "networkprotection", keychainAppGroup: Bundle.main.appGroup(bundle: .netP)), - waitlistActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), - minimumRefreshInterval: TimeInterval, - userDefaults: UserDefaults = .standard - ) { - self.messageRequest = messageRequest - self.messageStorage = messageStorage - self.waitlistStorage = waitlistStorage - self.waitlistActivationDateStore = waitlistActivationDateStore - self.networkProtectionVisibility = networkProtectionVisibility - self.minimumRefreshInterval = minimumRefreshInterval - self.userDefaults = userDefaults - } - - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - - if let lastRefreshDate = lastRefreshDate(), lastRefreshDate.addingTimeInterval(minimumRefreshInterval) > Date() { - fetchCompletion?() - return - } - - self.messageRequest.fetchHomePageRemoteMessages { [weak self] result in - defer { - fetchCompletion?() - } - - guard let self else { return } - - // Cast the generic parameter to a concrete type: - let result: Result<[NetworkProtectionRemoteMessage], Error> = result - - switch result { - case .success(let messages): - do { - try self.messageStorage.store(messages: messages) - self.updateLastRefreshDate() // Update last refresh date on success, otherwise let the app try again next time - } catch { - PixelKit.fire(DebugEvent(GeneralPixel.networkProtectionRemoteMessageStorageFailed, error: error)) - } - case .failure(let error): - // Ignore 403 errors, those happen when a file can't be found on S3 - if case APIRequest.Error.invalidStatusCode(403) = error { - self.updateLastRefreshDate() // Avoid refreshing constantly when the file isn't available - return - } - - PixelKit.fire(DebugEvent(GeneralPixel.networkProtectionRemoteMessageFetchingFailed, error: error)) - } - } - - } - - /// Uses the "days since VPN activated" count combined with the set of dismissed messages to determine which messages should be displayed to the user. - func presentableRemoteMessages() -> [NetworkProtectionRemoteMessage] { - let dismissedMessageIDs = messageStorage.dismissedMessageIDs() - let possibleMessages: [NetworkProtectionRemoteMessage] = messageStorage.storedMessages() - - // Only show messages that haven't been dismissed, and check whether they have a - // requirement on how long the user has used the VPN for. - let filteredMessages = possibleMessages.filter { message in - - // Don't show messages that have already been dismissed. If you need to show the same message to a user again, - // it should get a new message ID. - if dismissedMessageIDs.contains(message.id) { - return false - } - - // First, check messages that require a number of days of NetP usage - if let requiredDaysSinceActivation = message.daysSinceNetworkProtectionEnabled, - let daysSinceActivation = waitlistActivationDateStore.daysSinceActivation() { - if requiredDaysSinceActivation <= daysSinceActivation { - return true - } else { - return false - } - } - - // Next, check if the message requires access to NetP but it's not visible: - if message.requiresNetworkProtectionAccess, !networkProtectionVisibility.isVPNVisible() { - return false - } - - // Finally, check if the message requires NetP usage, and check if the user has used it at all: - if message.requiresNetworkProtectionUsage, waitlistActivationDateStore.daysSinceActivation() == nil { - return false - } - - return true - - } - - return filteredMessages - } - - func dismiss(message: NetworkProtectionRemoteMessage) { - messageStorage.dismissRemoteMessage(with: message.id) - } - - func resetLastRefreshTimestamp() { - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - } - - // MARK: - Private - - private func lastRefreshDate() -> Date? { - guard let object = userDefaults.object(forKey: Constants.lastRefreshDateKey) else { - return nil - } - - guard let date = object as? Date else { - assertionFailure("Got rate limited date, but couldn't convert it to Date") - userDefaults.removeObject(forKey: Constants.lastRefreshDateKey) - return nil - } - - return date - } - - private func updateLastRefreshDate() { - userDefaults.setValue(Date(), forKey: Constants.lastRefreshDateKey) - } - -} diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift index e7e56043b8..39914a5dad 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -212,8 +212,11 @@ final class AutofillPreferencesModel: ObservableObject { operatingSystemVersion: operatingSystemVersion, appVersion: appVersion, hardwareModel: hardwareModel, - daysSinceActivation: activationDateStore.daysSinceActivation(), - daysSinceLastActive: activationDateStore.daysSinceLastActive() + 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 { diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 6184263d85..42fb1feab6 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -117,13 +117,14 @@ enum GeneralPixel: PixelKitEventV2 { case dashboardProtectionAllowlistAdd(triggerOrigin: String?) case dashboardProtectionAllowlistRemove(triggerOrigin: String?) + // Survey + case surveyRemoteMessageDisplayed(messageID: String) + case surveyRemoteMessageDismissed(messageID: String) + case surveyRemoteMessageOpened(messageID: String) + // VPN case vpnBreakageReport(category: String, description: String, metadata: String) - // VPN - case networkProtectionRemoteMessageDisplayed(messageID: String) - case networkProtectionRemoteMessageDismissed(messageID: String) - case networkProtectionRemoteMessageOpened(messageID: String) case networkProtectionEnabledOnSearch case networkProtectionGeoswitchingOpened case networkProtectionGeoswitchingSetNearest @@ -152,9 +153,6 @@ enum GeneralPixel: PixelKitEventV2 { case dataBrokerProtectionWaitlistCardUITapped case dataBrokerProtectionWaitlistTermsAndConditionsDisplayed case dataBrokerProtectionWaitlistTermsAndConditionsAccepted - case dataBrokerProtectionRemoteMessageDisplayed(messageID: String) - case dataBrokerProtectionRemoteMessageDismissed(messageID: String) - case dataBrokerProtectionRemoteMessageOpened(messageID: String) // Login Item events case dataBrokerEnableLoginItemDaily @@ -343,11 +341,8 @@ enum GeneralPixel: PixelKitEventV2 { case burnerTabMisplaced - case networkProtectionRemoteMessageFetchingFailed - case networkProtectionRemoteMessageStorageFailed - case dataBrokerProtectionRemoteMessageFetchingFailed - case dataBrokerProtectionRemoteMessageStorageFailed - + case surveyRemoteMessageFetchingFailed + case surveyRemoteMessageStorageFailed case loginItemUpdateError(loginItemBundleID: String, action: String, buildType: String, osVersion: String) // Tracks installation without tracking retention. @@ -530,12 +525,12 @@ enum GeneralPixel: PixelKitEventV2 { case .vpnBreakageReport: return "m_mac_vpn_breakage_report" - case .networkProtectionRemoteMessageDisplayed(let messageID): - return "m_mac_netp_remote_message_displayed_\(messageID)" - case .networkProtectionRemoteMessageDismissed(let messageID): - return "m_mac_netp_remote_message_dismissed_\(messageID)" - case .networkProtectionRemoteMessageOpened(let messageID): - return "m_mac_netp_remote_message_opened_\(messageID)" + case .surveyRemoteMessageDisplayed(let messageID): + return "m_mac_survey_remote_message_displayed_\(messageID)" + case .surveyRemoteMessageDismissed(let messageID): + return "m_mac_survey_remote_message_dismissed_\(messageID)" + case .surveyRemoteMessageOpened(let messageID): + return "m_mac_survey_remote_message_opened_\(messageID)" case .networkProtectionEnabledOnSearch: return "m_mac_netp_ev_enabled_on_search" @@ -575,12 +570,6 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_dbp_imp_terms" case .dataBrokerProtectionWaitlistTermsAndConditionsAccepted: return "m_mac_dbp_ev_terms_accepted" - case .dataBrokerProtectionRemoteMessageDisplayed(let messageID): - return "m_mac_dbp_remote_message_displayed_\(messageID)" - case .dataBrokerProtectionRemoteMessageDismissed(let messageID): - return "m_mac_dbp_remote_message_dismissed_\(messageID)" - case .dataBrokerProtectionRemoteMessageOpened(let messageID): - return "m_mac_dbp_remote_message_opened_\(messageID)" case .dataBrokerEnableLoginItemDaily: return "m_mac_dbp_daily_login-item_enable" case .dataBrokerDisableLoginItemDaily: return "m_mac_dbp_daily_login-item_disable" @@ -863,12 +852,8 @@ enum GeneralPixel: PixelKitEventV2 { case .burnerTabMisplaced: return "burner_tab_misplaced" - case .networkProtectionRemoteMessageFetchingFailed: return "netp_remote_message_fetching_failed" - case .networkProtectionRemoteMessageStorageFailed: return "netp_remote_message_storage_failed" - - case .dataBrokerProtectionRemoteMessageFetchingFailed: return "dbp_remote_message_fetching_failed" - case .dataBrokerProtectionRemoteMessageStorageFailed: return "dbp_remote_message_storage_failed" - + case .surveyRemoteMessageFetchingFailed: return "survey_remote_message_fetching_failed" + case .surveyRemoteMessageStorageFailed: return "survey_remote_message_storage_failed" case .loginItemUpdateError: return "login-item_update-error" // Installation Attribution diff --git a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index cd6186444d..eebcb0f3d9 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -50,10 +50,7 @@ let extraInputFiles: [TargetName: Set] = [ "Unit Tests": [ .init("BWEncryptionTests.swift", .source), - .init("WKWebViewPrivateMethodsAvailabilityTests.swift", .source), - .init("NetworkProtectionRemoteMessageTests.swift", .source), - .init("network-protection-messages.json", .resource), - .init("dbp-messages.json", .resource), + .init("WKWebViewPrivateMethodsAvailabilityTests.swift", .source) ], "Integration Tests": [] diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 9d49ab4073..0986fc1150 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -21,42 +21,22 @@ import BrowserServicesKit import Common @testable import DuckDuckGo_Privacy_Browser -final class MockNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging { +final class MockSurveyRemoteMessaging: SurveyRemoteMessaging { - var messages: [NetworkProtectionRemoteMessage] = [] + var messages: [SurveyRemoteMessage] = [] - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - fetchCompletion?() + func fetchRemoteMessages() async { + return } - func presentableRemoteMessages() -> [NetworkProtectionRemoteMessage] { + func presentableRemoteMessages() -> [SurveyRemoteMessage] { messages } - func dismiss(message: NetworkProtectionRemoteMessage) {} + func dismiss(message: SurveyRemoteMessage) {} } -#if DBP - -final class MockDataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging { - - var messages: [DataBrokerProtectionRemoteMessage] = [] - - func fetchRemoteMessages(completion fetchCompletion: (() -> Void)? = nil) { - fetchCompletion?() - } - - func presentableRemoteMessages() -> [DataBrokerProtectionRemoteMessage] { - messages - } - - func dismiss(message: DataBrokerProtectionRemoteMessage) {} - -} - -#endif - final class ContinueSetUpModelTests: XCTestCase { var vm: HomePage.Models.ContinueSetUpModel! @@ -70,6 +50,7 @@ final class ContinueSetUpModelTests: XCTestCase { var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! var dockCustomizer: DockCustomization! + var mockSurveyMessaging: SurveyRemoteMessaging! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @MainActor override func setUp() { @@ -87,22 +68,9 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() + mockSurveyMessaging = MockSurveyRemoteMessaging() dockCustomizer = DockCustomizerMock() -#if DBP - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults, - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: userDefaults - ) -#else - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults - ) -#endif - vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, @@ -110,7 +78,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: messaging, + surveyRemoteMessaging: mockSurveyMessaging, privacyConfigurationManager: privacyConfigManager, permanentSurveyManager: MockPermanentSurveyManager() ) @@ -154,7 +122,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: createMessaging(), + surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -368,7 +336,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: createMessaging(), + surveyRemoteMessaging: createMessaging(), permanentSurveyManager: MockPermanentSurveyManager() ) @@ -467,7 +435,7 @@ final class ContinueSetUpModelTests: XCTestCase { XCTAssertFalse(vm2.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.permanentSurvey)) } - @MainActor func testWhenAskedToPerformActionForPermanetShowsTheSurveySite() async { + @MainActor func testWhenAskedToPerformActionForPermanentShowsTheSurveySite() async { let expectedURL = URL(string: "someurl.com") let surveyManager = MockPermanentSurveyManager(isSurveyAvailable: true, url: expectedURL) userDefaults.set(true, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) @@ -478,7 +446,7 @@ final class ContinueSetUpModelTests: XCTestCase { tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: createMessaging(), + surveyRemoteMessaging: createMessaging(), privacyConfigurationManager: privacyConfigManager, permanentSurveyManager: surveyManager ) @@ -509,20 +477,8 @@ final class ContinueSetUpModelTests: XCTestCase { return features.chunked(into: HomePage.featuresPerRow) } - private func createMessaging() -> HomePageRemoteMessaging { -#if DBP - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults, - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: userDefaults - ) -#else - return HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: userDefaults - ) -#endif + private func createMessaging() -> SurveyRemoteMessaging { + MockSurveyRemoteMessaging() } @MainActor func test_WhenUserDoesntHaveApplicationInTheDock_ThenAddToDockCardIsDisplayed() { @@ -566,20 +522,6 @@ extension HomePage.Models.ContinueSetUpModel { let manager = MockPrivacyConfigurationManager() manager.privacyConfig = privacyConfig -#if DBP - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: appGroupUserDefaults, - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: appGroupUserDefaults - ) -#else - let messaging = HomePageRemoteMessaging( - networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), - networkProtectionUserDefaults: appGroupUserDefaults - ) -#endif - return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: defaultBrowserProvider, dockCustomizer: dockCustomizer, @@ -587,7 +529,7 @@ extension HomePage.Models.ContinueSetUpModel { tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, - homePageRemoteMessaging: messaging, + surveyRemoteMessaging: MockSurveyRemoteMessaging(), privacyConfigurationManager: manager, permanentSurveyManager: permanentSurveyManager) } diff --git a/UnitTests/HomePage/Resources/survey-messages.json b/UnitTests/HomePage/Resources/survey-messages.json new file mode 100644 index 0000000000..1259271b49 --- /dev/null +++ b/UnitTests/HomePage/Resources/survey-messages.json @@ -0,0 +1,20 @@ +[ + { + "id": "message-1", + "cardTitle": "Title 1", + "cardDescription": "Description 1", + "attributes": { + "subscriptionStatus": "Auto-Renewable", + "subscriptionBillingPeriod": "Monthly", + "minimumDaysSinceSubscriptionStarted": 1, + "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, + "daysSinceVPNEnabled": 2, + "daysSincePIREnabled": 3 + }, + "action": { + "actionTitle": "Action 1", + "actionType": "openSurveyURL", + "actionURL": "https://duckduckgo.com/" + } + } +] diff --git a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessageTests.swift b/UnitTests/HomePage/SurveyRemoteMessageTests.swift similarity index 52% rename from UnitTests/NetworkProtection/NetworkProtectionRemoteMessageTests.swift rename to UnitTests/HomePage/SurveyRemoteMessageTests.swift index 1a791b92e9..1c7939b36f 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessageTests.swift +++ b/UnitTests/HomePage/SurveyRemoteMessageTests.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionRemoteMessageTests.swift +// SurveyRemoteMessageTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,7 +19,7 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser -final class NetworkProtectionRemoteMessageTests: XCTestCase { +final class SurveyRemoteMessageTests: XCTestCase { func testWhenDecodingMessages_ThenMessagesDecodeSuccessfully() throws { let mockStatisticsStore = MockStatisticsStore() @@ -34,66 +34,32 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { let data = try Data(contentsOf: fileURL) let decoder = JSONDecoder() - let decodedMessages = try decoder.decode([NetworkProtectionRemoteMessage].self, from: data) + let decodedMessages = try decoder.decode([SurveyRemoteMessage].self, from: data) - XCTAssertEqual(decodedMessages.count, 3) + XCTAssertEqual(decodedMessages.count, 1) - guard let firstMessage = decodedMessages.first(where: { $0.id == "123"}) else { + guard let firstMessage = decodedMessages.first(where: { $0.id == "message-1"}) else { XCTFail("Failed to find expected message") return } let firstMessagePresentableSurveyURL = firstMessage.presentableSurveyURL( statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, + vpnActivationDateStore: mockActivationDateStore, operatingSystemVersion: "1.2.3", appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" + hardwareModel: "MacBookPro,123", + subscription: nil ) XCTAssertEqual(firstMessage.cardTitle, "Title 1") XCTAssertEqual(firstMessage.cardDescription, "Description 1") XCTAssertEqual(firstMessage.action.actionTitle, "Action 1") - XCTAssertNil(firstMessagePresentableSurveyURL) - XCTAssertNil(firstMessage.daysSinceNetworkProtectionEnabled) - - guard let secondMessage = decodedMessages.first(where: { $0.id == "456"}) else { - XCTFail("Failed to find expected message") - return - } - - let secondMessagePresentableSurveyURL = secondMessage.presentableSurveyURL( - statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, - operatingSystemVersion: "1.2.3", - appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" - ) - - XCTAssertEqual(secondMessage.daysSinceNetworkProtectionEnabled, 1) - XCTAssertEqual(secondMessage.cardTitle, "Title 2") - XCTAssertEqual(secondMessage.cardDescription, "Description 2") - XCTAssertEqual(secondMessage.action.actionTitle, "Action 2") - XCTAssertNil(secondMessagePresentableSurveyURL) - - guard let thirdMessage = decodedMessages.first(where: { $0.id == "789"}) else { - XCTFail("Failed to find expected message") - return - } - - let thirdMessagePresentableSurveyURL = thirdMessage.presentableSurveyURL( - statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, - operatingSystemVersion: "1.2.3", - appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" - ) - - XCTAssertEqual(thirdMessage.daysSinceNetworkProtectionEnabled, 5) - XCTAssertEqual(thirdMessage.cardTitle, "Title 3") - XCTAssertEqual(thirdMessage.cardDescription, "Description 3") - XCTAssertEqual(thirdMessage.action.actionTitle, "Action 3") - XCTAssertTrue(thirdMessagePresentableSurveyURL!.absoluteString.hasPrefix("https://duckduckgo.com/")) + XCTAssertEqual(firstMessage.attributes.minimumDaysSinceSubscriptionStarted, 1) + XCTAssertEqual(firstMessage.attributes.daysSinceVPNEnabled, 2) + XCTAssertEqual(firstMessage.attributes.daysSincePIREnabled, 3) + XCTAssertEqual(firstMessage.attributes.maximumDaysUntilSubscriptionExpirationOrRenewal, 30) + XCTAssertNotNil(firstMessagePresentableSurveyURL) } func testWhenGettingSurveyURL_AndSurveyURLHasParameters_ThenParametersAreReplaced() { @@ -103,8 +69,13 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { "daysSinceNetworkProtectionEnabled": 0, "cardTitle": "Title", "cardDescription": "Description", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, + "attributes": { + "subscriptionStatus": "", + "minimumDaysSinceSubscriptionStarted": 1, + "maximumDaysUntilSubscriptionExpirationOrRenewal": 30, + "daysSinceVPNEnabled": 1, + "daysSincePIREnabled": 1 + }, "action": { "actionTitle": "Action", "actionType": "openSurveyURL", @@ -114,9 +85,9 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { """ let decoder = JSONDecoder() - let message: NetworkProtectionRemoteMessage + let message: SurveyRemoteMessage do { - message = try decoder.decode(NetworkProtectionRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) + message = try decoder.decode(SurveyRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) } catch { XCTFail("Failed to decode with error: \(error.localizedDescription)") return @@ -132,19 +103,23 @@ final class NetworkProtectionRemoteMessageTests: XCTestCase { let presentableSurveyURL = message.presentableSurveyURL( statisticsStore: mockStatisticsStore, - activationDateStore: mockActivationDateStore, + vpnActivationDateStore: mockActivationDateStore, operatingSystemVersion: "1.2.3", appVersion: "4.5.6", - hardwareModel: "MacBookPro,123" + hardwareModel: "MacBookPro,123", + subscription: nil ) - let expectedURL = "https://duckduckgo.com/?atb=atb-123&var=variant&delta=2&mv=1.2.3&ddgv=4.5.6&mo=MacBookPro%252C123&da=1" + let expectedURL = """ + https://duckduckgo.com/?atb=atb-123&var=variant&osv=1.2.3&ddgv=4.5.6&mo=MacBookPro%252C123&vpn_first_used=2&vpn_last_used=1 + """ + XCTAssertEqual(presentableSurveyURL!.absoluteString, expectedURL) } private func mockMessagesURL() -> URL { - let bundle = Bundle(for: NetworkProtectionRemoteMessageTests.self) - return bundle.resourceURL!.appendingPathComponent("network-protection-messages.json") + let bundle = Bundle(for: SurveyRemoteMessageTests.self) + return bundle.resourceURL!.appendingPathComponent("survey-messages.json") } } diff --git a/UnitTests/HomePage/SurveyRemoteMessagingTests.swift b/UnitTests/HomePage/SurveyRemoteMessagingTests.swift new file mode 100644 index 0000000000..09a6f43fbb --- /dev/null +++ b/UnitTests/HomePage/SurveyRemoteMessagingTests.swift @@ -0,0 +1,296 @@ +// +// SurveyRemoteMessagingTests.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 SubscriptionTestingUtilities +@testable import Subscription +@testable import DuckDuckGo_Privacy_Browser + +@available(macOS 12.0, *) +final class SurveyRemoteMessagingTests: XCTestCase { + + private var defaults: UserDefaults! + private let testGroupName = "remote-messaging" + + private var accountManager: AccountManaging! + private var subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching! + + override func setUp() { + defaults = UserDefaults(suiteName: testGroupName)! + defaults.removePersistentDomain(forName: testGroupName) + + accountManager = AccountManagerMock(isUserAuthenticated: true, accessToken: "mock-token") + subscriptionFetcher = MockSubscriptionFetcher() + } + + func testWhenFetchingRemoteMessages_AndTheUserDidNotSignUpViaWaitlist_ThenMessagesAreFetched() async { + let request = MockNetworkProtectionRemoteMessagingRequest() + request.result = .success([]) + let storage = MockSurveyRemoteMessagingStorage() + let activationDateStorage = MockWaitlistActivationDateStore() + + let messaging = DefaultSurveyRemoteMessaging( + messageRequest: request, + messageStorage: storage, + accountManager: accountManager, + subscriptionFetcher: subscriptionFetcher, + vpnActivationDateStore: activationDateStorage, + pirActivationDateStore: activationDateStorage, + minimumRefreshInterval: 0, + userDefaults: defaults + ) + + await messaging.fetchRemoteMessages() + + XCTAssertTrue(request.didFetchMessages) + } + + func testWhenFetchingRemoteMessages_AndTheUserDidSignUpViaWaitlist_ButUserHasNotActivatedNetP_ThenMessagesAreFetched() async { + let request = MockNetworkProtectionRemoteMessagingRequest() + let storage = MockSurveyRemoteMessagingStorage() + let activationDateStorage = MockWaitlistActivationDateStore() + + request.result = .success([]) + + let messaging = DefaultSurveyRemoteMessaging( + messageRequest: request, + messageStorage: storage, + accountManager: accountManager, + subscriptionFetcher: subscriptionFetcher, + vpnActivationDateStore: activationDateStorage, + pirActivationDateStore: activationDateStorage, + minimumRefreshInterval: 0, + userDefaults: defaults + ) + + XCTAssertNil(activationDateStorage.daysSinceActivation()) + + await messaging.fetchRemoteMessages() + + XCTAssertTrue(request.didFetchMessages) + } + + func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ThenMessagesAreFetched_AndMessagesAreStored() async { + let request = MockNetworkProtectionRemoteMessagingRequest() + let storage = MockSurveyRemoteMessagingStorage() + let activationDateStorage = MockWaitlistActivationDateStore() + + let messages = [mockMessage(id: "123")] + + request.result = .success(messages) + activationDateStorage._daysSinceActivation = 10 + + let messaging = DefaultSurveyRemoteMessaging( + messageRequest: request, + messageStorage: storage, + accountManager: accountManager, + subscriptionFetcher: subscriptionFetcher, + vpnActivationDateStore: activationDateStorage, + pirActivationDateStore: activationDateStorage, + minimumRefreshInterval: 0, + userDefaults: defaults + ) + + XCTAssertEqual(storage.storedMessages(), []) + XCTAssertNotNil(activationDateStorage.daysSinceActivation()) + + await messaging.fetchRemoteMessages() + + XCTAssertTrue(request.didFetchMessages) + XCTAssertEqual(storage.storedMessages(), messages) + } + + func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ButRateLimitedOperationCannotRunAgain_ThenMessagesAreNotFetched() async { + let request = MockNetworkProtectionRemoteMessagingRequest() + let storage = MockSurveyRemoteMessagingStorage() + let activationDateStorage = MockWaitlistActivationDateStore() + + activationDateStorage._daysSinceActivation = 10 + + defaults.setValue(Date(), forKey: DefaultSurveyRemoteMessaging.Constants.lastRefreshDateKey) + + let messaging = DefaultSurveyRemoteMessaging( + messageRequest: request, + messageStorage: storage, + accountManager: accountManager, + subscriptionFetcher: subscriptionFetcher, + vpnActivationDateStore: activationDateStorage, + pirActivationDateStore: activationDateStorage, + minimumRefreshInterval: .days(7), // Use a large number to hit the refresh check + userDefaults: defaults + ) + + XCTAssertNotNil(activationDateStorage.daysSinceActivation()) + + await messaging.fetchRemoteMessages() + + XCTAssertFalse(request.didFetchMessages) + XCTAssertEqual(storage.storedMessages(), []) + } + + func testWhenStoredMessagesExist_AndSomeMessagesHaveBeenDismissed_ThenPresentableMessagesDoNotIncludeDismissedMessages() { + let request = MockNetworkProtectionRemoteMessagingRequest() + let storage = MockSurveyRemoteMessagingStorage() + let activationDateStorage = MockWaitlistActivationDateStore() + + let dismissedMessage = mockMessage(id: "123") + let activeMessage = mockMessage(id: "456") + try? storage.store(messages: [dismissedMessage, activeMessage]) + activationDateStorage._daysSinceActivation = 10 + + let messaging = DefaultSurveyRemoteMessaging( + messageRequest: request, + messageStorage: storage, + accountManager: accountManager, + subscriptionFetcher: subscriptionFetcher, + vpnActivationDateStore: activationDateStorage, + pirActivationDateStore: activationDateStorage, + minimumRefreshInterval: 0, + userDefaults: defaults + ) + + let presentableMessagesBefore = messaging.presentableRemoteMessages() + XCTAssertEqual(presentableMessagesBefore, [dismissedMessage, activeMessage]) + messaging.dismiss(message: dismissedMessage) + let presentableMessagesAfter = messaging.presentableRemoteMessages() + XCTAssertEqual(presentableMessagesAfter, [activeMessage]) + } + + func testWhenStoredMessagesExist_AndSomeMessagesRequireNetPUsage_ThenPresentableMessagesDoNotIncludeInvalidMessages() { + let request = MockNetworkProtectionRemoteMessagingRequest() + let storage = MockSurveyRemoteMessagingStorage() + let activationDateStorage = MockWaitlistActivationDateStore() + + let message = mockMessage(id: "123") + try? storage.store(messages: [message]) + + let messaging = DefaultSurveyRemoteMessaging( + messageRequest: request, + messageStorage: storage, + accountManager: accountManager, + subscriptionFetcher: subscriptionFetcher, + vpnActivationDateStore: activationDateStorage, + pirActivationDateStore: activationDateStorage, + minimumRefreshInterval: 0, + userDefaults: defaults + ) + + let presentableMessages = messaging.presentableRemoteMessages() + XCTAssertEqual(presentableMessages, [message]) + } + + private func mockMessage(id: String, + subscriptionStatus: String = Subscription.Status.autoRenewable.rawValue, + minimumDaysSinceSubscriptionStarted: Int = 0, + maximumDaysUntilSubscriptionExpirationOrRenewal: Int = 0, + daysSinceVPNEnabled: Int = 0, + daysSincePIREnabled: Int = 0) -> SurveyRemoteMessage { + let remoteMessageJSON = """ + { + "id": "\(id)", + "cardTitle": "Title", + "cardDescription": "Description 1", + "attributes": { + "subscriptionStatus": "\(subscriptionStatus)", + "minimumDaysSinceSubscriptionStarted": \(minimumDaysSinceSubscriptionStarted), + "maximumDaysUntilSubscriptionExpirationOrRenewal": \(maximumDaysUntilSubscriptionExpirationOrRenewal), + "daysSinceVPNEnabled": \(daysSinceVPNEnabled), + "daysSincePIREnabled": \(daysSincePIREnabled) + }, + "action": { + "actionTitle": "Action 1" + } + } + """ + + let decoder = JSONDecoder() + return try! decoder.decode(SurveyRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) + } + +} + +// MARK: - Mocks + +private final class MockNetworkProtectionRemoteMessagingRequest: HomePageRemoteMessagingRequest { + + var result: Result<[SurveyRemoteMessage], Error>! + var didFetchMessages: Bool = false + + func fetchHomePageRemoteMessages() async -> Result<[SurveyRemoteMessage], any Error> { + didFetchMessages = true + return result + } + +} + +private final class MockSurveyRemoteMessagingStorage: SurveyRemoteMessagingStorage { + + var _storedMessages: [SurveyRemoteMessage] = [] + var _storedDismissedMessageIDs: [String] = [] + + func store(messages: [SurveyRemoteMessage]) throws { + self._storedMessages = messages + } + + func storedMessages() -> [SurveyRemoteMessage] { + _storedMessages + } + + func dismissRemoteMessage(with id: String) { + if !_storedDismissedMessageIDs.contains(id) { + _storedDismissedMessageIDs.append(id) + } + } + + func dismissedMessageIDs() -> [String] { + _storedDismissedMessageIDs + } + +} + +final class MockWaitlistActivationDateStore: WaitlistActivationDateStore { + + var _daysSinceActivation: Int? + var _daysSinceLastActive: Int? + + func daysSinceActivation() -> Int? { + _daysSinceActivation + } + + func daysSinceLastActive() -> Int? { + _daysSinceLastActive + } + +} + +final class MockSubscriptionFetcher: SurveyRemoteMessageSubscriptionFetching { + + var subscription: Subscription = Subscription( + productId: "product", + name: "name", + billingPeriod: .monthly, + startedAt: Date(), + expiresOrRenewsAt: Date(), + platform: .apple, + status: .autoRenewable) + + func getSubscription(accessToken: String) async -> Result { + return .success(subscription) + } + +} diff --git a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift b/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift deleted file mode 100644 index 3f96a7d0ea..0000000000 --- a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift +++ /dev/null @@ -1,376 +0,0 @@ -// -// NetworkProtectionRemoteMessagingTests.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -@testable import DuckDuckGo_Privacy_Browser - -final class NetworkProtectionRemoteMessagingTests: XCTestCase { - - private var defaults: UserDefaults! - private let testGroupName = "remote-messaging" - - override func setUp() { - defaults = UserDefaults(suiteName: testGroupName)! - defaults.removePersistentDomain(forName: testGroupName) - } - - func testWhenFetchingRemoteMessages_AndTheUserDidNotSignUpViaWaitlist_ThenMessagesAreFetched() { - let request = MockNetworkProtectionRemoteMessagingRequest() - request.result = .success([]) - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertTrue(!waitlistStorage.isWaitlistUser) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertTrue(request.didFetchMessages) - } - - func testWhenFetchingRemoteMessages_AndTheUserDidSignUpViaWaitlist_ButUserHasNotActivatedNetP_ThenMessagesAreFetched() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - request.result = .success([]) - waitlistStorage.store(waitlistToken: "token") - waitlistStorage.store(waitlistTimestamp: 123) - waitlistStorage.store(inviteCode: "ABCD1234") - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertTrue(waitlistStorage.isWaitlistUser) - XCTAssertNil(activationDateStorage.daysSinceActivation()) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertTrue(request.didFetchMessages) - } - - func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ThenMessagesAreFetched_AndMessagesAreStored() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let messages = [mockMessage(id: "123")] - - request.result = .success(messages) - waitlistStorage.store(waitlistToken: "token") - waitlistStorage.store(waitlistTimestamp: 123) - waitlistStorage.store(inviteCode: "ABCD1234") - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - XCTAssertTrue(waitlistStorage.isWaitlistUser) - XCTAssertEqual(storage.storedMessages(), []) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertTrue(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), messages) - } - - func testWhenFetchingRemoteMessages_AndWaitlistUserHasActivatedNetP_ButRateLimitedOperationCannotRunAgain_ThenMessagesAreNotFetched() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - waitlistStorage.store(waitlistToken: "token") - waitlistStorage.store(waitlistTimestamp: 123) - waitlistStorage.store(inviteCode: "ABCD1234") - activationDateStorage._daysSinceActivation = 10 - - defaults.setValue(Date(), forKey: DefaultNetworkProtectionRemoteMessaging.Constants.lastRefreshDateKey) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: .days(7), // Use a large number to hit the refresh check - userDefaults: defaults - ) - - XCTAssertTrue(waitlistStorage.isWaitlistUser) - XCTAssertNotNil(activationDateStorage.daysSinceActivation()) - - let expectation = expectation(description: "Remote Message Fetch") - - messaging.fetchRemoteMessages { - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertFalse(request.didFetchMessages) - XCTAssertEqual(storage.storedMessages(), []) - } - - func testWhenStoredMessagesExist_AndSomeMessagesHaveBeenDismissed_ThenPresentableMessagesDoNotIncludeDismissedMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let dismissedMessage = mockMessage(id: "123") - let activeMessage = mockMessage(id: "456") - try? storage.store(messages: [dismissedMessage, activeMessage]) - activationDateStorage._daysSinceActivation = 10 - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessagesBefore = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessagesBefore, [dismissedMessage, activeMessage]) - messaging.dismiss(message: dismissedMessage) - let presentableMessagesAfter = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessagesAfter, [activeMessage]) - } - - func testWhenStoredMessagesExist_AndSomeMessagesRequireDaysActive_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let hiddenMessage = mockMessage(id: "123", daysSinceNetworkProtectionEnabled: 10) - let activeMessage = mockMessage(id: "456") - try? storage.store(messages: [hiddenMessage, activeMessage]) - activationDateStorage._daysSinceActivation = 5 - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessagesAfter = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessagesAfter, [activeMessage]) - } - - func testWhenStoredMessagesExist_AndSomeMessagesNetPVisibility_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) - - let hiddenMessage = mockMessage(id: "123", requiresNetPAccess: true) - try? storage.store(messages: [hiddenMessage]) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessages = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessages, []) - } - - func testWhenStoredMessagesExist_AndSomeMessagesRequireNetPUsage_ThenPresentableMessagesDoNotIncludeInvalidMessages() { - let request = MockNetworkProtectionRemoteMessagingRequest() - let storage = MockNetworkProtectionRemoteMessagingStorage() - let waitlistStorage = MockWaitlistStorage() - let activationDateStorage = MockWaitlistActivationDateStore() - let visibility = NetworkProtectionVisibilityMock(isInstalled: false, visible: true) - - let message = mockMessage(id: "123", requiresNetPUsage: false, requiresNetPAccess: true) - try? storage.store(messages: [message]) - - let messaging = DefaultNetworkProtectionRemoteMessaging( - messageRequest: request, - messageStorage: storage, - waitlistStorage: waitlistStorage, - waitlistActivationDateStore: activationDateStorage, - networkProtectionVisibility: visibility, - minimumRefreshInterval: 0, - userDefaults: defaults - ) - - let presentableMessages = messaging.presentableRemoteMessages() - XCTAssertEqual(presentableMessages, [message]) - } - - private func mockMessage(id: String, - daysSinceNetworkProtectionEnabled: Int = 0, - requiresNetPUsage: Bool = true, - requiresNetPAccess: Bool = true) -> NetworkProtectionRemoteMessage { - let remoteMessageJSON = """ - { - "id": "\(id)", - "daysSinceNetworkProtectionEnabled": \(daysSinceNetworkProtectionEnabled), - "cardTitle": "Title", - "cardDescription": "Description", - "surveyURL": "https://duckduckgo.com/", - "requiresNetworkProtectionUsage": \(String(describing: requiresNetPUsage)), - "requiresNetworkProtectionAccess": \(String(describing: requiresNetPAccess)), - "action": { - "actionTitle": "Action" - } - } - """ - - let decoder = JSONDecoder() - return try! decoder.decode(NetworkProtectionRemoteMessage.self, from: remoteMessageJSON.data(using: .utf8)!) - } - -} - -// MARK: - Mocks - -private final class MockNetworkProtectionRemoteMessagingRequest: HomePageRemoteMessagingRequest { - - var result: Result<[NetworkProtectionRemoteMessage], Error>! - var didFetchMessages: Bool = false - - func fetchHomePageRemoteMessages(completion: @escaping (Result<[T], Error>) -> Void) where T: Decodable { - didFetchMessages = true - - if let castResult = self.result as? Result<[T], Error> { - completion(castResult) - } else { - fatalError("Could not cast result to expected type") - } - } - -} - -private final class MockNetworkProtectionRemoteMessagingStorage: HomePageRemoteMessagingStorage { - - var _storedMessages: [NetworkProtectionRemoteMessage] = [] - var _storedDismissedMessageIDs: [String] = [] - - func store(messages: [NetworkProtectionRemoteMessage]) throws { - self._storedMessages = messages - } - - func storedMessages() -> [NetworkProtectionRemoteMessage] { - _storedMessages - } - - func store(messages: [Message]) throws { - if let messages = messages as? [NetworkProtectionRemoteMessage] { - self._storedMessages = messages - } else { - fatalError("Failed to cast messages") - } - } - - func storedMessages() -> [Message] { - return _storedMessages as! [Message] - } - - func dismissRemoteMessage(with id: String) { - if !_storedDismissedMessageIDs.contains(id) { - _storedDismissedMessageIDs.append(id) - } - } - - func dismissedMessageIDs() -> [String] { - _storedDismissedMessageIDs - } - -} - -final class MockWaitlistActivationDateStore: WaitlistActivationDateStore { - - var _daysSinceActivation: Int? - var _daysSinceLastActive: Int? - - func daysSinceActivation() -> Int? { - _daysSinceActivation - } - - func daysSinceLastActive() -> Int? { - _daysSinceLastActive - } - -} diff --git a/UnitTests/NetworkProtection/Resources/dbp-messages.json b/UnitTests/NetworkProtection/Resources/dbp-messages.json deleted file mode 100644 index 8d7a6dc7dc..0000000000 --- a/UnitTests/NetworkProtection/Resources/dbp-messages.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "id": "123", - "cardTitle": "Title 1", - "cardDescription": "Description 1", - "requiresDataBrokerProtectionAccess": true, - "requiresDataBrokerProtectionUsage": true, - "action": { - "actionTitle": "Action 1" - } - }, - { - "id": "456", - "daysSinceDataBrokerProtectionEnabled": 1, - "cardTitle": "Title 2", - "cardDescription": "Description 2", - "requiresDataBrokerProtectionAccess": true, - "requiresDataBrokerProtectionUsage": true, - "action": { - "actionTitle": "Action 2" - } - }, - { - "id": "789", - "daysSinceDataBrokerProtectionEnabled": 5, - "cardTitle": "Title 3", - "cardDescription": "Description 3", - "requiresDataBrokerProtectionAccess": true, - "requiresDataBrokerProtectionUsage": true, - "action": { - "actionTitle": "Action 3", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - - } -] diff --git a/UnitTests/NetworkProtection/Resources/network-protection-messages.json b/UnitTests/NetworkProtection/Resources/network-protection-messages.json deleted file mode 100644 index d69450766f..0000000000 --- a/UnitTests/NetworkProtection/Resources/network-protection-messages.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "id": "123", - "cardTitle": "Title 1", - "cardDescription": "Description 1", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, - "action": { - "actionTitle": "Action 1" - } - }, - { - "id": "456", - "daysSinceNetworkProtectionEnabled": 1, - "cardTitle": "Title 2", - "cardDescription": "Description 2", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, - "action": { - "actionTitle": "Action 2" - } - }, - { - "id": "789", - "daysSinceNetworkProtectionEnabled": 5, - "cardTitle": "Title 3", - "cardDescription": "Description 3", - "requiresNetworkProtectionAccess": true, - "requiresNetworkProtectionUsage": true, - "action": { - "actionTitle": "Action 3", - "actionType": "openSurveyURL", - "actionURL": "https://duckduckgo.com/" - } - - } -] From 96111c440680760f3066663a7fc4f9cd97875924 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 27 May 2024 09:25:24 +0000 Subject: [PATCH 30/42] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- .../AppTrackerDataSetProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 64 +++++++++++++++---- DuckDuckGo/ContentBlocker/trackerData.json | 6 +- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 6309508918..4009e67d84 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 = "\"f2379428bfb9f97dffb6f9e700c27840\"" - public static let embeddedDataSHA = "0fb9abce9db169dec195c611c4520a8d643f620c3dc1bf075c73052591f7acf4" + public static let embeddedDataETag = "\"1d52c7748b83498990caa1a58407b9e7\"" + public static let embeddedDataSHA = "be9d07bf6557974c7e267cf82fd07db4ada9caad67e6ae6c78d800b0b8da7e34" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift index 85ba594612..9783e9d521 100644 --- a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"004872ea25514c61490f047cd5f088b8\"" - public static let embeddedDataSHA = "4a06a3df999fad7829baecc9ccfcbc54c20526ba304f6c5f2846899d29b281cc" + public static let embeddedDataETag = "\"ea184137cdaa19ca5de76352215a9e0e\"" + public static let embeddedDataSHA = "faa4dfbef4903710374b153c9a87e09b713fc19d64fa0bcfd1fd392fff93af21" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 82bac9ddc8..06945987ef 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": 1716212212537, + "version": 1716586822463, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -303,6 +303,9 @@ { "domain": "www.thrifty.com" }, + { + "domain": "sports.tipico.de" + }, { "domain": "marvel.com" }, @@ -318,7 +321,7 @@ ] }, "state": "enabled", - "hash": "6ea22969c48f1cb624808cd49a296f4d" + "hash": "0e21f0096d2a135a3c36332ae79b656c" }, "autofill": { "exceptions": [ @@ -1228,6 +1231,11 @@ "state": "enabled", "hash": "b685397a8317384bec3b8d7e8b7571bb" }, + "dummyWebMessageListener": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "elementHiding": { "exceptions": [ { @@ -3476,6 +3484,15 @@ } ] }, + { + "domain": "post-gazette.com", + "rules": [ + { + "selector": "[data-dfpads-position]", + "type": "hide-empty" + } + ] + }, { "domain": "prajwaldesai.com", "rules": [ @@ -3921,6 +3938,15 @@ } ] }, + { + "domain": "toledoblade.com", + "rules": [ + { + "selector": "[data-dfpads-position]", + "type": "hide-empty" + } + ] + }, { "domain": "tripadvisor.ca", "rules": [ @@ -4193,6 +4219,10 @@ { "selector": ".gam-placeholder", "type": "closest-empty" + }, + { + "selector": "[class*='sdaContainer']", + "type": "hide-empty" } ] }, @@ -4354,7 +4384,7 @@ ] }, "state": "enabled", - "hash": "9a895fea083dabb6e3496242636a8370" + "hash": "1c7a079280844400d77a0b0baf89c967" }, "exceptionHandler": { "exceptions": [ @@ -4712,6 +4742,12 @@ { "domain": "tirerack.com" }, + { + "domain": "milesplit.live" + }, + { + "domain": "dollargeneral.com" + }, { "domain": "marvel.com" }, @@ -4728,7 +4764,7 @@ "privacy-test-pages.site" ] }, - "hash": "05bddff3ae61a9536e38a6ef7d383eb3" + "hash": "3803c69233510b43ebaee8368b38a31d" }, "harmfulApis": { "settings": { @@ -6643,6 +6679,7 @@ "domains": [ "abril.com.br", "algomalegalclinic.com", + "bodyelectricvitality.com.au", "cosmicbook.news", "eatroyo.com", "thesimsresource.com", @@ -6772,6 +6809,7 @@ "newschannel20.com", "newschannel9.com", "okcfox.com", + "post-gazette.com", "raleighcw.com", "siouxlandnews.com", "southernoregoncw.com", @@ -6780,6 +6818,7 @@ "thecw46.com", "thecwtc.com", "thenationaldesk.com", + "toledoblade.com", "triblive.com", "turnto10.com", "univisionseattle.com", @@ -6834,7 +6873,8 @@ "rule": "grow.me/main.js", "domains": [ "budgetbytes.com", - "foodfornet.com" + "foodfornet.com", + "homesteadingfamily.com" ] }, { @@ -7077,11 +7117,7 @@ { "rule": "static.klaviyo.com/onsite/js/klaviyo.js", "domains": [ - "essentialpraxis.com", - "kidsguide.com", - "muc-off.com", - "paria.cc", - "urbanebikes.com" + "" ] }, { @@ -7932,6 +7968,12 @@ "domains": [ "" ] + }, + { + "rule": "c.slickstream.com/app/", + "domains": [ + "" + ] } ] }, @@ -8458,7 +8500,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "d9db93f32201ee8d2e545eeead7cc66f" + "hash": "36ca79804d05bc24247f804f48fda77b" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/ContentBlocker/trackerData.json b/DuckDuckGo/ContentBlocker/trackerData.json index 2c632f15c2..ef11c78f3c 100644 --- a/DuckDuckGo/ContentBlocker/trackerData.json +++ b/DuckDuckGo/ContentBlocker/trackerData.json @@ -1,6 +1,6 @@ { "_builtWith": { - "tracker-radar": "a8f276714a31d43fb185a1b233ed92f12680627ca54c6f98da44de9fe27f097e-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-radar": "9c02278e8bfb43db5e6b8756cdedc469924d600b8c4d6c9d7177de01405c8f5c-4013b4e91930c643394cb31c6c745356f133b04f", "tracker-surrogates": "0528e3226df15b1a3e319ad68ef76612a8f26623" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", @@ -41441,7 +41441,8 @@ "twittercommunity.com", "twttr.com", "twttr.net", - "vine.co" + "vine.co", + "x.com" ], "prevalence": 8.79, "displayName": "Twitter" @@ -52872,6 +52873,7 @@ "twttr.com": "Twitter, Inc.", "twttr.net": "Twitter, Inc.", "vine.co": "Twitter, Inc.", + "x.com": "Twitter, Inc.", "ads1-adnow.com": "Mas Capital Group Ltd", "ads2-adnow.com": "Mas Capital Group Ltd", "ads3-adnow.com": "Mas Capital Group Ltd", From 4e092ae745490f1265e9217a665b2b80a59cc4d1 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 27 May 2024 09:25:24 +0000 Subject: [PATCH 31/42] Set marketing version to 1.90.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index cee5ef12e7..f8e717a626 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.89.0 +MARKETING_VERSION = 1.90.0 From 480df123a9ae3ae01ed5d8ffb980b45016ad7956 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 27 May 2024 09:38:23 +0000 Subject: [PATCH 32/42] Bump version to 1.90.0 (192) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 189892f698..ffacba59c9 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 191 +CURRENT_PROJECT_VERSION = 192 From 5fc933deb9f94fb6dfc8b882fe1deea26ac8a82a Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 29 May 2024 12:41:15 +0100 Subject: [PATCH 33/42] Remove autofill survey (#2819) Task/Issue URL: https://app.asana.com/0/1187352151074490/1207364165261200/f Tech Design URL: CC: **Description**: Remove the autofill survey but not all associated code (e.g. the persisted flag), since I have zero context there. **Steps to test this PR**: 1. On main, go to Settings > Passwords and confirm the survey prompt is visible 2. Do likewise on this branch and confirm it isn't. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../View/PreferencesAutofillView.swift | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index 77fe6bec54..6cf2b84b6f 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -68,42 +68,6 @@ extension Preferences { // Autofill Content Button PreferencePaneSection { - // New section - if model.autofillSurveyEnabled { - HStack(alignment: .top, spacing: 20) { - Image(.passwordsDDG128) - .frame(width: 64, height: 48) - - VStack(alignment: .leading) { - Text(verbatim: "Help us improve!") - .bold() - Text(verbatim: "We want to make using passwords in DuckDuckGo better.") - .foregroundColor(.greyText) - .padding(.top, 1) - - HStack { - Button(action: { - model.disableAutofillSurvey() - }, label: { - Text(verbatim: "No Thanks") - }) - Button(action: { - model.launchSurvey() - }, label: { - Text(verbatim: "Take Survey") - }) - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - } - .padding(.top, 12) - } - - Spacer() - } - .padding() - .roundedBorder() - .padding(.bottom, 24) - } - Button(UserText.autofillViewContentButton) { model.showAutofillPopover() } From c5e7969df871a84f626bf5994b68b330ef0e22d6 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 29 May 2024 12:41:15 +0100 Subject: [PATCH 34/42] Remove autofill survey (#2819) Task/Issue URL: https://app.asana.com/0/1187352151074490/1207364165261200/f Tech Design URL: CC: **Description**: Remove the autofill survey but not all associated code (e.g. the persisted flag), since I have zero context there. **Steps to test this PR**: 1. On main, go to Settings > Passwords and confirm the survey prompt is visible 2. Do likewise on this branch and confirm it isn't. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../View/PreferencesAutofillView.swift | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index 77fe6bec54..6cf2b84b6f 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -68,42 +68,6 @@ extension Preferences { // Autofill Content Button PreferencePaneSection { - // New section - if model.autofillSurveyEnabled { - HStack(alignment: .top, spacing: 20) { - Image(.passwordsDDG128) - .frame(width: 64, height: 48) - - VStack(alignment: .leading) { - Text(verbatim: "Help us improve!") - .bold() - Text(verbatim: "We want to make using passwords in DuckDuckGo better.") - .foregroundColor(.greyText) - .padding(.top, 1) - - HStack { - Button(action: { - model.disableAutofillSurvey() - }, label: { - Text(verbatim: "No Thanks") - }) - Button(action: { - model.launchSurvey() - }, label: { - Text(verbatim: "Take Survey") - }) - .buttonStyle(DefaultActionButtonStyle(enabled: true)) - } - .padding(.top, 12) - } - - Spacer() - } - .padding() - .roundedBorder() - .padding(.bottom, 24) - } - Button(UserText.autofillViewContentButton) { model.showAutofillPopover() } From a1692ff305170819ead0aa1739f2bc2c541ae477 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 30 May 2024 08:38:40 +0000 Subject: [PATCH 35/42] Bump version to 1.90.0 (193) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index ffacba59c9..f3433420ac 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 192 +CURRENT_PROJECT_VERSION = 193 From 3db1af07f13b05ce2bf4e5ddcf0e78dadc5ddf78 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 30 May 2024 11:15:14 +0200 Subject: [PATCH 36/42] Fixes some VPN uninstallation issues (#2820) Task/Issue URL: https://app.asana.com/0/1206580121312550/1207431594119528/f ## Description Fixes some VPN uninstallation issues in v1.90.0. --- .../NetworkProtectionTunnelController.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 44394aa36f..8610b020c7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -37,6 +37,7 @@ import Subscription typealias NetworkProtectionStatusChangeHandler = (NetworkProtection.ConnectionStatus) -> Void typealias NetworkProtectionConfigChangeHandler = () -> Void +// swiftlint:disable:next type_body_length final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { // MARK: - Settings @@ -94,6 +95,13 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private var internalManager: NETunnelProviderManager? + /// Simply clears the internal manager so the VPN manager is reloaded next time it's requested. + /// + @MainActor + private func clearInternalManager() { + internalManager = nil + } + /// The last known VPN status. /// /// Should not be used for checking the current status. @@ -178,6 +186,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr subscribeToSettingsChanges() subscribeToStatusChanges() + subscribeToConfigurationChanges() } // MARK: - Observing Status Changes @@ -209,6 +218,31 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } + // MARK: - Observing Configuation Changes + + private func subscribeToConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + Task { @MainActor in + guard let manager = await self.manager else { + return + } + + do { + try await manager.loadFromPreferences() + + if manager.connection.status == .invalid { + self.clearInternalManager() + } + } catch { + self.clearInternalManager() + } + } + } + .store(in: &cancellables) + } + // MARK: - Subscriptions private func subscribeToSettingsChanges() { @@ -693,6 +727,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr func disableOnDemand(tunnelManager: NETunnelProviderManager) async throws { try await tunnelManager.loadFromPreferences() + guard tunnelManager.connection.status != .invalid else { + // An invalid connection status means the VPN isn't really configured + // so we don't want to save changed because that would re-create the VPN + // configuration. + clearInternalManager() + return + } + tunnelManager.isOnDemandEnabled = false try await tunnelManager.saveToPreferences() From 1bd688cfca33c027f379d19a6d2e3687b3a089f5 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 30 May 2024 11:29:13 +0100 Subject: [PATCH 37/42] Add PIPAgent entitlement (#2821) Task/Issue URL: https://app.asana.com/0/0/1207443801635705/f **Description**: Add PIP entitlement to the sandbox app --- DuckDuckGo/DuckDuckGoAppStore.entitlements | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index 7b07bc92d8..6cd32658ba 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -48,6 +48,7 @@ com.apple.security.temporary-exception.mach-lookup.global-name + com.apple.PIPAgent $(DBP_BACKGROUND_AGENT_BUNDLE_ID) $(AGENT_BUNDLE_ID) From 62e06de93da2a20548e73e92b2b80355809c776c Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 30 May 2024 11:30:35 +0000 Subject: [PATCH 38/42] Bump version to 1.90.0 (194) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index f3433420ac..1b44f8dc8a 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 193 +CURRENT_PROJECT_VERSION = 194 From f0d2c8e7d14ecc26d8dad866dc63c492096a60fc Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 30 May 2024 11:42:24 +0000 Subject: [PATCH 39/42] Bump version to 1.90.0 (195) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 1b44f8dc8a..580c5aad72 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 194 +CURRENT_PROJECT_VERSION = 195 From 3f57821bdb1dcb9d04390d5a6cf47b8992146e29 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 31 May 2024 10:14:17 +0100 Subject: [PATCH 40/42] Revert "Add PIPAgent entitlement (#2821)" (#2823) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207452214141537/f Tech Design URL: CC: **Description**: This reverts commit 1bd688cfca33c027f379d19a6d2e3687b3a089f5. --- DuckDuckGo/DuckDuckGoAppStore.entitlements | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index 6cd32658ba..7b07bc92d8 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -48,7 +48,6 @@ com.apple.security.temporary-exception.mach-lookup.global-name - com.apple.PIPAgent $(DBP_BACKGROUND_AGENT_BUNDLE_ID) $(AGENT_BUNDLE_ID) From a0baec001853a0f95b1f46fceb1c146263acb87e Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 31 May 2024 13:24:31 +0000 Subject: [PATCH 41/42] Bump version to 1.90.0 (196) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 580c5aad72..ddcf864b19 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 195 +CURRENT_PROJECT_VERSION = 196 From 2e0956acc66f34170557657010dde8603314164d Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Fri, 31 May 2024 21:25:40 +0200 Subject: [PATCH 42/42] Update autoconsent to v10.9.0 (#2822) Task/Issue URL: https://app.asana.com/0/1207444284182419/1207444284182419 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v10.9.0 ## Description Updates Autoconsent to version [v10.9.0](https://github.com/duckduckgo/autoconsent/releases/tag/v10.9.0). ### Autoconsent v10.9.0 release notes See release notes [here](https://github.com/duckduckgo/autoconsent/blob/v10.9.0/CHANGELOG.md) --- 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 77774fdb03..bbdc3e35c1 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_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)?.[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!0}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){if(await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST"))return!0;let e=3e3;return await this.mainWorldEval("EVAL_TRUSTARC_FRAME_GTM")&&(e=1500),await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",e),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',e),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",e),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",e),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",10*e),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_TRUSTARC_FRAME_TEST")}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!await this.mainWorldEval("EVAL_KLARO_TRY_API_OPT_OUT")||(!!this.click(".klaro .cn-decline")||(await this.mainWorldEval("EVAL_KLARO_OPEN_POPUP"),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button"))))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:['[data-project="mol-fe-cmp"] [class*=footer]',"xpath///button[contains(., 'Save & Exit')]"]}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"espace-personnel.agirc-arrco.fr",runContext:{urlPattern:"^https://espace-personnel\\.agirc-arrco\\.fr/"},prehideSelectors:[".cdk-overlay-container"],detectCmp:[{exists:".cdk-overlay-container app-esaa-cookie-component"}],detectPopup:[{visible:".cdk-overlay-container app-esaa-cookie-component"}],optIn:[{waitForThenClick:".btn-cookie-accepter"}],optOut:[{waitForThenClick:".btn-cookie-refuser"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6:has(._a9--)"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{waitForThenClick:".iubenda-cs-accept-btn"}],optOut:[{waitForThenClick:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{waitForThenClick:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"#rejectAllMain"}],optIn:[{click:"#acceptAllMain"}],optOut:[{click:"#rejectAllMain"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",prehideSelectors:[".cc_dialog.cc_css_reboot,.cc_overlay_lock"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{if:{exists:".cc_dialog.cc_css_reboot .cc_b_cp"},then:[{click:".cc_dialog.cc_css_reboot .cc_b_cp"},{waitForVisible:".cookie-consent-preferences-dialog .cc_cp_f_save button"},{waitForThenClick:".cookie-consent-preferences-dialog .cc_cp_f_save button"}],else:[{hide:".cc_dialog.cc_css_reboot,.cc_overlay_lock"}]}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!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)}}(); diff --git a/package-lock.json b/package-lock.json index 29fc93354e..4d2e2ff741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.8.0" + "@duckduckgo/autoconsent": "^10.9.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.8.0.tgz", - "integrity": "sha512-n4axPmOsDxK9X6UYUb+S7KWqvppG75IemnH+pK1SAp01+US4ez+t7fzq4VQU19dy0EpGyC1bUnsB2BzdhUx65g==" + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.9.0.tgz", + "integrity": "sha512-eTlPFmW7QzThb9OGDWSSnUmB8Kq74YDO1N63cC/XsBHruaeN13N60GwhcL2/p4C05Gfkv8AhyOl/fTvUjya/hg==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index a66b5e26a8..41c8b18220 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.8.0" + "@duckduckgo/autoconsent": "^10.9.0" } }