diff --git a/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig b/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig index 85193a1bf7..9897b7d75b 100644 --- a/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig +++ b/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig @@ -17,10 +17,10 @@ #include "../../DeveloperID.xcconfig" // Override AppTargetsBase.xcconfig until we resolve bundle IDs. -PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(DBP_AGENT_BUNDLE_ID) -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(DBP_AGENT_BUNDLE_ID) -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(DBP_AGENT_BUNDLE_ID) -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(DBP_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) INFOPLIST_FILE = DuckDuckGoDBPBackgroundAgent/Info.plist GENERATE_INFOPLIST_FILE = YES @@ -40,7 +40,7 @@ CODE_SIGN_IDENTITY[sdk=macosx*] = Developer ID Application CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = -PRODUCT_NAME = $(DBP_AGENT_PRODUCT_NAME) +PRODUCT_NAME = $(DBP_BACKGROUND_AGENT_PRODUCT_NAME) PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS DBP Agent - Review diff --git a/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig b/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig index a953696765..e9a31677df 100644 --- a/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig +++ b/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig @@ -17,10 +17,10 @@ #include "../../AppStore.xcconfig" // Override AppTargetsBase.xcconfig until we resolve bundle IDs. -PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(DBP_AGENT_BUNDLE_ID) -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(DBP_AGENT_BUNDLE_ID) -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(DBP_AGENT_BUNDLE_ID) -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(DBP_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID) INFOPLIST_FILE = DuckDuckGoDBPBackgroundAgent/Info.plist GENERATE_INFOPLIST_FILE = YES @@ -40,7 +40,7 @@ CODE_SIGN_IDENTITY[sdk=macosx*] = Developer ID Application CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = -PRODUCT_NAME = $(DBP_AGENT_PRODUCT_NAME) +PRODUCT_NAME = $(DBP_BACKGROUND_AGENT_PRODUCT_NAME) PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS DBP Agent - Review diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index e30e795825..ec99e58e5f 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -59,12 +59,6 @@ NOTIFICATIONS_AGENT_PRODUCT_NAME = DuckDuckGo Notifications AGENT_BUNDLE_ID_BASE[sdk=*] = com.duckduckgo.macos.vpn -DBP_AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent -DBP_AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent.debug -DBP_AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent.debug -DBP_AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent.review -DBP_AGENT_PRODUCT_NAME = DuckDuckGoDBPBackgroundAgent - DBP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.dbp DBP_APP_GROUP[config=CI][sdk=*] = $(DBP_BASE_APP_GROUP).debug DBP_APP_GROUP[config=Review][sdk=*] = $(DBP_BASE_APP_GROUP).review @@ -78,8 +72,8 @@ AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN DBP_BACKGROUND_AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent -DBP_BACKGROUND_AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent.debug -DBP_BACKGROUND_AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent.debug -DBP_BACKGROUND_AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.macos.DBP.backgroundAgent.review +DBP_BACKGROUND_AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID).debug +DBP_BACKGROUND_AGENT_BUNDLE_ID[config=CI][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID).debug +DBP_BACKGROUND_AGENT_BUNDLE_ID[config=Review][sdk=*] = $(DBP_BACKGROUND_AGENT_BUNDLE_ID).review -DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGoDBPBackgroundAgent +DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGo Personal Information Removal diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b4f700f1a4..ded39000c7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2219,8 +2219,8 @@ 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */; }; 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFE95512A9DF1CE0081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift */; }; - 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGoDBPBackgroundAgent.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 7BFF85102B0C09E300ECACA2 /* DuckDuckGoDBPBackgroundAgent.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7BFF85102B0C09E300ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85012B0129133F9F003D0DCC /* NavigationBarPopovers.swift */; }; 850E8DFB2A6FEC5E00691187 /* BookmarksBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */; }; 8511E18425F82B34002F516B /* 01_Fire_really_small.json in Resources */ = {isa = PBXBuildFile; fileRef = 8511E18325F82B34002F516B /* 01_Fire_really_small.json */; }; @@ -3125,7 +3125,7 @@ dstPath = Contents/Library/LoginItems; dstSubfolderSpec = 1; files = ( - 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGoDBPBackgroundAgent.app in Embed Login Items */, + 7BFF850F2B0C09DA00ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */, 4B2D065F2A11D2D700DE1F49 /* DuckDuckGo VPN.app in Embed Login Items */, 4B2D065E2A11D2D700DE1F49 /* DuckDuckGo Notifications.app in Embed Login Items */, ); @@ -3138,7 +3138,7 @@ dstPath = Contents/Library/LoginItems; dstSubfolderSpec = 1; files = ( - 7BFF85102B0C09E300ECACA2 /* DuckDuckGoDBPBackgroundAgent.app in Embed Login Items */, + 7BFF85102B0C09E300ECACA2 /* DuckDuckGo Personal Information Removal.app in Embed Login Items */, 4B957C342AC7AE700062CA31 /* DuckDuckGo VPN.app in Embed Login Items */, 4B957C352AC7AE700062CA31 /* DuckDuckGo Notifications.app in Embed Login Items */, ); @@ -3804,7 +3804,7 @@ 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LoginItem+DataBrokerProtection.swift"; sourceTree = ""; }; 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LoginItem+NetworkProtection.swift"; sourceTree = ""; }; 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginItemsManager.swift; sourceTree = ""; }; - 9D9AE8D12AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DuckDuckGoDBPBackgroundAgent.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DuckDuckGo Personal Information Removal.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9D9AE8F22AAA39D30026E7DC /* .app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = .app; sourceTree = BUILT_PRODUCTS_DIR; }; 9D9AE9142AAA3B450026E7DC /* Info-AppStore.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-AppStore.plist"; sourceTree = ""; }; 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoDBPBackgroundAgentAppDelegate.swift; sourceTree = ""; }; @@ -6496,7 +6496,7 @@ 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */, 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */, 4B2D06692A13318400DE1F49 /* DuckDuckGo VPN App Store.app */, - 9D9AE8D12AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent.app */, + 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */, 9D9AE8F22AAA39D30026E7DC /* .app */, 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */, 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */, @@ -8408,7 +8408,7 @@ 9DEF97E02B06C4EE00764F03 /* Networking */, ); productName = DuckDuckGoAgent; - productReference = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent.app */; + productReference = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; productType = "com.apple.product-type.application"; }; 9D9AE8D32AAA39D30026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore */ = { diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 5376f28302..eeb5d9f6c6 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -23,6 +23,10 @@ import Foundation import NetworkProtection #endif +#if DBP +import DataBrokerProtection +#endif + @MainActor final class URLEventHandler { @@ -100,9 +104,19 @@ final class URLEventHandler { private static func openURL(_ url: URL) { #if NETWORK_PROTECTION - if url.scheme == "networkprotection" { + if url.scheme?.isNetworkProtectionScheme == true { handleNetworkProtectionURL(url) - } else { + } +#endif + +#if DBP + if url.scheme?.isDataBrokerProtectionScheme == true { + handleDataBrokerProtectionURL(url) + } +#endif + +#if NETWORK_PROTECTION || DBP + if url.scheme?.isNetworkProtectionScheme == false && url.scheme?.isDataBrokerProtectionScheme == false { WaitlistModalDismisser.dismissWaitlistModalViewControllerIfNecessary(url) WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) } @@ -111,7 +125,7 @@ final class URLEventHandler { #endif } -#if NETWORK_PROTECTION +#if NETWORK_PROTECTION || DBP /// Handles NetP URLs /// @@ -132,4 +146,31 @@ final class URLEventHandler { #endif +#if DBP + /// Handles DBP URLs + /// + private static func handleDataBrokerProtectionURL(_ url: URL) { + switch url { + case DataBrokerProtectionNotificationCommand.showDashboard.url: + WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + default: + return + } + } + +#endif + +} + +private extension String { + static let dataBrokerProtectionScheme = "databrokerprotection" + static let networkProtectionScheme = "networkprotection" + + var isDataBrokerProtectionScheme: Bool { + return self == String.dataBrokerProtectionScheme + } + + var isNetworkProtectionScheme: Bool { + return self == String.networkProtectionScheme + } } diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 56d36b2afa..8ae4d52dd5 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -133,6 +133,7 @@ extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDeleg public class DataBrokerProtectionPixelsHandler: EventMapping { + // swiftlint:disable:next function_body_length public init() { super.init { event, _, _, _ in switch event { @@ -175,7 +176,15 @@ public class DataBrokerProtectionPixelsHandler: EventMappingnetworkprotection + + CFBundleTypeRole + Viewer + CFBundleURLName + DataBroker Protection URLs + CFBundleURLSchemes + + databrokerprotection + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift index 526288a62c..787a859f90 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift @@ -56,12 +56,17 @@ public final class DataBrokerProtectionBackgroundManager { sessionKey: sessionKey, featureToggles: features) + let pixelHandler = DataBrokerProtectionPixelsHandler() + + let userNotificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) + return DefaultDataBrokerProtectionScheduler(privacyConfigManager: privacyConfigurationManager, - contentScopeProperties: prefs, - dataManager: dataManager, - notificationCenter: NotificationCenter.default, - pixelHandler: DataBrokerProtectionPixelsHandler(), - redeemUseCase: redeemUseCase) + contentScopeProperties: prefs, + dataManager: dataManager, + notificationCenter: NotificationCenter.default, + pixelHandler: pixelHandler, + redeemUseCase: redeemUseCase, + userNotificationService: userNotificationService) }() private init() { diff --git a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index ea9284cf51..918fa1a982 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -45,7 +45,7 @@ let nonSandboxedExtraInputFiles: Set = [ .init("VPNMetadataCollector.swift", .source), .init("VPNFeedbackCategory.swift", .source), .init("VPNFeedbackSender.swift", .source), - .init("DuckDuckGoDBPBackgroundAgent.app", .unknown), + .init("DuckDuckGo Personal Information Removal.app", .unknown), .init("DataBrokerProtectionSubscriptionEventHandler.swift", .source) ] diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index fd6b0cddcf..7a664c2c60 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -39,6 +39,7 @@ final class DataBrokerOperationsCollection: Operation { 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)) @@ -52,6 +53,7 @@ final class DataBrokerOperationsCollection: Operation { notificationCenter: NotificationCenter = NotificationCenter.default, runner: WebOperationRunner, pixelHandler: EventMapping, + userNotificationService: DataBrokerProtectionUserNotificationService, showWebView: Bool) { self.brokerProfileQueriesData = brokerProfileQueriesData @@ -63,6 +65,7 @@ final class DataBrokerOperationsCollection: Operation { self.runner = runner self.pixelHandler = pixelHandler self.showWebView = showWebView + self.userNotificationService = userNotificationService super.init() } @@ -145,6 +148,7 @@ final class DataBrokerOperationsCollection: Operation { runner: runner, pixelHandler: pixelHandler, showWebView: showWebView, + userNotificationService: userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } return !self.isCancelled diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 08820bdf04..496a15c81d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -35,6 +35,7 @@ protocol OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, showWebView: Bool, + userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -45,6 +46,7 @@ extension OperationsManager { notificationCenter: NotificationCenter, runner: WebOperationRunner, pixelHandler: EventMapping, + userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { try await runOperation(operationData: operationData, @@ -54,6 +56,7 @@ extension OperationsManager { runner: runner, pixelHandler: pixelHandler, showWebView: false, + userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } } @@ -67,6 +70,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { runner: WebOperationRunner, pixelHandler: EventMapping, showWebView: Bool = false, + userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { if operationData as? ScanOperationData != nil { @@ -76,6 +80,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, + userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } else if let optOutOperationData = operationData as? OptOutOperationData { try await runOptOutOperation(for: optOutOperationData.extractedProfile, @@ -85,17 +90,19 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, + userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } } - // swiftlint:disable:next function_body_length + // swiftlint:disable:next cyclomatic_complexity function_body_length internal func runScanOperation(on runner: WebOperationRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, notificationCenter: NotificationCenter, pixelHandler: EventMapping, showWebView: Bool = false, + userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { os_log("Running scan operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) @@ -174,12 +181,13 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } if !removedProfiles.isEmpty { + var shouldSendProfileRemovedNotification = false for removedProfile in removedProfiles { if let extractedProfileId = removedProfile.id { let event = HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutConfirmed) database.add(event) database.updateRemovedDate(Date(), on: extractedProfileId) - + shouldSendProfileRemovedNotification = true try updateOperationDataDates( origin: .scan, brokerId: brokerId, @@ -191,7 +199,6 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { os_log("Profile removed from optOutsData: %@", log: .dataBrokerProtection, String(describing: removedProfile)) - // Add a comment explaining this piece of code if let attempt = database.fetchAttemptInformation(for: extractedProfileId), let attemptUUID = UUID(uuidString: attempt.attemptId) { let now = Date() let calculateDurationSinceLastStage = now.timeIntervalSince(attempt.lastStageDate) * 1000 @@ -201,6 +208,10 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } } } + if shouldSendProfileRemovedNotification { + sendProfileRemovedNotificationIfNecessary(userNotificationService: userNotificationService, + database: database) + } } else { try updateOperationDataDates( origin: .scan, @@ -224,6 +235,20 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { throw error } } + + private func sendProfileRemovedNotificationIfNecessary(userNotificationService: DataBrokerProtectionUserNotificationService, database: DataBrokerProtectionRepository) { + let savedExtractedProfiles = database.fetchAllBrokerProfileQueryData().flatMap { $0.extractedProfiles } + if savedExtractedProfiles.count == 1 { + userNotificationService.sendAllInfoRemovedNotificationIfPossible() + } else { + if savedExtractedProfiles.allSatisfy({ $0.removedDate != nil }) { + userNotificationService.sendAllInfoRemovedNotificationIfPossible() + } else { + userNotificationService.sendFirstRemovedNotificationIfPossible() + } + } + } + // swiftlint:disable:next function_body_length internal func runOptOutOperation(for extractedProfile: ExtractedProfile, on runner: WebOperationRunner, @@ -232,6 +257,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: NotificationCenter, pixelHandler: EventMapping, showWebView: Bool = false, + userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { guard let brokerId = brokerProfileQueryData.dataBroker.id, let profileQueryId = brokerProfileQueryData.profileQuery.id, let extractedProfileId = extractedProfile.id else { // Maybe send pixel? diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index cf6565d59c..0b1f6eedf6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -243,6 +243,16 @@ public enum DataBrokerProtectionPixels { case disableLoginItem case resetLoginItem + // DataBrokerProtection User Notifications + case dataBrokerProtectionNotificationSentFirstScanComplete + case dataBrokerProtectionNotificationOpenedFirstScanComplete + case dataBrokerProtectionNotificationSentFirstRemoval + case dataBrokerProtectionNotificationOpenedFirstRemoval + case dataBrokerProtectionNotificationScheduled2WeeksCheckIn + case dataBrokerProtectionNotificationOpened2WeeksCheckIn + case dataBrokerProtectionNotificationSentAllRecordsRemoved + case dataBrokerProtectionNotificationOpenedAllRecordsRemoved + // Scan/Search pixels case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int) case scanFailed(dataBroker: String, duration: Double, tries: Int) @@ -302,6 +312,24 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .restartLoginItem: return "m_mac_dbp_login-item_restart" case .disableLoginItem: return "m_mac_dbp_login-item_disable" case .resetLoginItem: return "m_mac_dbp_login-item_reset" + + // User Notifications + case .dataBrokerProtectionNotificationSentFirstScanComplete: + return "m_mac_dbp_notification_sent_first_scan_complete" + case .dataBrokerProtectionNotificationOpenedFirstScanComplete: + return "m_mac_dbp_notification_opened_first_scan_complete" + case .dataBrokerProtectionNotificationSentFirstRemoval: + return "m_mac_dbp_notification_sent_first_removal" + case .dataBrokerProtectionNotificationOpenedFirstRemoval: + return "m_mac_dbp_notification_opened_first_removal" + case .dataBrokerProtectionNotificationScheduled2WeeksCheckIn: + return "m_mac_dbp_notification_scheduled_2_weeks_check_in" + case .dataBrokerProtectionNotificationOpened2WeeksCheckIn: + return "m_mac_dbp_notification_opened_2_weeks_check_in" + case .dataBrokerProtectionNotificationSentAllRecordsRemoved: + return "m_mac_dbp_notification_sent_all_records_removed" + case .dataBrokerProtectionNotificationOpenedAllRecordsRemoved: + return "m_mac_dbp_notification_opened_all_records_removed" } } @@ -356,7 +384,15 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .enableLoginItem, .restartLoginItem, .disableLoginItem, - .resetLoginItem: + .resetLoginItem, + .dataBrokerProtectionNotificationSentFirstScanComplete, + .dataBrokerProtectionNotificationOpenedFirstScanComplete, + .dataBrokerProtectionNotificationSentFirstRemoval, + .dataBrokerProtectionNotificationOpenedFirstRemoval, + .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, + .dataBrokerProtectionNotificationOpened2WeeksCheckIn, + .dataBrokerProtectionNotificationSentAllRecordsRemoved, + .dataBrokerProtectionNotificationOpenedAllRecordsRemoved: return [:] case .ipcServerRegister, .ipcServerStartScheduler, @@ -381,6 +417,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { public class DataBrokerProtectionPixelsHandler: EventMapping { + // swiftlint:disable:next function_body_length public init() { super.init { event, _, _, _ in switch event { @@ -422,7 +459,16 @@ public class DataBrokerProtectionPixelsHandler: EventMapping + private let userNotificationService: DataBrokerProtectionUserNotificationService init(database: DataBrokerProtectionRepository, config: SchedulerConfig, operationRunnerProvider: OperationRunnerProvider, notificationCenter: NotificationCenter = NotificationCenter.default, - pixelHandler: EventMapping) { + pixelHandler: EventMapping, + userNotificationService: DataBrokerProtectionUserNotificationService) { self.database = database self.config = config @@ -45,6 +47,7 @@ final class DataBrokerProtectionProcessor { self.operationQueue = OperationQueue() self.pixelHandler = pixelHandler self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsDifferentBrokers + self.userNotificationService = userNotificationService } // MARK: - Public functions @@ -119,7 +122,7 @@ final class DataBrokerProtectionProcessor { operationQueue.addOperation(collection) } - operationQueue.addBarrierBlock { + operationQueue.addBarrierBlock { [weak self] in completion() } } @@ -145,6 +148,7 @@ final class DataBrokerProtectionProcessor { notificationCenter: notificationCenter, runner: operationRunnerProvider.getOperationRunner(), pixelHandler: pixelHandler, + userNotificationService: userNotificationService, showWebView: showWebView) collections.append(collection) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index b1468e0c19..595af9848f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -73,6 +73,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch private let notificationCenter: NotificationCenter private let emailService: EmailServiceProtocol private let captchaService: CaptchaServiceProtocol + private let userNotificationService: DataBrokerProtectionUserNotificationService /// Ensures that only one scheduler operation is executed at the same time. /// @@ -93,7 +94,8 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch config: DataBrokerProtectionSchedulerConfig(), operationRunnerProvider: runnerProvider, notificationCenter: notificationCenter, - pixelHandler: pixelHandler) + pixelHandler: pixelHandler, + userNotificationService: userNotificationService) }() public init(privacyConfigManager: PrivacyConfigurationManaging, @@ -101,7 +103,8 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch dataManager: DataBrokerProtectionDataManager, notificationCenter: NotificationCenter = NotificationCenter.default, pixelHandler: EventMapping, - redeemUseCase: DataBrokerProtectionRedeemUseCase + redeemUseCase: DataBrokerProtectionRedeemUseCase, + userNotificationService: DataBrokerProtectionUserNotificationService ) { activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) activity.repeats = true @@ -114,6 +117,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch self.contentScopeProperties = contentScopeProperties self.pixelHandler = pixelHandler self.notificationCenter = notificationCenter + self.userNotificationService = userNotificationService self.emailService = EmailService(redeemUseCase: redeemUseCase) self.captchaService = CaptchaService(redeemUseCase: redeemUseCase) @@ -163,9 +167,20 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public func scanAllBrokers(showWebView: Bool = false, completion: ((Error?) -> Void)? = nil) { stopScheduler() + userNotificationService.requestNotificationPermission() + os_log("Scanning all brokers...", log: .dataBrokerProtection) dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] in - self?.startScheduler(showWebView: showWebView) + guard let self = self else { return } + + self.startScheduler(showWebView: showWebView) + + self.userNotificationService.sendFirstScanCompletedNotification() + + if self.dataManager.hasMatches() { + self.userNotificationService.scheduleCheckInNotificationIfPossible() + } + completion?(nil) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionWebUIURLSettings.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionWebUIURLSettings.swift index e7f84b0657..c5fbced3f4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionWebUIURLSettings.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionWebUIURLSettings.swift @@ -88,7 +88,7 @@ private extension String { } } -extension UserDefaults { +private extension UserDefaults { enum Key: String { case customURLValue case urlType diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift new file mode 100644 index 0000000000..6b905a8fbe --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift @@ -0,0 +1,238 @@ +// +// DataBrokerProtectionUserNotificationService.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 UserNotifications +import Common +import AppKit + +public enum DataBrokerProtectionNotificationCommand: String { + case showDashboard = "databrokerprotection://show_dashboard" + + public var url: URL { + URL(string: self.rawValue)! + } +} + +public protocol DataBrokerProtectionUserNotificationService { + func requestNotificationPermission() + func sendFirstScanCompletedNotification() + func sendFirstRemovedNotificationIfPossible() + func sendAllInfoRemovedNotificationIfPossible() + func scheduleCheckInNotificationIfPossible() +} + +public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataBrokerProtectionUserNotificationService { + private let pixelHandler: EventMapping + private let userDefaults: UserDefaults + private let userNotificationCenter: UNUserNotificationCenter + private let areNotificationsEnabled = false + + public init(pixelHandler: EventMapping, + userDefaults: UserDefaults = .standard, + userNotificationCenter: UNUserNotificationCenter = .current()) { + self.pixelHandler = pixelHandler + self.userDefaults = userDefaults + self.userNotificationCenter = userNotificationCenter + + super.init() + + self.userNotificationCenter.delegate = self + } + + public func requestNotificationPermission() { + guard areNotificationsEnabled else { return } + + userNotificationCenter.requestAuthorization(options: [.alert]) { _, _ in } + } + + private func sendNotification(_ notification: UserNotification, afterDays days: Int? = nil) { + let notificationContent = UNMutableNotificationContent() + notificationContent.title = notification.title + notificationContent.body = notification.message + + if #available(macOS 12, *) { + notificationContent.interruptionLevel = .active + } + + let request: UNNotificationRequest + + if let days = days { + let calendar = Calendar.current + guard let date = calendar.date(byAdding: .day, value: days, to: Date()) else { + os_log("Notification scheduled for an invalid date", log: .dataBrokerProtection) + return + } + let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) + let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) + request = UNNotificationRequest(identifier: notification.identifier, content: notificationContent, trigger: trigger) + } else { + request = UNNotificationRequest(identifier: notification.identifier, content: notificationContent, trigger: nil) + } + + userNotificationCenter.add(request) { error in + if error == nil { + if days != nil { + os_log("Notification scheduled", log: .dataBrokerProtection) + } else { + os_log("Notification sent", log: .dataBrokerProtection) + } + } + } + } + + public func sendFirstScanCompletedNotification() { + guard areNotificationsEnabled else { return } + + sendNotification(.firstScanComplete) + pixelHandler.fire(.dataBrokerProtectionNotificationSentFirstScanComplete) + } + + public func sendFirstRemovedNotificationIfPossible() { + guard areNotificationsEnabled else { return } + + if userDefaults[.didSendFirstRemovedNotification] != true { + sendNotification(.firstProfileRemoved) + userDefaults[.didSendFirstRemovedNotification] = true + + pixelHandler.fire(.dataBrokerProtectionNotificationSentFirstRemoval) + } + } + + public func sendAllInfoRemovedNotificationIfPossible() { + guard areNotificationsEnabled else { return } + + if userDefaults[.didSendAllInfoRemovedNotification] != true { + sendNotification(.allInfoRemoved) + userDefaults[.didSendAllInfoRemovedNotification] = true + + pixelHandler.fire(.dataBrokerProtectionNotificationSentAllRecordsRemoved) + } + } + + public func scheduleCheckInNotificationIfPossible() { + guard areNotificationsEnabled else { return } + + if userDefaults[.didSendCheckedInNotification] != true { + sendNotification(.twoWeeksCheckIn, afterDays: 14) + userDefaults[.didSendCheckedInNotification] = true + + pixelHandler.fire(.dataBrokerProtectionNotificationScheduled2WeeksCheckIn) + } + } + +} + +extension DefaultDataBrokerProtectionUserNotificationService: UNUserNotificationCenterDelegate { + + public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + return .banner + } + + public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + guard let identifier = UNNotificationRequest.Identifier(rawValue: response.notification.request.identifier) else { return } + + let pixelMapper: [UNNotificationRequest.Identifier: DataBrokerProtectionPixels] = [.firstScanComplete: .dataBrokerProtectionNotificationSentFirstScanComplete, + .firstProfileRemoved: .dataBrokerProtectionNotificationOpenedFirstRemoval, + .allInfoRemoved: .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, + .twoWeeksCheckIn: .dataBrokerProtectionNotificationOpened2WeeksCheckIn] + + switch identifier { + case .firstScanComplete, .firstProfileRemoved, .allInfoRemoved, .twoWeeksCheckIn: + NSWorkspace.shared.open(DataBrokerProtectionNotificationCommand.showDashboard.url) + + if let pixel = pixelMapper[identifier] { + pixelHandler.fire(pixel) + } + } + } +} + +extension UNNotificationRequest { + + enum Identifier: String { + case firstScanComplete = "dbp.scan.complete" + case firstProfileRemoved = "dbp.first.removed" + case allInfoRemoved = "dbp.all.removed" + case twoWeeksCheckIn = "dbp.2-weeks-check-in" + } +} + +private enum UserNotification { + case firstScanComplete + case firstProfileRemoved + case allInfoRemoved + case twoWeeksCheckIn + + var title: String { + switch self { + case .firstScanComplete: + return "Scan complete!" + case .firstProfileRemoved: + return "Success! A record of your info was removed!" + case .allInfoRemoved: + return "All pending info removals complete!" + case .twoWeeksCheckIn: + return "We're making progress on your info removals" + } + } + + var message: String { + switch self { + case .firstScanComplete: + return "DuckDuckGo has started the process to remove records matching your personal info online. See what we found..." + case .firstProfileRemoved: + return "That’s one less creepy site storing and selling your personal info online. Check progress..." + case .allInfoRemoved: + return "See all the records matching your personal info that DuckDuckGo found and removed from the web..." + case .twoWeeksCheckIn: + return "See the records matching your personal info that DuckDuckGo found and removed from the web so far..." + } + } + + var identifier: String { + switch self { + case .firstScanComplete: + return UNNotificationRequest.Identifier.firstScanComplete.rawValue + case .firstProfileRemoved: + return UNNotificationRequest.Identifier.firstProfileRemoved.rawValue + case .allInfoRemoved: + return UNNotificationRequest.Identifier.allInfoRemoved.rawValue + case .twoWeeksCheckIn: + return UNNotificationRequest.Identifier.twoWeeksCheckIn.rawValue + } + } +} + +private extension UserDefaults { + enum Key: String { + case didSendFirstRemovedNotification + case didSendAllInfoRemovedNotification + case didSendCheckedInNotification + } + + subscript(key: Key) -> T? where T: Any { + get { + return value(forKey: key.rawValue) as? T + } + set { + set(newValue, forKey: key.rawValue) + } + } + +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index bef3c21452..13410d0c0b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,6 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -65,6 +66,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id for broker") @@ -85,6 +87,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertEqual(mockDatabase.eventsAdded.first?.type, .scanStarted) @@ -105,6 +108,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .noMatchFound })) @@ -128,6 +132,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasUpdateRemoveDateCalled) @@ -153,6 +158,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasUpdateRemoveDateCalled) @@ -176,6 +182,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasUpdateRemoveDateCalled) @@ -199,6 +206,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.wasSaveOptOutOperationCalled) @@ -221,6 +229,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutConfirmed })) @@ -245,6 +254,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutConfirmed })) @@ -270,6 +280,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Should throw!") @@ -298,6 +309,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -321,6 +333,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -344,6 +357,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Scan should fail when brokerProfileQueryData has no id profile query") @@ -367,6 +381,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasDatabaseCalled) @@ -390,6 +405,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertFalse(mockDatabase.wasDatabaseCalled) @@ -413,6 +429,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutStarted })) @@ -435,6 +452,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTAssertTrue(mockDatabase.eventsAdded.contains(where: { $0.type == .optOutRequested })) @@ -458,6 +476,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { database: mockDatabase, notificationCenter: .default, pixelHandler: MockDataBrokerProtectionPixelsHandler(), + userNotificationService: MockUserNotification(), shouldRunNextStep: { true } ) XCTFail("Should throw!") @@ -746,4 +765,13 @@ extension ExtractedProfile { ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: date) } } + +final class MockUserNotification: DataBrokerProtectionUserNotificationService { + func requestNotificationPermission() { } + func sendFirstScanCompletedNotification() { } + func sendFirstRemovedNotificationIfPossible() { } + func sendAllInfoRemovedNotificationIfPossible() { } + func scheduleCheckInNotificationIfPossible() { } +} + // swiftlint:enable type_body_length