From 86144596b016ccd4c1c13506525fc4e9ccde6692 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Wed, 8 May 2024 16:59:57 +0100 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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 5f20c6c8519bfb666f5637e11ec0f4b26ce0f404 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 21 May 2024 10:54:31 +0200 Subject: [PATCH 10/26] Adding to the Dock automatically (#2722) Task/Issue URL: https://app.asana.com/0/72649045549333/1206797051025460/f Tech Design URL: CC: **Description**: Adding to the Dock automatically during the onboarding, through the new tab page card and Settings. --- DuckDuckGo.xcodeproj/project.pbxproj | 24 + DuckDuckGo/Application/DockCustomizer.swift | 146 ++++ .../Application/DockPositionProvider.swift | 79 ++ .../Images/Dock-128.imageset/Contents.json | 12 + .../Images/Dock-128.imageset/Dock-128.pdf | Bin 0 -> 12816 bytes DuckDuckGo/Common/Localizables/UserText.swift | 11 + .../Utilities/UserDefaultsWrapper.swift | 1 + .../Model/HomePageContinueSetUpModel.swift | 105 ++- .../HomePage/View/ContinueSetUpView.swift | 35 +- .../View/HomePageViewController.swift | 1 + DuckDuckGo/Localizable.xcstrings | 678 +++++++++++++++++- .../Onboarding/View/OnboardingFlow.swift | 17 +- .../ViewModel/OnboardingViewModel.swift | 44 ++ .../Model/DefaultBrowserPreferences.swift | 7 +- .../View/PreferencesGeneralView.swift | 48 +- .../View/PreferencesRootView.swift | 3 +- .../Statistics/ATB/StatisticsLoader.swift | 10 + DuckDuckGo/Statistics/GeneralPixel.swift | 19 + .../Tab/View/BrowserTabViewController.swift | 6 + UnitTests/App/DockCustomizerMock.swift | 39 + UnitTests/App/DockPositionProviderTests.swift | 48 ++ .../HomePage/ContinueSetUpModelTests.swift | 38 +- .../CapturingDefaultBrowserProvider.swift | 2 + UnitTests/Onboarding/OnboardingTests.swift | 41 ++ .../DefaultBrowserPreferencesTests.swift | 2 + 25 files changed, 1378 insertions(+), 38 deletions(-) create mode 100644 DuckDuckGo/Application/DockCustomizer.swift create mode 100644 DuckDuckGo/Application/DockPositionProvider.swift create mode 100644 DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf create mode 100644 UnitTests/App/DockCustomizerMock.swift create mode 100644 UnitTests/App/DockPositionProviderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ddff21e5e7..acd1b22af2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC12936B816006F4388 /* BWMessageIdGeneratorTests.swift */; }; 1D3B1AC429378953006F4388 /* BWResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC329378953006F4388 /* BWResponseTests.swift */; }; 1D3B1AC62937A478006F4388 /* BWRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC52937A478006F4388 /* BWRequestTests.swift */; }; + 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */; }; + 1D4071AF2BD64267002D4537 /* DockCustomizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */; }; 1D43EB32292788C70065E5D6 /* BWEncryptionOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB31292788C70065E5D6 /* BWEncryptionOutput.m */; }; 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 1D43EB36292ACE690065E5D6 /* ApplicationVersionReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB35292ACE690065E5D6 /* ApplicationVersionReader.swift */; }; @@ -70,6 +72,8 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D69C552291302F200B75945 /* BWVault.swift */; }; 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */; }; 1D6A492129CF7A490011DF74 /* NSPopoverExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */; }; + 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */; }; + 1D7694002BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */; }; 1D77921828FDC54C00BE0210 /* FaviconReferenceCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921728FDC54C00BE0210 /* FaviconReferenceCacheTests.swift */; }; 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */; }; 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; @@ -85,6 +89,8 @@ 1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */; }; 1D8C2FF02B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */; }; 1D8C2FF12B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */; }; + 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */; }; + 1D9A37682BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */; }; 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; @@ -103,6 +109,8 @@ 1DA6D0FE2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; 1DA6D1032A1FFA3B00540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; + 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */; }; + 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */; }; 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */; }; 1DB67F2A2B6FEB17003DF243 /* WebViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */; }; 1DB67F2D2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F2C2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift */; }; @@ -2823,6 +2831,7 @@ 1D3B1AC12936B816006F4388 /* BWMessageIdGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWMessageIdGeneratorTests.swift; sourceTree = ""; }; 1D3B1AC329378953006F4388 /* BWResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWResponseTests.swift; sourceTree = ""; }; 1D3B1AC52937A478006F4388 /* BWRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWRequestTests.swift; sourceTree = ""; }; + 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockCustomizer.swift; sourceTree = ""; }; 1D43EB30292788C70065E5D6 /* BWEncryptionOutput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BWEncryptionOutput.h; sourceTree = ""; }; 1D43EB31292788C70065E5D6 /* BWEncryptionOutput.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BWEncryptionOutput.m; sourceTree = ""; }; 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWNotRespondingAlert.swift; sourceTree = ""; }; @@ -2833,6 +2842,7 @@ 1D6216B129069BBF00386B2C /* BWKeyStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWKeyStorage.swift; sourceTree = ""; }; 1D69C552291302F200B75945 /* BWVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWVault.swift; sourceTree = ""; }; 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPopoverExtension.swift; sourceTree = ""; }; + 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockCustomizerMock.swift; sourceTree = ""; }; 1D77921728FDC54C00BE0210 /* FaviconReferenceCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconReferenceCacheTests.swift; sourceTree = ""; }; 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconStoringMock.swift; sourceTree = ""; }; 1D77921C28FFF27C00BE0210 /* RunningApplicationCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningApplicationCheck.swift; sourceTree = ""; }; @@ -2842,6 +2852,7 @@ 1D8C2FE92B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTabSnapshotStore.swift; sourceTree = ""; }; + 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockPositionProvider.swift; sourceTree = ""; }; 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtension.swift; sourceTree = ""; }; 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferencesTests.swift; sourceTree = ""; }; 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferencesTests.swift; sourceTree = ""; }; @@ -2851,6 +2862,7 @@ 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatusTests.swift; sourceTree = ""; }; 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookie.swift; sourceTree = ""; }; 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookieTests.swift; sourceTree = ""; }; + 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockPositionProviderTests.swift; sourceTree = ""; }; 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSnapshotRenderer.swift; sourceTree = ""; }; 1DB67F2C2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewSnapshotRenderer.swift; sourceTree = ""; }; 1DB9617929F1D06D00CF5568 /* InternalUserDeciderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderMock.swift; sourceTree = ""; }; @@ -6514,6 +6526,8 @@ 858A798226A8B75F00A75A42 /* CopyHandler.swift */, 1D36E65A298ACD2900AA485D /* AppIconChanger.swift */, CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */, + 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */, + 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */, 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */, ); path = Application; @@ -7928,6 +7942,8 @@ B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */, B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */, B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */, + 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */, + 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */, ); path = App; sourceTree = ""; @@ -9821,6 +9837,7 @@ B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, + 1D4071AF2BD64267002D4537 /* DockCustomizer.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, @@ -10185,6 +10202,7 @@ 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 3706FC57293F65D500E42796 /* TabPreviewWindowController.swift in Sources */, + 1D9A37682BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 3706FC58293F65D500E42796 /* NSSizeExtension.swift in Sources */, 3706FC59293F65D500E42796 /* Fire.swift in Sources */, 3706FC5A293F65D500E42796 /* RandomAccessCollectionExtension.swift in Sources */, @@ -10385,6 +10403,7 @@ 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */, 3706FE19293F661700E42796 /* DeviceAuthenticatorTests.swift in Sources */, 3706FE1A293F661700E42796 /* BrowserProfileTests.swift in Sources */, + 1D7694002BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 3706FE1B293F661700E42796 /* PermissionManagerTests.swift in Sources */, 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, @@ -10520,6 +10539,7 @@ 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 3706FE68293F661700E42796 /* DuckPlayerURLExtensionTests.swift in Sources */, 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, + 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */, C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, @@ -11333,6 +11353,7 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */, AA6FFB4424DC33320028F4D0 /* NSViewExtension.swift in Sources */, B6C0B23E26E8BF1F0031CB7F /* DownloadListViewModel.swift in Sources */, + 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */, F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */, 4B723E1026B0006700E14D75 /* CSVImporter.swift in Sources */, @@ -11550,6 +11571,7 @@ B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, + 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, @@ -11742,6 +11764,7 @@ 4B11060525903E570039B979 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, 858A798826A99DBE00A75A42 /* PasswordManagementItemListModelTests.swift in Sources */, 566B196529CDB828007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, + 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, @@ -11866,6 +11889,7 @@ B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, + 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, diff --git a/DuckDuckGo/Application/DockCustomizer.swift b/DuckDuckGo/Application/DockCustomizer.swift new file mode 100644 index 0000000000..6a889bd058 --- /dev/null +++ b/DuckDuckGo/Application/DockCustomizer.swift @@ -0,0 +1,146 @@ +// +// DockCustomizer.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common + +protocol DockCustomization { + var isAddedToDock: Bool { get } + + @discardableResult + func addToDock() -> Bool +} + +final class DockCustomizer: DockCustomization { + + private let positionProvider: DockPositionProviding + + init(positionProvider: DockPositionProviding = DockPositionProvider()) { + self.positionProvider = positionProvider + } + + private var dockPlistURL: URL = URL(fileURLWithPath: NSString(string: "~/Library/Preferences/com.apple.dock.plist").expandingTildeInPath) + + private var dockPlistDict: [String: AnyObject]? { + return NSDictionary(contentsOf: dockPlistURL) as? [String: AnyObject] + } + + // This checks whether the bundle identifier of the current bundle + // is present in the 'persistent-apps' array of the Dock's plist. + var isAddedToDock: Bool { + guard let bundleIdentifier = Bundle.main.bundleIdentifier, + let dockPlistDict = dockPlistDict, + let persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] else { + return false + } + + return persistentApps.contains(where: { ($0["tile-data"] as? [String: AnyObject])?["bundle-identifier"] as? String == bundleIdentifier }) + } + + // Adds a dictionary representing the application, either by using an existing + // one from 'recent-apps' or creating a new one if the application isn't recently used. + // It then inserts this dictionary into the 'persistent-apps' list at a position + // determined by `positionProvider`. Following the plist update, it schedules the Dock + // to restart after a brief delay to apply the changes. + @discardableResult + func addToDock() -> Bool { + let appPath = Bundle.main.bundleURL.path + guard !isAddedToDock, + let bundleIdentifier = Bundle.main.bundleIdentifier, + var dockPlistDict = dockPlistDict else { + return false + } + + var persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] ?? [] + let recentApps = dockPlistDict["recent-apps"] as? [[String: AnyObject]] ?? [] + + let appDict: [String: AnyObject] + // Find the app in recent apps + if let recentAppIndex = recentApps.firstIndex(where: { appDict in + if let tileData = appDict["tile-data"] as? [String: AnyObject], + let appBundleIdentifier = tileData["bundle-identifier"] as? String { + return appBundleIdentifier == bundleIdentifier + } + return false + }) { + // Use existing dictonary from recentApps + appDict = recentApps[recentAppIndex] + } else { + // Create the dictionary for the current application if not found in recent apps + appDict = Self.appDict(appPath: appPath, bundleIdentifier: bundleIdentifier) + } + + // Insert to persistent apps + let index = positionProvider.newDockIndex(from: makeAppURLs(from: persistentApps)) + persistentApps.insert(appDict, at: index) + + // Update the plist + dockPlistDict["persistent-apps"] = persistentApps as AnyObject? + dockPlistDict["recent-apps"] = recentApps as AnyObject? + + // Update mod-count + dockPlistDict["mod-count"] = ((dockPlistDict["mod-count"] as? Int) ?? 0) + 1 as AnyObject + + do { + try (dockPlistDict as NSDictionary).write(to: dockPlistURL) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.restartDock() + } + return true + } catch { + os_log(.error, "Error writing to Dock plist: %{public}@", error.localizedDescription) + return false + } + } + + private func restartDock() { + let task = Process() + task.launchPath = "/usr/bin/killall" + task.arguments = ["Dock"] + task.launch() + } + + private func makeAppURLs(from persistentApps: [[String: AnyObject]]) -> [URL] { + return persistentApps.compactMap { appDict in + if let tileData = appDict["tile-data"] as? [String: AnyObject], + let appBundleIdentifier = tileData["file-data"] as? [String: AnyObject], + let urlString = appBundleIdentifier["_CFURLString"] as? String, + let url = URL(string: urlString) { + return url + } else { + return nil + } + } + } + + static func appDict(appPath: String, bundleIdentifier: String) -> [String: AnyObject] { + return ["tile-type": "file-tile" as AnyObject, + "tile-data": [ + "dock-extra": 0 as AnyObject, + "file-type": 1 as AnyObject, + "file-data": [ + "_CFURLString": "file://" + appPath + "/", + "_CFURLStringType": 15 + ], + "file-label": "DuckDuckGo" as AnyObject, + "bundle-identifier": bundleIdentifier as AnyObject, + "is-beta": 0 as AnyObject + ] as AnyObject + ] + } +} diff --git a/DuckDuckGo/Application/DockPositionProvider.swift b/DuckDuckGo/Application/DockPositionProvider.swift new file mode 100644 index 0000000000..f93ba428c1 --- /dev/null +++ b/DuckDuckGo/Application/DockPositionProvider.swift @@ -0,0 +1,79 @@ +// +// DockPositionProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum DockApp: String, CaseIterable { + case chrome = "/Applications/Google Chrome.app/" + case firefox = "/Applications/Firefox.app/" + case edge = "/Applications/Microsoft Edge.app/" + case brave = "/Applications/Brave Browser.app/" + case opera = "/Applications/Opera.app/" + case arc = "/Applications/Arc.app/" + case safari = "/Applications/Safari.app/" + case safariLong = "/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app/" + + var url: URL { + return URL(string: "file://" + self.rawValue)! + } +} + +protocol DockPositionProviding { + func newDockIndex(from currentAppURLs: [URL]) -> Int +} + +/// Class to determine the best positioning in the Dock +final class DockPositionProvider: DockPositionProviding { + + private let preferredOrder: [DockApp] = [ + .chrome, + .firefox, + .edge, + .brave, + .opera, + .arc, + .safari, + .safariLong + ] + + private var defaultBrowserProvider: DefaultBrowserProvider + + init(defaultBrowserProvider: DefaultBrowserProvider = SystemDefaultBrowserProvider()) { + self.defaultBrowserProvider = defaultBrowserProvider + } + + /// Determines the new dock index for a new app based on the default browser or preferred order + func newDockIndex(from currentAppURLs: [URL]) -> Int { + // Place next to the default browser + if !defaultBrowserProvider.isDefault, + let defaultBrowserURL = defaultBrowserProvider.defaultBrowserURL, + let position = currentAppURLs.firstIndex(of: defaultBrowserURL) { + return position + 1 + } + + // Place based on the preferred order + for app in preferredOrder { + if let position = currentAppURLs.firstIndex(of: app.url) { + return position + 1 + } + } + + // Otherwise, place at the end + return currentAppURLs.count + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json new file mode 100644 index 0000000000..e7bb0888d9 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Dock-128.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9a0511ce2c0c14e8f62484a2e049937b18473ab2 GIT binary patch literal 12816 zcmeHNTW=e=6@H&zF)smj3q%@{LkmRNiMQo?0@ff zNX|#<;z~|>LEFrOO?^Wio;wdYe0KTz&2u&Cs!G*Oy?Xy+RoCBrSC7A+EpN9UYYp%U z5T5bXWO+NQ|5!JpCOowo8s{K?N>pC2?k4l)Q8Gi9HxJ9{cD`Ev{-^s{-D2E}7u9CF zo=xtmpKDCt8Pix-&pu5*%CEk4t{Z;!4o~`Z^KhS;z1`M7$4>XF`y-8=&YOq#_48(A zOsBQ3?;aL;*2&FH9DQtnHr^RO42X<9J^*Sn@}&NWb@uGa zoma`4+Kbx;Ws}a?%7&lmF#NRoL?=;ws_m&hOH|bN^5yDN83Q?jv>&3XcSCE?-_>lh zdRR|qn-HRxlkIl4UdFZx|K3fu>$wnqu5}!&YpX{##_RK)BuD)7jhg zyp@+=w4>`beSa!Qco@Wh&`^DLAqvqK2<1__8QD#}NU4bmq<0@GvqTT~yb!7Xc; zXMCyZ&2(9PsFu}{1yX!1X86{2%`b{?|GzbYRe$-q}{U+#z2(G-$Xu2h=m zrQ9!1vV%fGpq`gj$Dfx-hn#Rk_`w-YFmX< zeo2>9(PQi$Co;a!Sw*4lYq$vQGBUofXZP%L&l75IbI%jAo3neWCU-qCeJmz}Cp|uW z&>a2@`vbp|{?e-4i}epzMdXZPQIuaux__z07plJ9RM?NEr7$mwxy6@CnW3MJEv$86 zeY9~~)r|HVlGD2p3f#6ebvp_h0#)BtcI36Uov-ak_43pUV7ArXwZOE+2&|)_felrB zWo_#29U4Nw_4TxhWK~{CHGMhwij_L+%nXUS zlQuJmEFt-#6>7oJf`%g}c~^myfR6b zybV?g;)$;FtaOXrfHqGEXeJO7TW7`dVMh77BL6?H$j!fbMV6}Ie|bf2C4gBrGr=7z zskI%FWE;zZvjP}P*j^bV2U4yRG^broeE5n4iiJgwj7|g+C6@c_PDDz?)SVa`hp$LL z7NoQ)kSlg4!|YCEH$&R96`T+9bWGGz-iosJcPd0=%fdX)`DIAIr*pnMd_@9s26)>dYq-uz~c? zvWai8BlD4nRG(?A<6N8|LwtvFl5IAm)=pH|IF6OODKAPqlBtiKkvNa-Zf{%g>qdR= zEcPv@)u^Phb~yVG6&QR##%4;+FtqHKh{^u6b*4eVNGvtvv_M|<&G1S?VSzx0m6g3v z`?lJECsIOv+~@{I;(SCMJ|eqGsDFYb zWH)P^Hz?XI{7-w>_=>r#? z)fN*`B02BDp^Xch!z4GhTu`kwSvO{6`@Yd~qLnl#3)j9@U*guj*jl{Yh%y@0V-?w1c=86jSb3D} z=y#VM#c4Q8hRS_C%b-!(?lGnHoDGt#X{18yK7Vrva(3B zLc_LLZQZWr+;A9OYzpRNr~*_s+BlZ5onAFvW1;2zUe8ivB@HsvmlCAuW-@0&Jbz9aAYDADO!BXA$fq* zz6Yc;1BX13Mxj5FV1pFcorvt1I&3|$`Gh#3fm{Ol!(}puii#jU-iLltC(1oFB(4la z8lm3JU?PIDupk$ypl6_dh7lBSAzL;lvzTDTJM^ax+_)HpDT zk2RQ7o&ti4>_Of_^xa-wLVX8<*FuwfkbIEbfv_qknE>-d*L`{A8KXG zoO*57llfw{u0(aed5xRD-)vTQvv2Ef<~QF)ON_5pE0{sF9Lznwp8sto8iNfs+sS$x Qbf|ROx$4=oiyz+n9rjo5;s5{u literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 53319c1e7b..02fb18ac46 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -616,6 +616,10 @@ struct UserText { static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") + static let shortcuts = NSLocalizedString("preferences.shortcuts", value: "Shortcuts", comment: "Name of the preferences section related to shortcuts") + static let isAddedToDock = NSLocalizedString("preferences.is-added-to-dock", value: "DuckDuckGo is added to the Dock.", comment: "Indicates that the browser is added to the macOS system Dock") + static let isNotAddedToDock = NSLocalizedString("preferences.not-added-to-dock", value: "DuckDuckGo is not added to the Dock.", comment: "Indicate that the browser is not added to macOS system Dock") + static let addToDock = NSLocalizedString("preferences.add-to-dock", value: "Add to Dock…", comment: "Action button to add the app to the Dock") static let onStartup = NSLocalizedString("preferences.on-startup", value: "On Startup", comment: "Name of the preferences section related to app startup") static let reopenAllWindowsFromLastSession = NSLocalizedString("preferences.reopen-windows", value: "Reopen all windows from last session", comment: "Option to control session restoration") static let showHomePage = NSLocalizedString("preferences.show-home", value: "Open a new window", comment: "Option to control session startup") @@ -794,11 +798,14 @@ struct UserText { static let onboardingWelcomeText = NSLocalizedString("onboarding.welcome.text", value: "Tired of being tracked online? You've come to the right place 👍\n\nI'll help you stay private️ as you search and browse the web. Trackers be gone!", comment: "Detailed welcome to the app text") static let onboardingImportDataText = NSLocalizedString("onboarding.importdata.text", value: "First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers.", comment: "Call to action to import data from other browsers") static let onboardingSetDefaultText = NSLocalizedString("onboarding.setdefault.text", value: "Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time.", comment: "Call to action to set the browser as default") + static let onboardingAddToDockText = NSLocalizedString("onboarding.addtodock.text", value: "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?", comment: "Call to action to add the DuckDuckGo app icon to the macOS system dock") static let onboardingStartBrowsingText = NSLocalizedString("onboarding.startbrowsing.text", value: "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser") + static let onboardingStartBrowsingAddedToDockText = NSLocalizedString("onboarding.startbrowsing.added-to-dock.text", value: "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser") static let onboardingStartButton = NSLocalizedString("onboarding.welcome.button", value: "Get Started", comment: "Start the onboarding flow") static let onboardingImportDataButton = NSLocalizedString("onboarding.importdata.button", value: "Import", comment: "Launch the import data UI") static let onboardingSetDefaultButton = NSLocalizedString("onboarding.setdefault.button", value: "Let's Do It!", comment: "Launch the set default UI") + static let onboardingAddToDockButton = NSLocalizedString("onboarding.addtodock.button", value: "Keep in Dock", comment: "Button label to add application to the macOS system dock") static let onboardingNotNowButton = NSLocalizedString("onboarding.notnow.button", value: "Maybe Later", comment: "Skip a step of the onboarding flow") static func importingBookmarks(_ numberOfBookmarks: Int?) -> String { @@ -1070,17 +1077,21 @@ struct UserText { // Set Up static let newTabSetUpSectionTitle = NSLocalizedString("newTab.setup.section.title", value: "Next Steps", comment: "Title of the setup section in the home page") static let newTabSetUpDefaultBrowserCardTitle = NSLocalizedString("newTab.setup.default.browser.title", value: "Default to Privacy", comment: "Title of the Default Browser card of the Set Up section in the home page") + static let newTabSetUpDockCardTitle = NSLocalizedString("newTab.setup.dock.title", value: "Keep in Your Dock", comment: "Title of the new tab page card for adding application to the Dock") static let newTabSetUpImportCardTitle = NSLocalizedString("newTab.setup.import.title", value: "Bring Your Stuff", comment: "Title of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerCardTitle = NSLocalizedString("newTab.setup.duck.player.title", value: "Clean Up YouTube", comment: "Title of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionCardTitle = NSLocalizedString("newTab.setup.email.protection.title", value: "Protect Your Inbox", comment: "Title of the Email Protection card of the Set Up section in the home page") static let newTabSetUpDefaultBrowserAction = NSLocalizedString("newTab.setup.default.browser.action", value: "Make Default Browser", comment: "Action title on the action menu of the Default Browser card") + static let newTabSetUpDockAction = NSLocalizedString("newTab.setup.dock.action", value: "Keep In Dock", comment: "Action title on the action menu of the 'Add App to the Dock' card") + static let newTabSetUpDockConfirmation = NSLocalizedString("newTab.setup.dock.confirmation", value: "Added to Dock!", comment: "Confirmation title after user clicks on 'Add to Dock' card") static let newTabSetUpImportAction = NSLocalizedString("newTab.setup.Import.action", value: "Import Now", comment: "Action title on the action menu of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerAction = NSLocalizedString("newTab.setup.duck.player.action", value: "Try Duck Player", comment: "Action title on the action menu of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionAction = NSLocalizedString("newTab.setup.email.protection.action", value: "Get a Duck Address", comment: "Action title on the action menu of the Email Protection card of the Set Up section in the home page") static let newTabSetUpRemoveItemAction = NSLocalizedString("newTab.setup.remove.item", value: "Dismiss", comment: "Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item") static let newTabSetUpDefaultBrowserSummary = NSLocalizedString("newTab.setup.default.browser.summary", value: "We automatically block trackers as you browse. It's privacy, simplified.", comment: "Summary of the Default Browser card") + static let newTabSetUpDockSummary = NSLocalizedString("newTab.setup.dock.summary", value: "Get to DuckDuckGo faster by adding it to your Dock.", comment: "Summary of the 'Add App to the Dock' card") static let newTabSetUpImportSummary = NSLocalizedString("newTab.setup.import.summary", value: "Import bookmarks, favorites, and passwords from your old browser.", comment: "Summary of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerSummary = NSLocalizedString("newTab.setup.duck.player.summary", value: "Enjoy a clean viewing experience without personalized ads.", comment: "Summary of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionSummary = NSLocalizedString("newTab.setup.email.protection.summary", value: "Generate custom @duck.com addresses that clean trackers from incoming email.", comment: "Summary of the Email Protection card of the Set Up section in the home page") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 5c72907b8e..13193f793f 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -117,6 +117,7 @@ public struct UserDefaultsWrapper { case homePageShowAllFavorites = "home.page.show.all.favorites" case homePageShowAllFeatures = "home.page.show.all.features" case homePageShowMakeDefault = "home.page.show.make.default" + case homePageShowAddToDock = "home.page.show.add.to.dock" case homePageShowImport = "home.page.show.import" case homePageShowDuckPlayer = "home.page.show.duck.player" case homePageShowEmailProtection = "home.page.show.email.protection" diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index b99a4a2949..22ddf43e7e 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -48,6 +48,7 @@ extension HomePage.Models { } private let defaultBrowserProvider: DefaultBrowserProvider + private let dockCustomizer: DockCustomization private let dataImportProvider: DataImportStatusProviding private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager @@ -63,6 +64,9 @@ extension HomePage.Models { @UserDefaultsWrapper(key: .homePageShowMakeDefault, defaultValue: true) private var shouldShowMakeDefaultSetting: Bool + @UserDefaultsWrapper(key: .homePageShowAddToDock, defaultValue: true) + private var shouldShowAddToDockSetting: Bool + @UserDefaultsWrapper(key: .homePageShowImport, defaultValue: true) private var shouldShowImportSetting: Bool @@ -100,6 +104,7 @@ extension HomePage.Models { @Published var visibleFeaturesMatrix: [[FeatureType]] = [[]] init(defaultBrowserProvider: DefaultBrowserProvider, + dockCustomizer: DockCustomization, dataImportProvider: DataImportStatusProviding, tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), @@ -108,6 +113,7 @@ extension HomePage.Models { privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, permanentSurveyManager: SurveyManager = PermanentSurveyManager()) { self.defaultBrowserProvider = defaultBrowserProvider + self.dockCustomizer = dockCustomizer self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager @@ -125,22 +131,15 @@ extension HomePage.Models { @MainActor func performAction(for featureType: FeatureType) { switch featureType { case .defaultBrowser: - do { - PixelKit.fire(GeneralPixel.defaultRequestedFromHomepageSetupView) - try defaultBrowserProvider.presentDefaultBrowserPrompt() - } catch { - defaultBrowserProvider.openSystemPreferences() - } + performDefaultBrowserAction() + case .dock: + performDockAction() case .importBookmarksAndPasswords: - dataImportProvider.showImportWindow(completion: {self.refreshFeaturesMatrix()}) + performImportBookmarksAndPasswordsAction() case .duckplayer: - if let videoUrl = URL(string: duckPlayerURL) { - let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - } + performDuckPlayerAction() case .emailProtection: - let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) + performEmailProtectionAction() case .permanentSurvey: visitSurvey() case .networkProtectionRemoteMessage(let message): @@ -148,16 +147,56 @@ extension HomePage.Models { case .dataBrokerProtectionRemoteMessage(let message): handle(remoteMessage: message) case .dataBrokerProtectionWaitlistInvited: -#if DBP - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .cardUI) -#endif + performDataBrokerProtectionWaitlistInvitedAction() + } + } + + private func performDefaultBrowserAction() { + do { + PixelKit.fire(GeneralPixel.defaultRequestedFromHomepageSetupView) + try defaultBrowserProvider.presentDefaultBrowserPrompt() + } catch { + defaultBrowserProvider.openSystemPreferences() } } + private func performImportBookmarksAndPasswordsAction() { + dataImportProvider.showImportWindow(completion: { self.refreshFeaturesMatrix() }) + } + + @MainActor + private func performDuckPlayerAction() { + if let videoUrl = URL(string: duckPlayerURL) { + let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + } + } + + @MainActor + private func performEmailProtectionAction() { + let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + } + + @MainActor + private func performDataBrokerProtectionWaitlistInvitedAction() { + #if DBP + DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .cardUI) + #endif + } + + func performDockAction() { + PixelKit.fire(GeneralPixel.userAddedToDockFromNewTabPageCard, + includeAppVersionParameter: false) + dockCustomizer.addToDock() + } + func removeItem(for featureType: FeatureType) { switch featureType { case .defaultBrowser: shouldShowMakeDefaultSetting = false + case .dock: + shouldShowAddToDockSetting = false case .importBookmarksAndPasswords: shouldShowImportSetting = false case .duckplayer: @@ -196,7 +235,6 @@ extension HomePage.Models { for message in homePageRemoteMessaging.networkProtectionRemoteMessaging.presentableRemoteMessages() { PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) } - appendFeatureCards(&features) featuresMatrix = features.chunked(into: itemsPerRow) @@ -214,6 +252,8 @@ extension HomePage.Models { return shouldMakeDefaultCardBeVisible case .importBookmarksAndPasswords: return shouldImportCardBeVisible + case .dock: + return shouldDockCardBeVisible case .duckplayer: return shouldDuckPlayerCardBeVisible case .emailProtection: @@ -274,6 +314,15 @@ extension HomePage.Models { !defaultBrowserProvider.isDefault } + private var shouldDockCardBeVisible: Bool { +#if !APPSTORE + shouldShowAddToDockSetting && + !dockCustomizer.isAddedToDock +#else + return false +#endif + } + private var shouldImportCardBeVisible: Bool { shouldShowImportSetting && !dataImportProvider.didImport @@ -369,12 +418,17 @@ extension HomePage.Models { // We ignore the `networkProtectionRemoteMessage` case here to avoid it getting accidentally included - it has special handling and will get // included elsewhere. static var allCases: [HomePage.Models.FeatureType] { +#if APPSTORE [.duckplayer, .emailProtection, .defaultBrowser, .importBookmarksAndPasswords, .permanentSurvey] +#else + [.duckplayer, .emailProtection, .defaultBrowser, .dock, .importBookmarksAndPasswords, .permanentSurvey] +#endif } case duckplayer case emailProtection case defaultBrowser + case dock case importBookmarksAndPasswords case permanentSurvey case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage) @@ -385,6 +439,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserCardTitle + case .dock: + return UserText.newTabSetUpDockCardTitle case .importBookmarksAndPasswords: return UserText.newTabSetUpImportCardTitle case .duckplayer: @@ -406,6 +462,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserSummary + case .dock: + return UserText.newTabSetUpDockSummary case .importBookmarksAndPasswords: return UserText.newTabSetUpImportSummary case .duckplayer: @@ -427,6 +485,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserAction + case .dock: + return UserText.newTabSetUpDockAction case .importBookmarksAndPasswords: return UserText.newTabSetUpImportAction case .duckplayer: @@ -444,12 +504,23 @@ extension HomePage.Models { } } + var confirmation: String? { + switch self { + case .dock: + return UserText.newTabSetUpDockConfirmation + default: + return nil + } + } + var icon: NSImage { let iconSize = NSSize(width: 64, height: 48) switch self { case .defaultBrowser: return .defaultApp128.resized(to: iconSize)! + case .dock: + return .dock128.resized(to: iconSize)! case .importBookmarksAndPasswords: return .import128.resized(to: iconSize)! case .duckplayer: diff --git a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift index 1fa9e6639b..e6d56fd18f 100644 --- a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift +++ b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift @@ -18,6 +18,7 @@ import SwiftUI import SwiftUIExtensions +import PixelKit extension HomePage.Views { @@ -100,7 +101,7 @@ extension HomePage.Views { .frame(width: 24, height: 24) } ZStack { - CardTemplate(title: featureType.title, summary: featureType.summary, actionText: featureType.action, icon: icon, width: model.itemWidth, height: model.itemHeight, action: { model.performAction(for: featureType) }) + CardTemplate(title: featureType.title, summary: featureType.summary, actionText: featureType.action, confirmationText: featureType.confirmation, icon: icon, width: model.itemWidth, height: model.itemHeight, action: { model.performAction(for: featureType) }) .contextMenu(ContextMenu(menuItems: { Button(featureType.action, action: { model.performAction(for: featureType) }) Divider() @@ -121,6 +122,13 @@ extension HomePage.Views { .onHover { isHovering in self.isHovering = isHovering } + .onAppear { + if featureType == .dock { + PixelKit.fire(GeneralPixel.addToDockNewTabPageCardPresented, + frequency: .unique, + includeAppVersionParameter: false) + } + } } } @@ -129,12 +137,14 @@ extension HomePage.Views { var title: String var summary: String var actionText: String + var confirmationText: String? @ViewBuilder var icon: Content let width: CGFloat let height: CGFloat let action: () -> Void @State var isHovering = false + @State var isClicked = false var body: some View { ZStack(alignment: .center) { @@ -166,7 +176,23 @@ extension HomePage.Views { .frame(width: 208, height: 130) VStack { Spacer() - ActionButton(title: actionText, isHoveringOnCard: $isHovering, action: action) + if let confirmationText, isClicked { + HStack { + Image(.successCheckmark) + Text(confirmationText) + .bold() + .multilineTextAlignment(.center) + .lineLimit(1) + .font(.system(size: 11)) + .fixedSize(horizontal: false, vertical: true) + } + .offset(y: -3) + } else { + ActionButton(title: actionText, + isHoveringOnCard: $isHovering, + isClicked: $isClicked, + action: action) + } } .padding(8) } @@ -188,11 +214,13 @@ extension HomePage.Views { @State var isHovering = false @Binding var isHoveringOnCard: Bool + @Binding var isClicked: Bool - init(title: String, isHoveringOnCard: Binding, action: @escaping () -> Void) { + init(title: String, isHoveringOnCard: Binding, isClicked: Binding, action: @escaping () -> Void) { self.title = title self.action = action self._isHoveringOnCard = isHoveringOnCard + self._isClicked = isClicked self.titleWidth = (title as NSString).size(withAttributes: [.font: NSFont.systemFont(ofSize: 11) as Any]).width + 14 } @@ -217,6 +245,7 @@ extension HomePage.Views { .foregroundColor(Color(.linkBlue)) } .onTapGesture { + isClicked = true action() } .onHover { isHovering in diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 9769333182..092a33384c 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -150,6 +150,7 @@ final class HomePageViewController: NSViewController { func createFeatureModel() -> HomePage.Models.ContinueSetUpModel { return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: SystemDefaultBrowserProvider(), + dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 9ff3c8becb..4503c03e71 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -30609,6 +30609,246 @@ } } }, + "newTab.setup.dock.action" : { + "comment" : "Action title on the action menu of the 'Add App to the Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep In Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans le Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resta nel dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In het Dock houden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык для док-панели" + } + } + } + }, + "newTab.setup.dock.confirmation" : { + "comment" : "Confirmation title after user clicks on 'Add to Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Dock hinzugefügt!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Added to Dock!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Añadido al Dock!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouté au Dock !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiunto al dock!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegevoegd aan Dock!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodano do Docka!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionado à Dock!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык добавлен на док-панель." + } + } + } + }, + "newTab.setup.dock.summary" : { + "comment" : "Summary of the 'Add App to the Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst DuckDuckGo schneller erreichen, indem du es zu deinem Dock hinzufügst." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Get to DuckDuckGo faster by adding it to your Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accede a DuckDuckGo más rápido añadiéndolo a tu Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accédez plus rapidement à DuckDuckGo en l'ajoutant à votre Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accedi più velocemente a DuckDuckGo aggiungendolo al tuo dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ga sneller naar DuckDuckGo door het aan je Dock toe te voegen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzyskuj szybszy dostęp do przeglądarki DuckDuckGo dzięki jej dodaniu do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acede ao DuckDuckGo mais rapidamente adicionando-o à tua Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавьте DuckDuckGo на док-панель для быстрого запуска." + } + } + } + }, + "newTab.setup.dock.title" : { + "comment" : "Title of the new tab page card for adding application to the Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In deinem Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep in Your Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en tu Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans votre Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni nel tuo dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bewaar in je dock" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na tua Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык для док-панели" + } + } + } + }, "newTab.setup.duck.player.action" : { "comment" : "Action title on the action menu of the Duck Player card of the Set Up section in the home page", "extractionState" : "extracted_with_value", @@ -31696,55 +31936,175 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "OK" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "OK" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De acuerdo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хорошо" + } + } + } + }, + "onboarding.addtodock.button" : { + "comment" : "Button label to add application to the macOS system dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep in Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans le Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni nel Dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Dock bewaren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Держать на док-панели" + } + } + } + }, + "onboarding.addtodock.text" : { + "comment" : "Call to action to add the DuckDuckGo app icon to the macOS system dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine letzte Sache. Du möchtest DuckDuckGo in deinem Dock haben, damit der Browser immer in Reichweite ist?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "OK" + "value" : "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "De acuerdo" + "value" : "Una última cosa. ¿Quieres tener DuckDuckGo en tu Dock para que el navegador esté siempre al alcance de la mano?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "Une dernière chose. Vous voulez garder DuckDuckGo dans votre Dock pour que le navigateur reste à portée de main ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Un'ultima cosa. Vuoi tenere DuckDuckGo nel tuo dock in modo che il browser sia sempre disponibile?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Nog een laatste ding. Wil je DuckDuckGo in je Dock houden zodat de browser altijd binnen handbereik is?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Ostatnia sprawa. Czy chcesz trzymać przeglądarkę DuckDuckGo w Docku, aby zawsze ją mieć pod ręką?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Só mais uma coisa. Queres ter o navegador DuckDuckGo na tua Dock estar sempre à mão?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Хорошо" + "value" : "И кое-что еще... Хотите сохранить DuckDuckGo на док-панели, чтобы наш браузер всегда был под рукой?" } } } @@ -32049,6 +32409,66 @@ } } }, + "onboarding.startbrowsing.added-to-dock.text" : { + "comment" : "Call to action to start using the app as a browser", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Du bist bereit! Du kannst mich jederzeit im Dock antreffen.\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglichu{00A0}🔒" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "¡Ya está todo listo! Puedes encontrarme en el Dock en cualquier momento.\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nNo pierdas de vista la barra de direcciones al navegar. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible{00A0}🔒" + } + }, + "fr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Tout est prêt ! Vous pouvez me trouver sur le Dock à tout moment.\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\nContinuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Tutto pronto! Puoi trovarmi nel dock in qualsiasi momento.\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆\n\nContinua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile{00A0} 🔒" + } + }, + "nl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Je bent helemaal klaar! Je kunt me altijd vinden in het Dock.\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆\n\nKijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Wszystko gotowe! W każdej chwili możesz mnie znaleźć w Docku.\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" + } + }, + "pt" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Está tudo pronto! Podes encontrar-me na Dock em qualquer altura.\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" + } + }, + "ru" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" + } + } + } + }, "onboarding.startbrowsing.text" : { "comment" : "Call to action to start using the app as a browser", "extractionState" : "extracted_with_value", @@ -43887,6 +44307,66 @@ } } }, + "preferences.add-to-dock" : { + "comment" : "Action button to add the app to the Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Dock hinzufügen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add to Dock…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir al Dock…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter au Dock…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi al dock…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "App toevoegen aan je dock…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj do Docka…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar à Dock…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить на док-панель…" + } + } + } + }, "preferences.always-on" : { "comment" : "Status indicator of a browser privacy protection feature.", "extractionState" : "extracted_with_value", @@ -45267,6 +45747,66 @@ } } }, + "preferences.is-added-to-dock" : { + "comment" : "Indicates that the browser is added to the macOS system Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo wird zum Dock hinzugefügt." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo is added to the Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo se ha añadido al Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo a été ajouté au Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo è stato aggiunto al dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo is toegevoegd aan het Dock." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądarka DuckDuckGo została dodana do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo foi adicionado à Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык DuckDuckGo добавлен на док-панель." + } + } + } + }, "preferences.main-settings" : { "comment" : "Section header in Preferences for main settings", "extractionState" : "extracted_with_value", @@ -45327,6 +45867,66 @@ } } }, + "preferences.not-added-to-dock" : { + "comment" : "Indicate that the browser is not added to macOS system Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo wird nicht zum Dock hinzugefügt." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo is not added to the Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo no se ha añadido al Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo n'a pas été ajouté au Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo non è stato aggiunto al dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo is niet toegevoegd aan het Dock." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądarka DuckDuckGo nie została dodana do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo não foi adicionado à Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык DuckDuckGo не добавлен на док-панель." + } + } + } + }, "preferences.off" : { "comment" : "Status indicator of a browser privacy protection feature.", "extractionState" : "extracted_with_value", @@ -45687,6 +46287,66 @@ } } }, + "preferences.shortcuts" : { + "comment" : "Name of the preferences section related to shortcuts", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcuts" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Shortcuts" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesos directos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raccourcis" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scorciatoie" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sneltoetsen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skróty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atalhos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлыки" + } + } + } + }, "preferences.show-home" : { "comment" : "Option to control session startup", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Onboarding/View/OnboardingFlow.swift b/DuckDuckGo/Onboarding/View/OnboardingFlow.swift index 721f60bb88..23b2179eaf 100644 --- a/DuckDuckGo/Onboarding/View/OnboardingFlow.swift +++ b/DuckDuckGo/Onboarding/View/OnboardingFlow.swift @@ -30,6 +30,14 @@ struct OnboardingFlow: View { @State var daxInSpeechPosition = false @State var showDialogs = false + var startBrowsingText: String { + if model.addToDockPressed { + return UserText.onboardingStartBrowsingAddedToDockText + } else { + return UserText.onboardingStartBrowsingText + } + } + var body: some View { VStack(alignment: daxInSpeechPosition ? .leading : .center) { @@ -62,7 +70,14 @@ struct OnboardingFlow: View { model.onSetDefaultSkipped() }.visibility(model.state == .setDefault ? .visible : .gone) - DaxSpeech(text: UserText.onboardingStartBrowsingText, onTypingFinished: nil) + ActionSpeech(text: UserText.onboardingAddToDockText, + actionName: UserText.onboardingAddToDockButton) { + model.onAddToDockPressed() + } skip: { + model.onAddToDockSkipped() + }.visibility(model.state == .addToDock ? .visible : .gone) + + DaxSpeech(text: startBrowsingText, onTypingFinished: nil) .visibility(model.state == .startBrowsing ? .visible : .gone) }.visibility(showDialogs ? .visible : .gone) diff --git a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift index e9448763fc..8a38258b23 100644 --- a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift +++ b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift @@ -17,6 +17,7 @@ // import SwiftUI +import PixelKit protocol OnboardingDelegate: NSObjectProtocol { @@ -26,6 +27,9 @@ protocol OnboardingDelegate: NSObjectProtocol { /// Request set default should be launched. Whatever happens, call the completion to move on to the next screen. func onboardingDidRequestSetDefault(completion: @escaping () -> Void) + /// Adding to the Dock should be launched. Whatever happens, call the completion to move on to the next screen. + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) + /// Has finished, but still showing a screen. This is when to re-enable the UI. func onboardingHasFinished() @@ -39,16 +43,27 @@ final class OnboardingViewModel: ObservableObject { case welcome case importData case setDefault + case addToDock case startBrowsing } var typingDisabled = false + var addToDockPressed = false @Published var skipTypingRequested = false @Published var state: OnboardingPhase = .startFlow { didSet { skipTypingRequested = false + + if state == .addToDock { + PixelKit.fire(GeneralPixel.addToDockOnboardingStepPresented, + includeAppVersionParameter: false) + } + if state == .startBrowsing { + PixelKit.fire(GeneralPixel.startBrowsingOnboardingStepPresented, + includeAppVersionParameter: false) + } } } @@ -105,14 +120,43 @@ final class OnboardingViewModel: ObservableObject { @MainActor func onSetDefaultPressed() { delegate?.onboardingDidRequestSetDefault { [weak self] in +#if !APPSTORE + self?.state = .addToDock +#else self?.state = .startBrowsing Self.isOnboardingFinished = true self?.delegate?.onboardingHasFinished() +#endif } } @MainActor func onSetDefaultSkipped() { +#if !APPSTORE + state = .addToDock +#else + state = .startBrowsing + Self.isOnboardingFinished = true + delegate?.onboardingHasFinished() +#endif + } + + @MainActor + func onAddToDockPressed() { + PixelKit.fire(GeneralPixel.userAddedToDockDuringOnboarding, + includeAppVersionParameter: false) + addToDockPressed = true + delegate?.onboardingDidRequestAddToDock { [weak self] in + self?.state = .startBrowsing + Self.isOnboardingFinished = true + self?.delegate?.onboardingHasFinished() + } + } + + @MainActor + func onAddToDockSkipped() { + PixelKit.fire(GeneralPixel.userSkippedAddingToDockFromOnboarding, + includeAppVersionParameter: false) state = .startBrowsing Self.isOnboardingFinished = true delegate?.onboardingHasFinished() diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index a5495827fe..45d7bf74f7 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -24,6 +24,7 @@ import PixelKit protocol DefaultBrowserProvider { var bundleIdentifier: String { get } + var defaultBrowserURL: URL? { get } var isDefault: Bool { get } func presentDefaultBrowserPrompt() throws func openSystemPreferences() @@ -37,8 +38,12 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { let bundleIdentifier: String + var defaultBrowserURL: URL? { + return NSWorkspace.shared.urlForApplication(toOpen: URL(string: "http://")!) + } + var isDefault: Bool { - guard let defaultBrowserURL = NSWorkspace.shared.urlForApplication(toOpen: URL(string: "http://")!), + guard let defaultBrowserURL = defaultBrowserURL, let ddgBrowserURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return false diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 41e92b6886..956df10da6 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -21,6 +21,7 @@ import Combine import PreferencesViews import SwiftUI import SwiftUIExtensions +import PixelKit extension Preferences { @@ -31,11 +32,48 @@ extension Preferences { @ObservedObject var tabsModel: TabsPreferences @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false + @State private var isAddedToDock = false + var dockCustomizer: DockCustomizer var body: some View { PreferencePane(UserText.general) { - // SECTION 1: On Startup + // SECTION 1: Shortcuts +#if !APPSTORE + PreferencePaneSection(UserText.shortcuts, spacing: 4) { + PreferencePaneSubSection { + HStack { + if isAddedToDock || dockCustomizer.isAddedToDock { + HStack { + Image(.successCheckmark) + Text(UserText.isAddedToDock) + } + .transition(.opacity) + .padding(.trailing, 8) + } else { + HStack { + Image(.warning).foregroundColor(Color(.linkBlue)) + Text(UserText.isNotAddedToDock) + } + .padding(.trailing, 8) + Button(action: { + withAnimation { + PixelKit.fire(GeneralPixel.userAddedToDockFromSettings, + includeAppVersionParameter: false) + dockCustomizer.addToDock() + isAddedToDock = true + } + }) { + Text(UserText.addToDock) + .fixedSize(horizontal: true, vertical: false) + .multilineTextAlignment(.center) + } + } + } + } + } +#endif + // SECTION 2: On Startup PreferencePaneSection(UserText.onStartup) { PreferencePaneSubSection { @@ -61,7 +99,7 @@ extension Preferences { } } - // SECTION 2: Tabs + // SECTION 3: Tabs PreferencePaneSection(UserText.tabs) { PreferencePaneSubSection { ToggleMenuItem(UserText.preferNewTabsToWindows, isOn: $tabsModel.preferNewTabsToWindows) @@ -80,7 +118,7 @@ extension Preferences { } } - // SECTION 3: Home Page + // SECTION 4: Home Page PreferencePaneSection(UserText.homePage) { PreferencePaneSubSection { @@ -124,12 +162,12 @@ extension Preferences { CustomHomePageSheet(startupModel: startupModel, isSheetPresented: $showingCustomHomePageSheet) } - // SECTION 4: Search Settings + // SECTION 5: Search Settings PreferencePaneSection(UserText.privateSearch) { ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions).accessibilityIdentifier("PreferencesGeneralView.showAutocompleteSuggestions") } - // SECTION 5: Downloads + // SECTION 6: Downloads PreferencePaneSection(UserText.downloads) { PreferencePaneSubSection { ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 1f4fc03135..350eb84f96 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -89,7 +89,8 @@ enum Preferences { downloadsModel: DownloadsPreferences.shared, searchModel: SearchPreferences.shared, tabsModel: TabsPreferences.shared, - dataClearingModel: DataClearingPreferences.shared) + dataClearingModel: DataClearingPreferences.shared, + dockCustomizer: DockCustomizer()) case .sync: SyncView() case .appearance: diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 84219ba446..19e7c70108 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -62,6 +62,7 @@ final class StatisticsLoader { } PixelKit.fire(GeneralPixel.serp) self.fireDailyOsVersionCounterPixel() + self.fireDockPixel() } else if !self.statisticsStore.isAppRetentionFiredToday { self.refreshAppRetentionAtb(completion: completion) } else { @@ -231,4 +232,13 @@ final class StatisticsLoader { } } + private func fireDockPixel() { + DispatchQueue.global().asyncAfter(deadline: .now() + Double.random(in: 0.5...5)) { + if DockCustomizer().isAddedToDock { + PixelKit.fire(GeneralPixel.serpAddedToDock, + includeAppVersionParameter: false) + } + } + } + } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 0380cedd98..d5846e38a7 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -146,6 +146,16 @@ enum GeneralPixel: PixelKitEventV2 { case defaultRequestedFromSettings case defaultRequestedFromOnboarding + // Adding to the Dock + case addToDockOnboardingStepPresented + case userAddedToDockDuringOnboarding + case userSkippedAddingToDockFromOnboarding + case startBrowsingOnboardingStepPresented + case addToDockNewTabPageCardPresented + case userAddedToDockFromNewTabPageCard + case userAddedToDockFromSettings + case serpAddedToDock + case protectionToggledOffBreakageReport case toggleProtectionsDailyCount case toggleReportDoNotSend @@ -530,6 +540,15 @@ enum GeneralPixel: PixelKitEventV2 { case .defaultRequestedFromSettings: return "m_mac_default_requested_from_settings" case .defaultRequestedFromOnboarding: return "m_mac_default_requested_from_onboarding" + case .addToDockOnboardingStepPresented: return "m_mac_add_to_dock_onboarding_step_presented" + case .userAddedToDockDuringOnboarding: return "m_mac_user_added_to_dock_during_onboarding" + case .userSkippedAddingToDockFromOnboarding: return "m_mac_user_skipped_adding_to_dock_from_onboarding" + case .startBrowsingOnboardingStepPresented: return "m_mac_start_browsing_onboarding_step_presented" + case .addToDockNewTabPageCardPresented: return "m_mac_add_to_dock_new_tab_page_card_presented_u" + case .userAddedToDockFromNewTabPageCard: return "m_mac_user_added_to_dock_from_new_tab_page_card" + case .userAddedToDockFromSettings: return "m_mac_user_added_to_dock_from_settings" + case .serpAddedToDock: return "m_mac_serp_added_to_dock" + case .protectionToggledOffBreakageReport: return "m_mac_protection-toggled-off-breakage-report" case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" case .toggleReportDoNotSend: return "m_mac_toggle-report-do-not-send" diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index dde4a10a29..b785424d39 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -41,6 +41,7 @@ final class BrowserTabViewController: NSViewController { private let tabCollectionViewModel: TabCollectionViewModel private let bookmarkManager: BookmarkManager + private let dockCustomizer = DockCustomizer() private var tabViewModelCancellables = Set() private var activeUserDialogCancellable: Cancellable? @@ -1139,6 +1140,11 @@ extension BrowserTabViewController: OnboardingDelegate { } } + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) { + dockCustomizer.addToDock() + completion() + } + func onboardingHasFinished() { (view.window?.windowController as? MainWindowController)?.userInteraction(prevented: false) } diff --git a/UnitTests/App/DockCustomizerMock.swift b/UnitTests/App/DockCustomizerMock.swift new file mode 100644 index 0000000000..5e8812bad0 --- /dev/null +++ b/UnitTests/App/DockCustomizerMock.swift @@ -0,0 +1,39 @@ +// +// DockCustomizerMock.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import DuckDuckGo_Privacy_Browser + +class DockCustomizerMock: DockCustomization { + private var dockStatus: Bool = false + + var isAddedToDock: Bool { + return dockStatus + } + + @discardableResult + func addToDock() -> Bool { + if !dockStatus { + dockStatus = true + return true + } else { + return false + } + } +} diff --git a/UnitTests/App/DockPositionProviderTests.swift b/UnitTests/App/DockPositionProviderTests.swift new file mode 100644 index 0000000000..d7eb2895cb --- /dev/null +++ b/UnitTests/App/DockPositionProviderTests.swift @@ -0,0 +1,48 @@ +// +// DockPositionProviderTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +class DockPositionProviderTests: XCTestCase { + + var provider: DockPositionProvider! + var mockBrowserProvider: DefaultBrowserProviderMock! + + override func setUp() { + super.setUp() + mockBrowserProvider = DefaultBrowserProviderMock() + provider = DockPositionProvider(defaultBrowserProvider: mockBrowserProvider) + } + + override func tearDown() { + provider = nil + mockBrowserProvider = nil + super.tearDown() + } + + func testWhenNotDefaultBrowser_ThenIndexIsNextToDefault() { + mockBrowserProvider.isDefault = false + mockBrowserProvider.defaultBrowserURL = URL(string: "file:///Applications/Firefox.app/")! + let currentApps = [URL(string: "file:///Applications/Safari.app/")!, URL(string: "file:///Applications/Firefox.app/")!, URL(string: "file:///Applications/Arc.app/")!] + let index = provider.newDockIndex(from: currentApps) + + XCTAssertEqual(index, 2, "The new app should be placed next to default browser.") + } + +} diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index f7c2060ffc..9d49ab4073 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -69,6 +69,7 @@ final class ContinueSetUpModelTests: XCTestCase { var coookiePopupProtectionPreferences: MockCookiePopupProtectionPreferencesPersistor! var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! + var dockCustomizer: DockCustomization! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @MainActor override func setUp() { @@ -86,6 +87,7 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() + dockCustomizer = DockCustomizerMock() #if DBP let messaging = HomePageRemoteMessaging( @@ -103,6 +105,7 @@ final class ContinueSetUpModelTests: XCTestCase { vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -146,6 +149,7 @@ final class ContinueSetUpModelTests: XCTestCase { vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -226,6 +230,7 @@ final class ContinueSetUpModelTests: XCTestCase { @MainActor func testWhenAskedToPerformActionForImportPromptThrowsThenItOpensImportWindow() { let numberOfFeatures = HomePage.Models.FeatureType.allCases.count - 1 + vm.shouldShowAllFeatures = true XCTAssertEqual(vm.visibleFeaturesMatrix.flatMap { $0 }.count, numberOfFeatures) @@ -353,10 +358,12 @@ final class ContinueSetUpModelTests: XCTestCase { emailStorage.isEmailProtectionEnabled = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true capturingDataImportProvider.didImport = true + dockCustomizer.addToDock() userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -385,6 +392,11 @@ final class ContinueSetUpModelTests: XCTestCase { vm.removeItem(for: .emailProtection) XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.emailProtection)) +#if !APPSTORE + vm.removeItem(for: .dock) + XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.dock)) +#endif + let vm2 = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) XCTAssertTrue(vm2.visibleFeaturesMatrix.flatMap { $0 }.isEmpty) } @@ -461,6 +473,7 @@ final class ContinueSetUpModelTests: XCTestCase { userDefaults.set(true, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) let vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -512,6 +525,27 @@ final class ContinueSetUpModelTests: XCTestCase { #endif } + @MainActor func test_WhenUserDoesntHaveApplicationInTheDock_ThenAddToDockCardIsDisplayed() { +#if !APPSTORE + let dockCustomizer = DockCustomizerMock() + + let vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, dockCustomizer: dockCustomizer) + vm.shouldShowAllFeatures = true + + XCTAssert(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.dock)) +#endif + } + + @MainActor func test_WhenUserHasApplicationInTheDock_ThenAddToDockCardIsNotDisplayed() { + let dockCustomizer = DockCustomizerMock() + dockCustomizer.addToDock() + + let vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, dockCustomizer: dockCustomizer) + vm.shouldShowAllFeatures = true + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.dock)) + } + } extension HomePage.Models.ContinueSetUpModel { @@ -523,7 +557,8 @@ extension HomePage.Models.ContinueSetUpModel { privacyConfig: MockPrivacyConfiguration = MockPrivacyConfiguration(), appGroupUserDefaults: UserDefaults, permanentSurveyManager: MockPermanentSurveyManager = MockPermanentSurveyManager(), - randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator() + randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator(), + dockCustomizer: DockCustomization = DockCustomizerMock() ) -> HomePage.Models.ContinueSetUpModel { privacyConfig.featureSettings = [ "networkProtection": "disabled" @@ -547,6 +582,7 @@ extension HomePage.Models.ContinueSetUpModel { return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: defaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: dataImportProvider, tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, diff --git a/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift b/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift index 9931c9de94..dc544f2e5b 100644 --- a/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift +++ b/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift @@ -21,6 +21,8 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser class CapturingDefaultBrowserProvider: DefaultBrowserProvider { + var defaultBrowserURL: URL? + var presentDefaultBrowserPromptCalled = false var openSystemPreferencesCalled = false var throwError = false diff --git a/UnitTests/Onboarding/OnboardingTests.swift b/UnitTests/Onboarding/OnboardingTests.swift index f0366d6498..a123262061 100644 --- a/UnitTests/Onboarding/OnboardingTests.swift +++ b/UnitTests/Onboarding/OnboardingTests.swift @@ -43,13 +43,23 @@ class OnboardingTests: XCTestCase { assertStateChange(model, .startFlow, .welcome, model.onSplashFinished) assertStateChange(model, .welcome, .importData, model.onStartPressed) assertStateChange(model, .importData, .setDefault, model.onImportPressed) +#if APPSTORE assertStateChange(model, .setDefault, .startBrowsing, model.onSetDefaultPressed) +#else + assertStateChange(model, .setDefault, .addToDock, model.onSetDefaultPressed) + assertStateChange(model, .addToDock, .startBrowsing, model.onAddToDockPressed) +#endif model.state = .importData assertStateChange(model, .importData, .setDefault, model.onImportSkipped) model.state = .setDefault +#if APPSTORE assertStateChange(model, .setDefault, .startBrowsing, model.onSetDefaultSkipped) +#else + assertStateChange(model, .setDefault, .addToDock, model.onSetDefaultSkipped) + assertStateChange(model, .addToDock, .startBrowsing, model.onAddToDockSkipped) +#endif } func testWhenImportPressedDelegateIsCalled() { @@ -79,12 +89,37 @@ class OnboardingTests: XCTestCase { model.onSetDefaultSkipped() XCTAssertEqual(0, delegate.didRequestImportDataCalled) XCTAssertEqual(0, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) +#if APPSTORE XCTAssertEqual(1, delegate.hasFinishedCalled) +#else + XCTAssertEqual(0, delegate.hasFinishedCalled) +#endif model.onSetDefaultPressed() XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) +#if APPSTORE XCTAssertEqual(2, delegate.hasFinishedCalled) +#else + XCTAssertEqual(0, delegate.hasFinishedCalled) +#endif + +#if !APPSTORE + model.onAddToDockSkipped() + XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) + XCTAssertEqual(1, delegate.hasFinishedCalled) +#endif + +#if !APPSTORE + model.onAddToDockPressed() + XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(2, delegate.hasFinishedCalled) +#endif XCTAssertTrue(onboardingFinished) } @@ -114,6 +149,7 @@ class OnboardingTests: XCTestCase { final class MockOnboardingDelegate: NSObject, OnboardingDelegate { var didRequestImportDataCalled = 0 var didRequestSetDefaultCalled = 0 + var didRequestAddToDockCalled = 0 var hasFinishedCalled = 0 func onboardingDidRequestImportData(completion: @escaping () -> Void) { @@ -126,6 +162,11 @@ final class MockOnboardingDelegate: NSObject, OnboardingDelegate { completion() } + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) { + didRequestAddToDockCalled += 1 + completion() + } + func onboardingHasFinished() { hasFinishedCalled += 1 } diff --git a/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift b/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift index c7e2aae416..d8106d7574 100644 --- a/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift +++ b/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift @@ -20,11 +20,13 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser final class DefaultBrowserProviderMock: DefaultBrowserProvider { + enum MockError: Error { case generic } var bundleIdentifier: String = "com.duckduckgo.DefaultBrowserPreferencesTests" + var defaultBrowserURL: URL? var isDefault: Bool = false var _presentDefaultBrowserPrompt: () throws -> Void = {} var _openSystemPreferences: () -> Void = {} From d128cb41a9d540e8b07d2549c4b883c7424d89b7 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Tue, 21 May 2024 09:01:08 -0500 Subject: [PATCH 11/26] Fix top level navigation blocks (#2792) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207278769338326/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. See BSK PR --- ###### 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 | 9 +-------- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index acd1b22af2..ebf54146aa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -681,7 +681,6 @@ 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3706FA71293F65D500E42796 /* BrowserServicesKit */; }; 3706FCA9293F65D500E42796 /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 3706FA76293F65D500E42796 /* ContentBlocking */; }; 3706FCAA293F65D500E42796 /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 3706FA78293F65D500E42796 /* UserScript */; }; - 3706FCAB293F65D500E42796 /* TrackerRadarKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3706FA6B293F65D500E42796 /* TrackerRadarKit */; }; 3706FCAF293F65D500E42796 /* PrivacyDashboard in Frameworks */ = {isa = PBXBuildFile; productRef = 3706FA77293F65D500E42796 /* PrivacyDashboard */; }; 3706FCB4293F65D500E42796 /* CrashReports.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA693E5D2696E5B90007BB78 /* CrashReports.storyboard */; }; 3706FCB5293F65D500E42796 /* trackerData.json in Resources */ = {isa = PBXBuildFile; fileRef = 9833913027AAA4B500DAF119 /* trackerData.json */; }; @@ -4147,7 +4146,6 @@ B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, - 3706FCAB293F65D500E42796 /* TrackerRadarKit in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, 3739326529AE4B39009346AE /* DDGSync in Frameworks */, @@ -8298,7 +8296,6 @@ ); name = "DuckDuckGo Privacy Browser App Store"; packageProductDependencies = ( - 3706FA6B293F65D500E42796 /* TrackerRadarKit */, 3706FA71293F65D500E42796 /* BrowserServicesKit */, 3706FA76293F65D500E42796 /* ContentBlocking */, 3706FA77293F65D500E42796 /* PrivacyDashboard */, @@ -12952,7 +12949,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 145.3.2; + version = 145.3.3; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13064,10 +13061,6 @@ isa = XCSwiftPackageProductDependency; productName = DataBrokerProtection; }; - 3706FA6B293F65D500E42796 /* TrackerRadarKit */ = { - isa = XCSwiftPackageProductDependency; - productName = TrackerRadarKit; - }; 3706FA71293F65D500E42796 /* BrowserServicesKit */ = { isa = XCSwiftPackageProductDependency; package = 3706FA72293F65D500E42796 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4c9f1a616b..a0e5fe45de 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" : "1c692ce52ffd74edd1d1180e038a14a0cf1dd736", - "version" : "145.3.2" + "revision" : "a49bbac8aa58033981a5a946d220886366dd471b", + "version" : "145.3.3" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { - "revision" : "6c84fd19139414fc0edbf9673ade06e532a564f0", - "version" : "2.0.0" + "revision" : "c01e6a59d000356b58ec77053e0a99d538be56a5", + "version" : "2.1.1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 23fc546a25..b199cd1256 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.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.3"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 0b2dcd36e1..6bdacb0bff 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.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.3"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index a2bd8d37af..cd969a3ce6 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.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "145.3.3"), .package(path: "../SwiftUIExtensions") ], targets: [ From d4fe524a948cba0f987a95abb0f3e34988d5efb8 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Tue, 21 May 2024 13:09:38 -0500 Subject: [PATCH 12/26] Update age params for multiple brokers (#2800) Task/Issue URL: https://app.asana.com/0/608920331025329/1207360142555452/f Tech Design URL: CC: **Description**: Updates several brokers to remove an unnecessarily restrictive age filter, or apply the age filter properly (with `ageRange`). See [parent task](https://app.asana.com/0/608920331025329/1207348245954369) for further details. **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/advancedbackgroundchecks.com.json | 4 ++-- .../DataBrokerProtection/Resources/JSON/councilon.com.json | 4 ++-- .../DataBrokerProtection/Resources/JSON/kwold.com.json | 4 ++-- .../Resources/JSON/peoplefinders.com.json | 4 ++-- .../Resources/JSON/usa-people-search.com.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index d514b04052..f228be32c0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,7 +1,7 @@ { "name": "AdvancedBackgroundChecks", "url": "advancedbackgroundchecks.com", - "version": "0.1.7", + "version": "0.1.8", "parent": "peoplefinders.com", "addedDatetime": 1678060800000, "steps": [ @@ -12,7 +12,7 @@ { "actionType": "navigate", "id": "070b6417-8ccd-4111-b5ae-7ae470b0399a", - "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" + "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}" }, { "actionType": "extract", diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json index 08485294f7..c308a55a7f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json @@ -1,7 +1,7 @@ { "name": "Councilon", "url": "councilon.com", - "version": "0.1.5", + "version": "0.1.6", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -12,7 +12,7 @@ { "actionType": "navigate", "id": "ac6caadd-7930-4776-8473-9403b568611e", - "url": "https://councilon.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", + "url": "https://councilon.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", "31-40", diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json index 46f723a0d5..7c4f1d5b27 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json @@ -1,7 +1,7 @@ { "name": "Kwold", "url": "kwold.com", - "version": "0.1.5", + "version": "0.1.6", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -12,7 +12,7 @@ { "actionType": "navigate", "id": "878e00ab-dbad-4ca9-a303-645702a36ee2", - "url": "https://kwold.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", + "url": "https://kwold.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", "31-40", diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json index e9d117aa44..4736da3b98 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json @@ -1,7 +1,7 @@ { "name": "PeopleFinders", "url": "peoplefinders.com", - "version": "0.1.5", + "version": "0.1.6", "addedDatetime": 1677132000000, "steps": [ { @@ -11,7 +11,7 @@ { "actionType": "navigate", "id": "12c69911-415b-4904-8162-d7993d44e348", - "url": "https://www.peoplefinders.com/people/${firstName}-${lastName}/${state}/${city}?landing=all&age=${age}" + "url": "https://www.peoplefinders.com/people/${firstName}-${lastName}/${state}/${city}?landing=all" }, { "actionType": "extract", diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json index 0f276a2b0a..be21d07bc7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json @@ -1,7 +1,7 @@ { "name": "USA People Search", "url": "usa-people-search.com", - "version": "0.1.5", + "version": "0.1.6", "parent": "peoplefinders.com", "addedDatetime": 1678082400000, "steps": [ @@ -12,7 +12,7 @@ { "actionType": "navigate", "id": "3cb9a2ce-6443-4934-9df3-ec63a181d9bb", - "url": "https://usa-people-search.com/name/${firstName|downcase}-${lastName|downcase}/${city|downcase}-${state|stateFull|downcase}?age=${age}" + "url": "https://usa-people-search.com/name/${firstName|downcase}-${lastName|downcase}/${city|downcase}-${state|stateFull|downcase}" }, { "actionType": "extract", From 8dd682c91deef8949f4a75c98091b99d766b5b8d Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 22 May 2024 21:16:43 +0100 Subject: [PATCH 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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] }