From e4e0e15205c6d80dc13d1e9e0540732509a2e66b Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 21 Dec 2023 17:44:37 +0000 Subject: [PATCH 01/22] Remove redundant config --- Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig | 10 +++++----- .../App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig | 10 +++++----- Configuration/DeveloperID.xcconfig | 12 +++--------- 3 files changed, 13 insertions(+), 19 deletions(-) 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..1292dbf908 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 From 1648954afa91d1f3347b5706612f4cc5f03e0f69 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 28 Dec 2023 14:48:59 +0000 Subject: [PATCH 02/22] Restart agent --- DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index f462671ae7..61be787bd3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -84,6 +84,9 @@ struct DataBrokerProtectionAppEvents { private func restartBackgroundAgent(loginItemsManager: LoginItemsManager) { pixelHandler.fire(.resetLoginItem) - loginItemsManager.restartLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) + loginItemsManager.disableLoginItems([LoginItem.dbpBackgroundAgent]) + loginItemsManager.enableLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) + + // restartLoginItems doesn't work when we change the agent name } } From d7a9c7218ee3bf8405252f88a377d24b2e1a0715 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 28 Dec 2023 16:12:17 +0000 Subject: [PATCH 03/22] WIP: Send notification --- .../DataBrokerProtectionScheduler.swift | 5 ++ .../Utils/NotificationHelper.swift | 61 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index b1468e0c19..635a1dc687 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -163,9 +163,14 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public func scanAllBrokers(showWebView: Bool = false, completion: ((Error?) -> Void)? = nil) { stopScheduler() + NotificationHelper().requestNotificationPermission() + os_log("Scanning all brokers...", log: .dataBrokerProtection) dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] in self?.startScheduler(showWebView: showWebView) + + NotificationHelper().sendFirstScanCompletedNotification() + completion?(nil) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift new file mode 100644 index 0000000000..b5e1a86cd2 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift @@ -0,0 +1,61 @@ +// +// NotificationHelper.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 + +struct NotificationHelper { + struct NotificationIdentifier { + static let bundleID = Bundle.main.bundleIdentifier ?? "com.duckduckgo.dbp.agent" + static let scanComplete = "\(NotificationIdentifier.bundleID).scan.complete" + } + + func requestNotificationPermission() { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + // TODO: Send pixel with permission status? + if let error = error { + // Handle the error + print("Error requesting notification permission: \(error.localizedDescription)") + } else if granted { + // Permission granted + print("Notification permission granted") + } else { + // Permission denied + print("Notification permission denied") + } + } + } + + func sendFirstScanCompletedNotification() { + let notificationContent = UNMutableNotificationContent() + + notificationContent.title = "Scan complete!" + notificationContent.body = "DuckDuckGo has started the process to remove records matching your personal info online. See what we found..." + + let notificationIdentifier = NotificationIdentifier.scanComplete + + let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) + + UNUserNotificationCenter.current().add(request) { error in + if error == nil { + print("Notification sent") + } + } + } +} From 80ab57d6d2e7e5c0fdf6b4a74f01787268602348 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 28 Dec 2023 16:44:58 +0000 Subject: [PATCH 04/22] WIP: removed notification --- .../Utils/NotificationHelper.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift index b5e1a86cd2..be1ab8cfea 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift @@ -54,7 +54,24 @@ struct NotificationHelper { UNUserNotificationCenter.current().add(request) { error in if error == nil { - print("Notification sent") + print("Notification sent") + } + } + } + + func sendFirstRemovedNotification() { + let notificationContent = UNMutableNotificationContent() + + notificationContent.title = "Success! A record of your info was removed!" + notificationContent.body = "That’s one less creepy site storing and selling your personal info online. Check progress..." + + let notificationIdentifier = NotificationIdentifier.scanComplete + + let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) + + UNUserNotificationCenter.current().add(request) { error in + if error == nil { + print("Notification sent") } } } From 7ca617235cbafc6bf708d844171ff38f5acb1a49 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 29 Dec 2023 10:31:29 +0000 Subject: [PATCH 05/22] WIP: Notifications --- .../Utils/NotificationHelper.swift | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift index be1ab8cfea..2aa34ad4a2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift @@ -23,6 +23,9 @@ struct NotificationHelper { struct NotificationIdentifier { static let bundleID = Bundle.main.bundleIdentifier ?? "com.duckduckgo.dbp.agent" static let scanComplete = "\(NotificationIdentifier.bundleID).scan.complete" + static let firstRemoved = "\(NotificationIdentifier.bundleID).first.removed" + static let allRemoved = "\(NotificationIdentifier.bundleID).all.removed" + static let checkIn = "\(NotificationIdentifier.bundleID).check-in" } func requestNotificationPermission() { @@ -42,13 +45,13 @@ struct NotificationHelper { } } - func sendFirstScanCompletedNotification() { + private func sendNotification(title: String, message: String, identifier: String) { let notificationContent = UNMutableNotificationContent() - notificationContent.title = "Scan complete!" - notificationContent.body = "DuckDuckGo has started the process to remove records matching your personal info online. See what we found..." + notificationContent.title = title + notificationContent.body = message - let notificationIdentifier = NotificationIdentifier.scanComplete + let notificationIdentifier = identifier let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) @@ -58,21 +61,34 @@ struct NotificationHelper { } } } - + + func sendFirstScanCompletedNotification() { + sendNotification(title: "Scan complete!", + message: "DuckDuckGo has started the process to remove records matching your personal info online. See what we found...", + identifier: NotificationIdentifier.scanComplete) + } + func sendFirstRemovedNotification() { - let notificationContent = UNMutableNotificationContent() - - notificationContent.title = "Success! A record of your info was removed!" - notificationContent.body = "That’s one less creepy site storing and selling your personal info online. Check progress..." - - let notificationIdentifier = NotificationIdentifier.scanComplete - - let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) - - UNUserNotificationCenter.current().add(request) { error in - if error == nil { - print("Notification sent") - } - } + sendNotification(title: "Success! A record of your info was removed!", + message: "That’s one less creepy site storing and selling your personal info online. Check progress...", + identifier: NotificationIdentifier.firstRemoved) + } + + func sendAllInfoRemovedNotification() { + sendNotification(title: "All pending info removals complete!", + message: "See all the records matching your personal info that DuckDuckGo found and removed from the web...", + identifier: NotificationIdentifier.allRemoved) + } + + func sendCheckInNotification() { + sendNotification(title: "We're making progress on your info removals", + message: "See the records matching your personal info that DuckDuckGo found and removed from the web so far...", + identifier: NotificationIdentifier.checkIn) } } + + + + + + From 7e264c7b70d24e0018e33b7dbbb880c52d295fb3 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 29 Dec 2023 14:33:19 +0000 Subject: [PATCH 06/22] WIP: Send notifications --- ...ataBrokerProtectionBackgroundManager.swift | 15 +- .../DataBrokerOperationsCollection.swift | 3 + ...taBrokerProfileQueryOperationManager.swift | 11 +- .../DataBrokerProtectionProcessor.swift | 9 +- .../DataBrokerProtectionScheduler.swift | 14 +- ...DataBrokerProtectionWebUIURLSettings.swift | 2 +- ...kerProtectionUserNotificationService.swift | 164 ++++++++++++++++++ .../Utils/NotificationHelper.swift | 94 ---------- 8 files changed, 204 insertions(+), 108 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift 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/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index fd6b0cddcf..11c99548ed 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() } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 08820bdf04..9cf586a8d9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -67,6 +67,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 +77,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,6 +87,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, + userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } } @@ -96,6 +99,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { 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 +178,13 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } if !removedProfiles.isEmpty { + var shouldSendNotification = 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) - + shouldSendNotification = true try updateOperationDataDates( origin: .scan, brokerId: brokerId, @@ -201,6 +206,9 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } } } + if shouldSendNotification { + userNotificationService.sendFirstRemovedNotificationIfPossible() + } } else { try updateOperationDataDates( origin: .scan, @@ -232,6 +240,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/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index f9471b6f5c..abe6ad2a46 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -31,12 +31,14 @@ final class DataBrokerProtectionProcessor { private let notificationCenter: NotificationCenter private let operationQueue: OperationQueue private var pixelHandler: 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,8 @@ final class DataBrokerProtectionProcessor { operationQueue.addOperation(collection) } - operationQueue.addBarrierBlock { + operationQueue.addBarrierBlock { [weak self] in + self?.userNotificationService.sendAllInfoRemovedNotificationIfPossible() completion() } } @@ -145,6 +149,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 635a1dc687..91c5117ca6 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,14 +167,14 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public func scanAllBrokers(showWebView: Bool = false, completion: ((Error?) -> Void)? = nil) { stopScheduler() - NotificationHelper().requestNotificationPermission() + userNotificationService.requestNotificationPermission() os_log("Scanning all brokers...", log: .dataBrokerProtection) dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] in self?.startScheduler(showWebView: showWebView) - NotificationHelper().sendFirstScanCompletedNotification() - + self?.userNotificationService.sendFirstScanCompletedNotification() + 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/Utils/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift new file mode 100644 index 0000000000..3e00e9a3c8 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift @@ -0,0 +1,164 @@ +// +// 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 + +public protocol DataBrokerProtectionUserNotificationService { + func requestNotificationPermission() + func sendFirstScanCompletedNotification() + func sendFirstRemovedNotificationIfPossible() + func sendAllInfoRemovedNotificationIfPossible() + func sendCheckInNotificationIfPossible() +} + +public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProtectionUserNotificationService { + private let pixelHandler: EventMapping + private let userDefaults: UserDefaults + + public init(pixelHandler: EventMapping, userDefaults: UserDefaults = .standard) { + self.pixelHandler = pixelHandler + self.userDefaults = userDefaults + } + + public func requestNotificationPermission() { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + // TODO: Send pixel with permission status? + if let error = error { + // Handle the error + print("Error requesting notification permission: \(error.localizedDescription)") + } else if granted { + // Permission granted + print("Notification permission granted") + } else { + // Permission denied + print("Notification permission denied") + } + } + } + + private func sendNotification(_ notification: UserNotification) { + let notificationContent = UNMutableNotificationContent() + + notificationContent.title = notification.title + notificationContent.body = notification.message + + let notificationIdentifier = notification.identifier + + let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) + + UNUserNotificationCenter.current().add(request) { error in + if error == nil { + print("Notification sent") + } + } + } + + public func sendFirstScanCompletedNotification() { + sendNotification(.firstScanComplete) + } + + public func sendFirstRemovedNotificationIfPossible() { + // if userDefaults[.didSendFirstRemovedNotification] != true { + sendNotification(.firstProfileRemoved) + userDefaults[.didSendFirstRemovedNotification] = true + // } + } + + public func sendAllInfoRemovedNotificationIfPossible() { + // if userDefaults[.didSendAllInfoRemovedNotification] != true { + sendNotification(.allInfoRemoved) + userDefaults[.didSendAllInfoRemovedNotification] = true + // } + } + + public func sendCheckInNotificationIfPossible() { + // if userDefaults[.didSendCheckedInNotification] != true { + sendNotification(.checkIn) + userDefaults[.didSendCheckedInNotification] = true + // } + } +} + +private enum UserNotification { + case firstScanComplete + case firstProfileRemoved + case allInfoRemoved + case checkIn + + 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 .checkIn: + 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 .checkIn: + return "See the records matching your personal info that DuckDuckGo found and removed from the web so far..." + } + } + + var identifier: String { + let notificationPrefix = "data.broker.protection.user.notification" + + switch self { + case .firstScanComplete: + return "\(notificationPrefix).scan.complete" + case .firstProfileRemoved: + return "\(notificationPrefix).first.removed" + case .allInfoRemoved: + return "\(notificationPrefix).all.removed" + case .checkIn: + return "\(notificationPrefix).check-in" + } + } +} + +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/Sources/DataBrokerProtection/Utils/NotificationHelper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift deleted file mode 100644 index 2aa34ad4a2..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/NotificationHelper.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// NotificationHelper.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 - -struct NotificationHelper { - struct NotificationIdentifier { - static let bundleID = Bundle.main.bundleIdentifier ?? "com.duckduckgo.dbp.agent" - static let scanComplete = "\(NotificationIdentifier.bundleID).scan.complete" - static let firstRemoved = "\(NotificationIdentifier.bundleID).first.removed" - static let allRemoved = "\(NotificationIdentifier.bundleID).all.removed" - static let checkIn = "\(NotificationIdentifier.bundleID).check-in" - } - - func requestNotificationPermission() { - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in - // TODO: Send pixel with permission status? - if let error = error { - // Handle the error - print("Error requesting notification permission: \(error.localizedDescription)") - } else if granted { - // Permission granted - print("Notification permission granted") - } else { - // Permission denied - print("Notification permission denied") - } - } - } - - private func sendNotification(title: String, message: String, identifier: String) { - let notificationContent = UNMutableNotificationContent() - - notificationContent.title = title - notificationContent.body = message - - let notificationIdentifier = identifier - - let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) - - UNUserNotificationCenter.current().add(request) { error in - if error == nil { - print("Notification sent") - } - } - } - - func sendFirstScanCompletedNotification() { - sendNotification(title: "Scan complete!", - message: "DuckDuckGo has started the process to remove records matching your personal info online. See what we found...", - identifier: NotificationIdentifier.scanComplete) - } - - func sendFirstRemovedNotification() { - sendNotification(title: "Success! A record of your info was removed!", - message: "That’s one less creepy site storing and selling your personal info online. Check progress...", - identifier: NotificationIdentifier.firstRemoved) - } - - func sendAllInfoRemovedNotification() { - sendNotification(title: "All pending info removals complete!", - message: "See all the records matching your personal info that DuckDuckGo found and removed from the web...", - identifier: NotificationIdentifier.allRemoved) - } - - func sendCheckInNotification() { - sendNotification(title: "We're making progress on your info removals", - message: "See the records matching your personal info that DuckDuckGo found and removed from the web so far...", - identifier: NotificationIdentifier.checkIn) - } -} - - - - - - From dd697b61becd233c5a27da484a2506cef89d6813 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 29 Dec 2023 14:36:58 +0000 Subject: [PATCH 07/22] fix compilation --- .../Operations/DataBrokerOperationsCollection.swift | 1 + .../Operations/DataBrokerProfileQueryOperationManager.swift | 3 +++ 2 files changed, 4 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index 11c99548ed..7a664c2c60 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -148,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 9cf586a8d9..1844065308 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) } } From c5ed9579d51aa5fc5ba18708d8fcc3e224712ba4 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 29 Dec 2023 15:39:04 +0000 Subject: [PATCH 08/22] WIP: Send profile removed notification --- ...taBrokerProfileQueryOperationManager.swift | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 1844065308..6aa3f708c8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -181,13 +181,13 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } if !removedProfiles.isEmpty { - var shouldSendNotification = false + 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) - shouldSendNotification = true + shouldSendProfileRemovedNotification = true try updateOperationDataDates( origin: .scan, brokerId: brokerId, @@ -199,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 @@ -209,8 +208,9 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } } } - if shouldSendNotification { - userNotificationService.sendFirstRemovedNotificationIfPossible() + if shouldSendProfileRemovedNotification { + sendProfileRemovedNotificationIfNecessary(userNotificationService: userNotificationService, + database: database) } } else { try updateOperationDataDates( @@ -235,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, From 4174a289378a1e5c581be2bc6ceaf1966e843982 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 10:06:20 +0000 Subject: [PATCH 09/22] Update binary name --- Configuration/DeveloperID.xcconfig | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 14 +++++++------- .../InputFilesChecker/InputFilesChecker.swift | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 1292dbf908..ec99e58e5f 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -76,4 +76,4 @@ DBP_BACKGROUND_AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(DBP_BACKGROUND_AGENT_BUN 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 84a726a4ab..dab96e4b94 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/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) ] From b454790b9887832c9d9b31332a731d7018b12e45 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 13:14:42 +0000 Subject: [PATCH 10/22] Remove wrong notification call --- .../Scheduler/DataBrokerProtectionProcessor.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index abe6ad2a46..b2fd96f431 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -123,7 +123,6 @@ final class DataBrokerProtectionProcessor { } operationQueue.addBarrierBlock { [weak self] in - self?.userNotificationService.sendAllInfoRemovedNotificationIfPossible() completion() } } From 4a67aa19fe70edea9a90218396370c71c7181983 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 13:37:39 +0000 Subject: [PATCH 11/22] Only send scan notifications if there was a match --- .../Scheduler/DataBrokerProtectionScheduler.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 91c5117ca6..d45a04e1c2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -171,9 +171,13 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch 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?.userNotificationService.sendFirstScanCompletedNotification() + self.startScheduler(showWebView: showWebView) + + if self.dataManager.hasMatches() { + self.userNotificationService.sendFirstScanCompletedNotification() + } completion?(nil) } From ed0d010b34fd425835fb76713bf84c1cca33d1d0 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 14:17:27 +0000 Subject: [PATCH 12/22] Schedule notification --- .../DataBrokerProtectionScheduler.swift | 1 + ...kerProtectionUserNotificationService.swift | 32 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index d45a04e1c2..6e05f50bb0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -177,6 +177,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch if self.dataManager.hasMatches() { self.userNotificationService.sendFirstScanCompletedNotification() + self.userNotificationService.scheduleCheckInNotificationIfPossible() } completion?(nil) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift index 3e00e9a3c8..1347268245 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift @@ -25,7 +25,7 @@ public protocol DataBrokerProtectionUserNotificationService { func sendFirstScanCompletedNotification() func sendFirstRemovedNotificationIfPossible() func sendAllInfoRemovedNotificationIfPossible() - func sendCheckInNotificationIfPossible() + func scheduleCheckInNotificationIfPossible() } public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProtectionUserNotificationService { @@ -71,6 +71,31 @@ public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProt } } + private func sendScheduledNotification(_ notification: UserNotification, forAfterDays days: Int) { + let notificationContent = UNMutableNotificationContent() + notificationContent.title = notification.title + notificationContent.body = notification.message + + let notificationIdentifier = notification.identifier + + let calendar = Calendar.current + guard let date = calendar.date(byAdding: .day, value: days, to: Date()) else { + os_log("Notification scheduled for a invalid date", log: .dataBrokerProtection) + return + } + let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) + + let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) + + let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: trigger) + + UNUserNotificationCenter.current().add(request) { error in + if error == nil { + print("Notification scheduled") + } + } + } + public func sendFirstScanCompletedNotification() { sendNotification(.firstScanComplete) } @@ -89,12 +114,13 @@ public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProt // } } - public func sendCheckInNotificationIfPossible() { + public func scheduleCheckInNotificationIfPossible() { // if userDefaults[.didSendCheckedInNotification] != true { - sendNotification(.checkIn) + sendScheduledNotification(.checkIn, forAfterDays: 14) userDefaults[.didSendCheckedInNotification] = true // } } + } private enum UserNotification { From d8d434552f7f08392a7182bddf5fecd8eb466b13 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 17:07:15 +0000 Subject: [PATCH 13/22] WIP: Open app when clicking notification --- ...kerProtectionUserNotificationService.swift | 102 +++++++++++------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift index 1347268245..218f7cd5f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift @@ -28,18 +28,25 @@ public protocol DataBrokerProtectionUserNotificationService { func scheduleCheckInNotificationIfPossible() } -public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProtectionUserNotificationService { +public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataBrokerProtectionUserNotificationService { private let pixelHandler: EventMapping private let userDefaults: UserDefaults + private let userNotificationCenter: UNUserNotificationCenter - public init(pixelHandler: EventMapping, userDefaults: UserDefaults = .standard) { + 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() { - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + userNotificationCenter.requestAuthorization(options: [.alert]) { granted, error in // TODO: Send pixel with permission status? if let error = error { // Handle the error @@ -54,44 +61,37 @@ public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProt } } - private func sendNotification(_ notification: UserNotification) { + private func sendNotification(_ notification: UserNotification, afterDays days: Int? = nil) { let notificationContent = UNMutableNotificationContent() - notificationContent.title = notification.title notificationContent.body = notification.message - let notificationIdentifier = notification.identifier - - let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: nil) - - UNUserNotificationCenter.current().add(request) { error in - if error == nil { - print("Notification sent") - } + if #available(macOS 12, *) { + notificationContent.interruptionLevel = .active } - } - private func sendScheduledNotification(_ notification: UserNotification, forAfterDays days: Int) { - let notificationContent = UNMutableNotificationContent() - notificationContent.title = notification.title - notificationContent.body = notification.message + let request: UNNotificationRequest - let notificationIdentifier = notification.identifier - - let calendar = Calendar.current - guard let date = calendar.date(byAdding: .day, value: days, to: Date()) else { - os_log("Notification scheduled for a invalid date", log: .dataBrokerProtection) - return + 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) } - let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) - - let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) - let request = UNNotificationRequest(identifier: notificationIdentifier, content: notificationContent, trigger: trigger) - - UNUserNotificationCenter.current().add(request) { error in + userNotificationCenter.add(request) { error in if error == nil { - print("Notification scheduled") + if days != nil { + os_log("Notification scheduled", log: .dataBrokerProtection) + } else { + os_log("Notification sent", log: .dataBrokerProtection) + } } } } @@ -116,13 +116,39 @@ public struct DefaultDataBrokerProtectionUserNotificationService: DataBrokerProt public func scheduleCheckInNotificationIfPossible() { // if userDefaults[.didSendCheckedInNotification] != true { - sendScheduledNotification(.checkIn, forAfterDays: 14) + sendNotification(.checkIn, afterDays: 14) userDefaults[.didSendCheckedInNotification] = true // } } } +extension DefaultDataBrokerProtectionUserNotificationService: UNUserNotificationCenterDelegate { + + public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + return .banner + } + + public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + switch UNNotificationRequest.Identifier(rawValue: response.notification.request.identifier) { + case .firstScanComplete, .firstProfileRemoved, .allInfoRemoved, .checkIn: + print("Open app") + case .none: + print("Do nothing") + } + } +} + +extension UNNotificationRequest { + + enum Identifier: String { + case firstScanComplete = "dbp.scan.complete" + case firstProfileRemoved = "dbp.first.removed" + case allInfoRemoved = "dbp.all.removed" + case checkIn = "dbp.check-in" + } +} + private enum UserNotification { case firstScanComplete case firstProfileRemoved @@ -156,17 +182,15 @@ private enum UserNotification { } var identifier: String { - let notificationPrefix = "data.broker.protection.user.notification" - switch self { case .firstScanComplete: - return "\(notificationPrefix).scan.complete" + return UNNotificationRequest.Identifier.firstScanComplete.rawValue case .firstProfileRemoved: - return "\(notificationPrefix).first.removed" + return UNNotificationRequest.Identifier.firstProfileRemoved.rawValue case .allInfoRemoved: - return "\(notificationPrefix).all.removed" + return UNNotificationRequest.Identifier.allInfoRemoved.rawValue case .checkIn: - return "\(notificationPrefix).check-in" + return UNNotificationRequest.Identifier.checkIn.rawValue } } } From 0c709827435b184b3007cc3443710c6b708b616b Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 17:54:40 +0000 Subject: [PATCH 14/22] WIP: Handle dbp scheme --- DuckDuckGo/Application/URLEventHandler.swift | 41 +++++++++++++++++-- DuckDuckGo/Info.plist | 10 +++++ ...kerProtectionUserNotificationService.swift | 5 ++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 5376f28302..7130494277 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,25 @@ final class URLEventHandler { #endif +#if DBP + /// Handles DBP URLs + /// + private static func handleDataBrokerProtectionURL(_ url: URL) { + WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + } +#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/Info.plist b/DuckDuckGo/Info.plist index 3d1075d6ff..8e6122e6ba 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -64,6 +64,16 @@ networkprotection + + CFBundleTypeRole + Viewer + CFBundleURLName + DataBroker Protection URLs + CFBundleURLSchemes + + databrokerprotection + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift index 218f7cd5f6..d4cecd7a81 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift @@ -19,6 +19,7 @@ import Foundation import UserNotifications import Common +import AppKit public protocol DataBrokerProtectionUserNotificationService { func requestNotificationPermission() @@ -132,7 +133,9 @@ extension DefaultDataBrokerProtectionUserNotificationService: UNUserNotification public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { switch UNNotificationRequest.Identifier(rawValue: response.notification.request.identifier) { case .firstScanComplete, .firstProfileRemoved, .allInfoRemoved, .checkIn: - print("Open app") + if let url = URL(string: "databrokerprotection://opendashboard") { + NSWorkspace.shared.open(url) + } case .none: print("Do nothing") } From 55e075967eb487af9c26ab4ff44bdfc35170c302 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 4 Jan 2024 18:19:08 +0000 Subject: [PATCH 15/22] Handle tapping on URL --- DuckDuckGo/Application/URLEventHandler.swift | 8 +++++- ...kerProtectionUserNotificationService.swift | 28 ++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) rename LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/{Utils => UserNotifications}/DataBrokerProtectionUserNotificationService.swift (91%) diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 7130494277..eeb5d9f6c6 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -150,8 +150,14 @@ final class URLEventHandler { /// Handles DBP URLs /// private static func handleDataBrokerProtectionURL(_ url: URL) { - WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + switch url { + case DataBrokerProtectionNotificationCommand.showDashboard.url: + WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + default: + return + } } + #endif } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift similarity index 91% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift index d4cecd7a81..c84f3f81b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift @@ -21,6 +21,14 @@ 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() @@ -47,19 +55,7 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB } public func requestNotificationPermission() { - userNotificationCenter.requestAuthorization(options: [.alert]) { granted, error in - // TODO: Send pixel with permission status? - if let error = error { - // Handle the error - print("Error requesting notification permission: \(error.localizedDescription)") - } else if granted { - // Permission granted - print("Notification permission granted") - } else { - // Permission denied - print("Notification permission denied") - } - } + userNotificationCenter.requestAuthorization(options: [.alert]) { _, _ in } } private func sendNotification(_ notification: UserNotification, afterDays days: Int? = nil) { @@ -133,11 +129,9 @@ extension DefaultDataBrokerProtectionUserNotificationService: UNUserNotification public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { switch UNNotificationRequest.Identifier(rawValue: response.notification.request.identifier) { case .firstScanComplete, .firstProfileRemoved, .allInfoRemoved, .checkIn: - if let url = URL(string: "databrokerprotection://opendashboard") { - NSWorkspace.shared.open(url) - } + NSWorkspace.shared.open(DataBrokerProtectionNotificationCommand.showDashboard.url) case .none: - print("Do nothing") + return } } } From 1148e594dd543b436c522c61c089d5cf1137c0a2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 5 Jan 2024 08:37:06 +0000 Subject: [PATCH 16/22] Remove first scan condition --- .../Scheduler/DataBrokerProtectionScheduler.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 6e05f50bb0..595af9848f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -175,8 +175,9 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch self.startScheduler(showWebView: showWebView) + self.userNotificationService.sendFirstScanCompletedNotification() + if self.dataManager.hasMatches() { - self.userNotificationService.sendFirstScanCompletedNotification() self.userNotificationService.scheduleCheckInNotificationIfPossible() } From 3ff5552f6117b039ad7bff5580a92bb6b8e4d1f6 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 5 Jan 2024 11:14:31 +0000 Subject: [PATCH 17/22] WIP: Add DBP pixels --- DuckDuckGo/Statistics/PixelEvent.swift | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 9a4eec300a..c4eb731b9b 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -205,6 +205,16 @@ extension Pixel { // DataBrokerProtection Other case dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn + // DataBrokerProtection User Notifications + case dataBrokerProtectionNotificationSentFirstScanComplete + case dataBrokerProtectionNotificationOpenedFirstScanComplete + case dataBrokerProtectionNotificationSentFirstRemoval + case dataBrokerProtectionNotificationOpenedFirstRemoval + case dataBrokerProtectionNotificationScheduled2WeeksCheckIn + case dataBrokerProtectionNotificationOpened2WeeksCheckIn + case dataBrokerProtectionNotificationSentAllRecordsRemoved + case dataBrokerProtectionNotificationOpenedAllRecordsRemoved + // 28-day Home Button case homeButtonHidden case homeButtonLeft @@ -557,6 +567,22 @@ extension Pixel.Event { return "m_mac_dbp_ev_terms_accepted" case .dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn: return "m_mac_dbp_error_when_fetching_subscription_auth_token_after_sign_in" + 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" // 28-day Home Button case .homeButtonHidden: From a5078ef913119218d6e220350a510cb5eece561e Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 5 Jan 2024 11:27:47 +0000 Subject: [PATCH 18/22] WIP: Send pixels --- DuckDuckGo/DBP/DBPHomeViewController.swift | 11 ++++- DuckDuckGo/Statistics/PixelEvent.swift | 26 ---------- .../Pixels/DataBrokerProtectionPixels.swift | 49 ++++++++++++++++++- 3 files changed, 57 insertions(+), 29 deletions(-) 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: EventMapping Date: Fri, 5 Jan 2024 11:35:39 +0000 Subject: [PATCH 19/22] Fire notification pixels --- ...kerProtectionUserNotificationService.swift | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift index c84f3f81b3..f22ae01bd4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift @@ -95,27 +95,34 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB public func sendFirstScanCompletedNotification() { sendNotification(.firstScanComplete) + pixelHandler.fire(.dataBrokerProtectionNotificationSentFirstScanComplete) } public func sendFirstRemovedNotificationIfPossible() { - // if userDefaults[.didSendFirstRemovedNotification] != true { + if userDefaults[.didSendFirstRemovedNotification] != true { sendNotification(.firstProfileRemoved) userDefaults[.didSendFirstRemovedNotification] = true - // } + + pixelHandler.fire(.dataBrokerProtectionNotificationSentFirstRemoval) + } } public func sendAllInfoRemovedNotificationIfPossible() { - // if userDefaults[.didSendAllInfoRemovedNotification] != true { + if userDefaults[.didSendAllInfoRemovedNotification] != true { sendNotification(.allInfoRemoved) userDefaults[.didSendAllInfoRemovedNotification] = true - // } + + pixelHandler.fire(.dataBrokerProtectionNotificationSentAllRecordsRemoved) + } } public func scheduleCheckInNotificationIfPossible() { - // if userDefaults[.didSendCheckedInNotification] != true { - sendNotification(.checkIn, afterDays: 14) + if userDefaults[.didSendCheckedInNotification] != true { + sendNotification(.twoWeeksCheckIn, afterDays: 14) userDefaults[.didSendCheckedInNotification] = true - // } + + pixelHandler.fire(.dataBrokerProtectionNotificationScheduled2WeeksCheckIn) + } } } @@ -127,11 +134,20 @@ extension DefaultDataBrokerProtectionUserNotificationService: UNUserNotification } public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - switch UNNotificationRequest.Identifier(rawValue: response.notification.request.identifier) { - case .firstScanComplete, .firstProfileRemoved, .allInfoRemoved, .checkIn: + 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) - case .none: - return + + if let pixel = pixelMapper[identifier] { + pixelHandler.fire(pixel) + } } } } @@ -142,7 +158,7 @@ extension UNNotificationRequest { case firstScanComplete = "dbp.scan.complete" case firstProfileRemoved = "dbp.first.removed" case allInfoRemoved = "dbp.all.removed" - case checkIn = "dbp.check-in" + case twoWeeksCheckIn = "dbp.2-weeks-check-in" } } @@ -150,7 +166,7 @@ private enum UserNotification { case firstScanComplete case firstProfileRemoved case allInfoRemoved - case checkIn + case twoWeeksCheckIn var title: String { switch self { @@ -160,7 +176,7 @@ private enum UserNotification { return "Success! A record of your info was removed!" case .allInfoRemoved: return "All pending info removals complete!" - case .checkIn: + case .twoWeeksCheckIn: return "We're making progress on your info removals" } } @@ -173,7 +189,7 @@ private enum UserNotification { 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 .checkIn: + case .twoWeeksCheckIn: return "See the records matching your personal info that DuckDuckGo found and removed from the web so far..." } } @@ -186,8 +202,8 @@ private enum UserNotification { return UNNotificationRequest.Identifier.firstProfileRemoved.rawValue case .allInfoRemoved: return UNNotificationRequest.Identifier.allInfoRemoved.rawValue - case .checkIn: - return UNNotificationRequest.Identifier.checkIn.rawValue + case .twoWeeksCheckIn: + return UNNotificationRequest.Identifier.twoWeeksCheckIn.rawValue } } } From 168e68531eed80f773aeafeb303c83c7f739abd6 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 5 Jan 2024 15:03:39 +0000 Subject: [PATCH 20/22] fix tests --- ...taBrokerProfileQueryOperationManager.swift | 1 + ...kerProfileQueryOperationManagerTests.swift | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 6aa3f708c8..e03b028738 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -96,6 +96,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } // swiftlint:disable:next function_body_length + // swiftlint:disable:next cyclomatic_complexity internal func runScanOperation(on runner: WebOperationRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, 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 From 07b42d1c30d7245dcd99034f9ee554a2cd47d3d2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 5 Jan 2024 15:13:12 +0000 Subject: [PATCH 21/22] linter --- .../Operations/DataBrokerProfileQueryOperationManager.swift | 3 +-- .../Pixels/DataBrokerProtectionPixels.swift | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index e03b028738..496a15c81d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -95,8 +95,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } } - // swiftlint:disable:next function_body_length - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length internal func runScanOperation(on runner: WebOperationRunner, brokerProfileQueryData: BrokerProfileQueryData, database: DataBrokerProtectionRepository, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 6a9731f9d5..0b1f6eedf6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -417,6 +417,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { public class DataBrokerProtectionPixelsHandler: EventMapping { + // swiftlint:disable:next function_body_length public init() { super.init { event, _, _, _ in switch event { From 56f27d3f03ed1336404709fa525ec0cf743db02d Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Mon, 8 Jan 2024 10:08:18 +0000 Subject: [PATCH 22/22] Add flag for notifications --- .../DataBrokerProtectionUserNotificationService.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift index f22ae01bd4..6b905a8fbe 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift @@ -41,6 +41,7 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB private let pixelHandler: EventMapping private let userDefaults: UserDefaults private let userNotificationCenter: UNUserNotificationCenter + private let areNotificationsEnabled = false public init(pixelHandler: EventMapping, userDefaults: UserDefaults = .standard, @@ -55,6 +56,8 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB } public func requestNotificationPermission() { + guard areNotificationsEnabled else { return } + userNotificationCenter.requestAuthorization(options: [.alert]) { _, _ in } } @@ -94,11 +97,15 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB } 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 @@ -108,6 +115,8 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB } public func sendAllInfoRemovedNotificationIfPossible() { + guard areNotificationsEnabled else { return } + if userDefaults[.didSendAllInfoRemovedNotification] != true { sendNotification(.allInfoRemoved) userDefaults[.didSendAllInfoRemovedNotification] = true @@ -117,6 +126,8 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB } public func scheduleCheckInNotificationIfPossible() { + guard areNotificationsEnabled else { return } + if userDefaults[.didSendCheckedInNotification] != true { sendNotification(.twoWeeksCheckIn, afterDays: 14) userDefaults[.didSendCheckedInNotification] = true