diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 06e4ce3ba6..fe39db8d4d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -166,6 +166,12 @@ 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */; }; 3168506D2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; 3168506E2AF3AD1D009A2828 /* WaitlistViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */; }; + 316913232BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */; }; + 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */; }; + 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; }; + 316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; }; + 316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; }; + 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; }; 3171D6B82889849F0068632A /* CookieManagedNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */; }; 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B9288984D00068632A /* BadgeAnimationView.swift */; }; 3171D6DB2889B64D0068632A /* CookieManagedNotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */; }; @@ -186,6 +192,8 @@ 31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */; }; 31CF3432288B0B1B0087244B /* NavigationBarBadgeAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */; }; 31D5375C291D944100407A95 /* PasswordManagementBitwardenItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D5375B291D944100407A95 /* PasswordManagementBitwardenItemView.swift */; }; + 31DC2F222BD6DE6C001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */; }; + 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */; }; 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */; }; 31E163BD293A579E00963C10 /* PrivacyReferenceTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */; }; 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; @@ -1414,7 +1422,6 @@ 560C3FFD2BC9911000F589CE /* PermanentSurveyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */; }; 560C3FFF2BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; 560C40002BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; - 560C40012BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; 561D66662B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 561D66672B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; @@ -2806,6 +2813,9 @@ 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubePlayerNavigationHandler.swift; sourceTree = ""; }; 3168506C2AF3AD1C009A2828 /* WaitlistViewControllerPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitlistViewControllerPresenter.swift; sourceTree = ""; }; 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionDebugMenu.swift; sourceTree = ""; }; + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionPixelsHandler.swift; sourceTree = ""; }; + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifier.swift; sourceTree = ""; }; + 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionErrorViewController.swift; sourceTree = ""; }; 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationView.swift; sourceTree = ""; }; 3171D6B9288984D00068632A /* BadgeAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeAnimationView.swift; sourceTree = ""; }; 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationContainerView.swift; sourceTree = ""; }; @@ -2825,6 +2835,7 @@ 31C9ADE42AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistFeatureSetupHandler.swift; sourceTree = ""; }; 31CF3431288B0B1B0087244B /* NavigationBarBadgeAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarBadgeAnimator.swift; sourceTree = ""; }; 31D5375B291D944100407A95 /* PasswordManagementBitwardenItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordManagementBitwardenItemView.swift; sourceTree = ""; }; + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifierTests.swift; sourceTree = ""; }; 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSiteReportingReferenceTests.swift; sourceTree = ""; }; 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = ""; }; 31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; @@ -4439,6 +4450,14 @@ path = Subscription; sourceTree = ""; }; + 3169132B2BD2C7960051B46D /* ErrorView */ = { + isa = PBXGroup; + children = ( + 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */, + ); + path = ErrorView; + sourceTree = ""; + }; 3171D6DC2889B6700068632A /* CookieManaged */ = { isa = PBXGroup; children = ( @@ -4461,6 +4480,8 @@ 3192EC862A4DCF0E001E97A5 /* DBP */ = { isa = PBXGroup; children = ( + 3169132B2BD2C7960051B46D /* ErrorView */, + 316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */, 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, @@ -4473,6 +4494,7 @@ BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */, 4B37EE652B4CFC9500A89A61 /* RemoteMessaging */, + 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */, ); path = DBP; sourceTree = ""; @@ -4481,6 +4503,7 @@ isa = PBXGroup; children = ( 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */, + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, ); path = DBP; sourceTree = ""; @@ -9683,6 +9706,7 @@ B66260E129AC6EBD00E9E3EE /* HistoryTabExtension.swift in Sources */, 3706FB8E293F65D500E42796 /* FirefoxEncryptionKeyReader.swift in Sources */, 3706FB8F293F65D500E42796 /* BookmarkManagementSplitViewController.swift in Sources */, + 316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 3706FB90293F65D500E42796 /* CookieManagedNotificationContainerView.swift in Sources */, 3706FB91293F65D500E42796 /* FileManagerExtension.swift in Sources */, 3706FB92293F65D500E42796 /* PermissionModel.swift in Sources */, @@ -9717,6 +9741,7 @@ 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, 3706FBA3293F65D500E42796 /* FireproofingURLExtensions.swift in Sources */, + 3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DDD3EC12B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, @@ -10019,6 +10044,7 @@ 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, + 316913242BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, @@ -10304,6 +10330,7 @@ 1D9FDEBE2B9B5F0F0040B78C /* CookiePopupProtectionPreferencesTests.swift in Sources */, 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, + 31DC2F232BD6E028001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, 9F3344632BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, @@ -10658,6 +10685,7 @@ AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, 1D2DC00629016798008083A1 /* BWCredential.swift in Sources */, EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, + 316913232BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift in Sources */, 37AFCE8727DA334800471A10 /* PreferencesRootView.swift in Sources */, B684590825C9027900DC17B6 /* AppStateChangedPublisher.swift in Sources */, 4B92928F26670D1700AD2C21 /* BookmarkTableCellView.swift in Sources */, @@ -10796,6 +10824,7 @@ 4B379C2227BDBA29008A968E /* LocalAuthenticationService.swift in Sources */, 37CEFCA92A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */, 4BB99D0326FE191E001E4761 /* SafariBookmarksReader.swift in Sources */, + 316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */, 1DA6D0FD2A1FF9A100540406 /* HTTPCookie.swift in Sources */, AACF6FD626BC366D00CF09F9 /* SafariVersionReader.swift in Sources */, 4BE65485271FCD7B008D1D63 /* LoginFaviconView.swift in Sources */, @@ -11076,6 +11105,7 @@ 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */, AA6AD95B2704B6DB00159F8A /* FirePopoverViewController.swift in Sources */, 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */, @@ -11536,6 +11566,7 @@ 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */, 566B195D29CDB692007E38F4 /* MoreOptionsMenuTests.swift in Sources */, AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */, + 31DC2F222BD6DE6C001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift in Sources */, B69B50452726C5C200758A2B /* AtbParserTests.swift in Sources */, 1D8C2FED2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/DBP-Icon.pdf b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/DBP-Icon.pdf similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Icon.imageset/DBP-Icon.pdf rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Icon.imageset/DBP-Icon.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/Contents.json rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/DBP-Information-Remover.svg b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/DBP-Information-Remover.svg similarity index 100% rename from DuckDuckGo/Assets.xcassets/Images/DataBrokerProtectionWaitlist/DBP-Information-Remover.imageset/DBP-Information-Remover.svg rename to DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/DBP-Information-Remover.imageset/DBP-Information-Remover.svg diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json new file mode 100644 index 0000000000..9869fe984d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "dbp-error-info.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf new file mode 100644 index 0000000000..c9486753e5 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/DataBrokerProtection/dbp-error-info.imageset/dbp-error-info.pdf differ diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 9cb9493ebf..85eb68cad2 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1138,4 +1138,14 @@ struct UserText { // Key: "subscription.progress.view.completing.purchase" // Comment: "Progress view title when completing the purchase" static let completingPurchaseTitle = "Completing purchase..." + + // MARK: - DBP Error pages + + static let dbpErrorPageBadPathTitle = "Move DuckDuckGo App to Applications" + static let dbpErrorPageBadPathMessage = "To use Personal Information Removal, the DuckDuckGo app needs to be in the Applications folder on your Mac. You can move the app yourself and restart the browser, or we can do it for you." + static let dbpErrorPageBadPathCTA = "Move App for Me..." + + static let dbpErrorPageNoPermissionTitle = "Change System Setting" + static let dbpErrorPageNoPermissionMessage = "Open System Settings and allow DuckDuckGo Personal Information Removal to run in the background." + static let dbpErrorPageNoPermissionCTA = "Open System Settings..." } diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 3e5c8ce916..55d08d7d62 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -34,8 +34,15 @@ final class DBPHomeViewController: NSViewController { private var presentedWindowController: NSWindowController? private let dataBrokerProtectionManager: DataBrokerProtectionManager private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + private var currentChildViewController: NSViewController? + private var observer: NSObjectProtocol? - lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { + private let prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier + private lazy var errorViewController: DataBrokerProtectionErrorViewController = { + DataBrokerProtectionErrorViewController() + }() + + private lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { let privacyConfigurationManager = PrivacyFeatures.contentBlocking.privacyConfigurationManager let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, @@ -64,8 +71,9 @@ final class DBPHomeViewController: NSViewController { }) }() - init(dataBrokerProtectionManager: DataBrokerProtectionManager) { + init(dataBrokerProtectionManager: DataBrokerProtectionManager, prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier = DefaultDataBrokerPrerequisitesStatusVerifier()) { self.dataBrokerProtectionManager = dataBrokerProtectionManager + self.prerequisiteVerifier = prerequisiteVerifier super.init(nibName: nil, bundle: nil) } @@ -80,9 +88,8 @@ final class DBPHomeViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() - if !dataBrokerProtectionManager.shouldAskForInviteCode() { - attachDataBrokerContainerView() - } + setupUI() + setupObserver() do { if try dataBrokerProtectionManager.dataManager.fetchProfile() != nil { @@ -95,15 +102,10 @@ final class DBPHomeViewController: NSViewController { } } - private func attachDataBrokerContainerView() { - addChild(dataBrokerProtectionViewController) - view.addSubview(dataBrokerProtectionViewController.view) - } - override func viewDidAppear() { super.viewDidAppear() - if dataBrokerProtectionManager.shouldAskForInviteCode() { + if shouldAskForInviteCode() { presentInviteCodeFlow() } } @@ -111,6 +113,19 @@ final class DBPHomeViewController: NSViewController { override func viewDidLayout() { super.viewDidLayout() dataBrokerProtectionViewController.view.frame = view.bounds + errorViewController.view.frame = view.bounds + } + + private func setupUI() { + if !shouldAskForInviteCode() { + setupUIWithCurrentStatus() + } + } + + private func setupObserver() { + observer = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in + self?.setupUI() + } } private func presentInviteCodeFlow() { @@ -128,13 +143,54 @@ final class DBPHomeViewController: NSViewController { } parentWindowController.window?.beginSheet(newWindow) } + + private func setupUIWithCurrentStatus() { + setupUIWithStatus(prerequisiteVerifier.checkStatus()) + } + + private func setupUIWithStatus(_ status: DataBrokerPrerequisitesStatus) { + switch status { + case .invalidDirectory: + displayWrongDirectoryErrorUI() + pixelHandler.fire(.homeViewShowBadPathError) + case .invalidSystemPermission: + displayWrongPermissionsErrorUI() + pixelHandler.fire(.homeViewShowNoPermissionError) + case .valid: + displayDBPUI() + pixelHandler.fire(.homeViewShowWebUI) + } + } + + private func shouldAskForInviteCode() -> Bool { + prerequisiteVerifier.checkStatus() == .valid && dataBrokerProtectionManager.shouldAskForInviteCode() + } + + private func displayDBPUI() { + replaceChildController(dataBrokerProtectionViewController) + } + + private func replaceChildController(_ childViewController: NSViewController) { + if let child = currentChildViewController { + child.removeCompletely() + } + + addAndLayoutChild(childViewController) + self.currentChildViewController = childViewController + } + + deinit { + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } + } } extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDelegate { func dataBrokerProtectionInviteDialogsViewModelDidReedemSuccessfully(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { presentedWindowController?.window?.close() presentedWindowController = nil - attachDataBrokerContainerView() + setupUIWithCurrentStatus() } func dataBrokerProtectionInviteDialogsViewModelDidCancel(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { @@ -144,92 +200,54 @@ extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDeleg } } -public class DataBrokerProtectionPixelsHandler: EventMapping { - - // swiftlint:disable:next function_body_length - public init() { - super.init { event, _, _, _ in - switch event { - case .error(let error, _): - PixelKit.fire(DebugEvent(event, error: error)) - case .generalError(let error, _), - .secureVaultInitError(let error), - .secureVaultError(let error): - PixelKit.fire(DebugEvent(event, error: error)) - case .ipcServerStartSchedulerXPCError(error: let error), - .ipcServerStopSchedulerXPCError(error: let error), - .ipcServerScanAllBrokersXPCError(error: let error), - .ipcServerScanAllBrokersCompletedOnAgentWithError(error: let error), - .ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: let error), - .ipcServerOptOutAllBrokersCompletion(error: let error), - .ipcServerRunQueuedOperationsCompletion(error: let error): - PixelKit.fire(DebugEvent(event, error: error), frequency: .dailyAndCount, includeAppVersionParameter: true) - case .ipcServerStartSchedulerCalledByApp, - .ipcServerStartSchedulerReceivedByAgent, - .ipcServerStopSchedulerCalledByApp, - .ipcServerStopSchedulerReceivedByAgent, - .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, - .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, - .ipcServerScanAllBrokersCalledByApp, - .ipcServerScanAllBrokersReceivedByAgent, - .ipcServerScanAllBrokersCompletedOnAgentWithoutError, - .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, - .ipcServerScanAllBrokersInterruptedOnAgent, - .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: - PixelKit.fire(event, frequency: .dailyAndCount, includeAppVersionParameter: true) - case .parentChildMatches, - .optOutStart, - .optOutEmailGenerate, - .optOutCaptchaParse, - .optOutCaptchaSend, - .optOutCaptchaSolve, - .optOutSubmit, - .optOutEmailReceive, - .optOutEmailConfirm, - .optOutValidate, - .optOutFinish, - .optOutSubmitSuccess, - .optOutFillForm, - .optOutSuccess, - .optOutFailure, - .backgroundAgentStarted, - .backgroundAgentRunOperationsAndStartSchedulerIfPossible, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, - .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, - .ipcServerOptOutAllBrokers, - .ipcServerRunQueuedOperations, - .ipcServerRunAllOperations, - .scanSuccess, - .scanFailed, - .scanError, - .dataBrokerProtectionNotificationSentFirstScanComplete, - .dataBrokerProtectionNotificationOpenedFirstScanComplete, - .dataBrokerProtectionNotificationSentFirstRemoval, - .dataBrokerProtectionNotificationOpenedFirstRemoval, - .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, - .dataBrokerProtectionNotificationOpened2WeeksCheckIn, - .dataBrokerProtectionNotificationSentAllRecordsRemoved, - .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, - .dailyActiveUser, - .weeklyActiveUser, - .monthlyActiveUser, - .weeklyReportScanning, - .weeklyReportRemovals, - .scanningEventNewMatch, - .scanningEventReAppearance, - .webUILoadingFailed, - .webUILoadingStarted, - .webUILoadingSuccess, - .emptyAccessTokenDaily, - .generateEmailHTTPErrorDaily: - PixelKit.fire(event) - } +// MARK: - Error UI + +extension DBPHomeViewController { + private func displayWrongDirectoryErrorUI() { + let errorViewModel = DataBrokerProtectionErrorViewModel(title: UserText.dbpErrorPageBadPathTitle, + message: UserText.dbpErrorPageBadPathMessage, + ctaText: UserText.dbpErrorPageBadPathCTA, + ctaAction: { [weak self] in + self?.moveToApplicationFolder() + }) + + errorViewController.errorViewModel = errorViewModel + replaceChildController(errorViewController) + } + + private func displayWrongPermissionsErrorUI() { + let errorViewModel = DataBrokerProtectionErrorViewModel(title: UserText.dbpErrorPageNoPermissionTitle, + message: UserText.dbpErrorPageNoPermissionMessage, + ctaText: UserText.dbpErrorPageNoPermissionCTA, + ctaAction: { [weak self] in + self?.openLoginItemSettings() + }) + + errorViewController.errorViewModel = errorViewModel + replaceChildController(errorViewController) + } +} + +// MARK: - System configuration + +import ServiceManagement + +extension DBPHomeViewController { + func openLoginItemSettings() { + pixelHandler.fire(.homeViewCTAGrantPermissionClicked) + if #available(macOS 13.0, *) { + SMAppService.openSystemSettingsLoginItems() + } else { + let loginItemsURL = URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension")! + NSWorkspace.shared.open(loginItemsURL) } } - override init(mapping: @escaping EventMapping.Mapping) { - fatalError("Use init()") + func moveToApplicationFolder() { + pixelHandler.fire(.homeViewCTAMoveApplicationClicked) + Task { @MainActor in + await AppLauncher(appBundleURL: Bundle.main.bundleURL).launchApp(withCommand: .moveAppToApplications) + } } } diff --git a/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift new file mode 100644 index 0000000000..f5f5c4d091 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerPrerequisitesStatusVerifier.swift @@ -0,0 +1,50 @@ +// +// DataBrokerPrerequisitesStatusVerifier.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import DataBrokerProtection +import LoginItems + +enum DataBrokerPrerequisitesStatus { + case invalidDirectory + case invalidSystemPermission + case valid +} + +protocol DataBrokerPrerequisitesStatusVerifier: AnyObject { + func checkStatus() -> DataBrokerPrerequisitesStatus +} + +final class DefaultDataBrokerPrerequisitesStatusVerifier: DataBrokerPrerequisitesStatusVerifier { + private let statusChecker: DBPLoginItemStatusChecker + + init(statusChecker: DBPLoginItemStatusChecker = LoginItem.dbpBackgroundAgent) { + self.statusChecker = statusChecker + } + + func checkStatus() -> DataBrokerPrerequisitesStatus { + if !statusChecker.doesHaveNecessaryPermissions() { + return .invalidSystemPermission + } else if !statusChecker.isInCorrectDirectory() { + return .invalidDirectory + } else { + return .valid + } + } +} diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift new file mode 100644 index 0000000000..08c5b74f18 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -0,0 +1,118 @@ +// +// DataBrokerProtectionPixelsHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import DataBrokerProtection +import PixelKit +import Common + +public class DataBrokerProtectionPixelsHandler: EventMapping { + + // swiftlint:disable:next function_body_length + public init() { + super.init { event, _, _, _ in + switch event { + case .error(let error, _): + PixelKit.fire(DebugEvent(event, error: error)) + case .generalError(let error, _), + .secureVaultInitError(let error), + .secureVaultError(let error): + PixelKit.fire(DebugEvent(event, error: error)) + case .ipcServerStartSchedulerXPCError(error: let error), + .ipcServerStopSchedulerXPCError(error: let error), + .ipcServerScanAllBrokersXPCError(error: let error), + .ipcServerScanAllBrokersCompletedOnAgentWithError(error: let error), + .ipcServerScanAllBrokersCompletionCalledOnAppWithError(error: let error), + .ipcServerOptOutAllBrokersCompletion(error: let error), + .ipcServerRunQueuedOperationsCompletion(error: let error): + PixelKit.fire(DebugEvent(event, error: error), frequency: .dailyAndCount, includeAppVersionParameter: true) + case .ipcServerStartSchedulerCalledByApp, + .ipcServerStartSchedulerReceivedByAgent, + .ipcServerStopSchedulerCalledByApp, + .ipcServerStopSchedulerReceivedByAgent, + .ipcServerScanAllBrokersAttemptedToCallWithoutLoginItemPermissions, + .ipcServerScanAllBrokersAttemptedToCallInWrongDirectory, + .ipcServerScanAllBrokersCalledByApp, + .ipcServerScanAllBrokersReceivedByAgent, + .ipcServerScanAllBrokersCompletedOnAgentWithoutError, + .ipcServerScanAllBrokersCompletionCalledOnAppWithoutError, + .ipcServerScanAllBrokersInterruptedOnAgent, + .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption: + PixelKit.fire(event, frequency: .dailyAndCount, includeAppVersionParameter: true) + case .parentChildMatches, + .optOutStart, + .optOutEmailGenerate, + .optOutCaptchaParse, + .optOutCaptchaSend, + .optOutCaptchaSolve, + .optOutSubmit, + .optOutEmailReceive, + .optOutEmailConfirm, + .optOutValidate, + .optOutFinish, + .optOutSubmitSuccess, + .optOutFillForm, + .optOutSuccess, + .optOutFailure, + .backgroundAgentStarted, + .backgroundAgentRunOperationsAndStartSchedulerIfPossible, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, + .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, + .ipcServerOptOutAllBrokers, + .ipcServerRunQueuedOperations, + .ipcServerRunAllOperations, + .scanSuccess, + .scanFailed, + .scanError, + .dataBrokerProtectionNotificationSentFirstScanComplete, + .dataBrokerProtectionNotificationOpenedFirstScanComplete, + .dataBrokerProtectionNotificationSentFirstRemoval, + .dataBrokerProtectionNotificationOpenedFirstRemoval, + .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, + .dataBrokerProtectionNotificationOpened2WeeksCheckIn, + .dataBrokerProtectionNotificationSentAllRecordsRemoved, + .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, + .dailyActiveUser, + .weeklyActiveUser, + .monthlyActiveUser, + .weeklyReportScanning, + .weeklyReportRemovals, + .scanningEventNewMatch, + .scanningEventReAppearance, + .webUILoadingFailed, + .webUILoadingStarted, + .webUILoadingSuccess, + .emptyAccessTokenDaily, + .generateEmailHTTPErrorDaily: + PixelKit.fire(event) + + case .homeViewShowNoPermissionError, + .homeViewShowWebUI, + .homeViewShowBadPathError, + .homeViewCTAMoveApplicationClicked, + .homeViewCTAGrantPermissionClicked: + PixelKit.fire(event, frequency: .dailyAndCount) + } + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} diff --git a/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift b/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift new file mode 100644 index 0000000000..fa628b686e --- /dev/null +++ b/DuckDuckGo/DBP/ErrorView/DataBrokerProtectionErrorViewController.swift @@ -0,0 +1,92 @@ +// +// DataBrokerProtectionErrorViewController.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +final class DataBrokerProtectionErrorViewController: NSViewController { + private var errorSubview: NSView? + + var errorViewModel: DataBrokerProtectionErrorViewModel? { + didSet { + guard let errorViewModel = errorViewModel else { return } + + errorSubview?.removeFromSuperview() + + let errorView = DataBrokerProtectionErrorView(viewModel: errorViewModel) + errorSubview = NSHostingView(rootView: errorView) + + if let errorSubview = errorSubview { + view.addAndLayout(errorSubview) + } + } + } +} + +struct DataBrokerProtectionErrorView: View { + var viewModel: DataBrokerProtectionErrorViewModel + + var body: some View { + VStack(alignment: .center, spacing: 16) { + + HStack { + Image("DaxLockScreenLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) + + Text("Privacy Pro") + .font(.title) + .fontWeight(.light) + } + .padding(.bottom, 25) + + HStack { + Image("dbp-error-info") + .resizable() + .frame(width: 24, height: 24) + + Text(viewModel.title) + .font(.title) + .fontWeight(.light) + } + + Text(viewModel.message) + .font(.body) + .fontWeight(.light) + .multilineTextAlignment(.center) + .padding(.bottom, 10) + + Button(action: { + viewModel.ctaAction() + }) { + Text(viewModel.ctaText) + } + + Spacer() + }.padding() + .frame(maxWidth: 500) + } +} + +struct DataBrokerProtectionErrorViewModel { + let title: String + let message: String + let ctaText: String + let ctaAction: () -> Void +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 075f405dc5..d9de1179a7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -161,6 +161,13 @@ public enum DataBrokerProtectionPixels { // Backend service errors case generateEmailHTTPErrorDaily(statusCode: Int, environment: String, wasOnWaitlist: Bool) case emptyAccessTokenDaily(environment: String, wasOnWaitlist: Bool, callSite: BackendServiceCallSite) + + // Home View + case homeViewShowNoPermissionError + case homeViewShowWebUI + case homeViewShowBadPathError + case homeViewCTAMoveApplicationClicked + case homeViewCTAGrantPermissionClicked } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -265,6 +272,13 @@ extension DataBrokerProtectionPixels: PixelKitEvent { // Backend service errors case .generateEmailHTTPErrorDaily: return "m_mac_dbp_service_email-generate-http-error" case .emptyAccessTokenDaily: return "m_mac_dbp_service_empty-auth-token" + + // Home View + case .homeViewShowNoPermissionError: return "m_mac_dbp_home_view_show-no-permission-error" + case .homeViewShowWebUI: return "m_mac_dbp_home_view_show-web-ui" + case .homeViewShowBadPathError: return "m_mac_dbp_home_view_show-bad-path-error" + case .homeViewCTAMoveApplicationClicked: return "m_mac_dbp_home_view-cta-move-application-clicked" + case .homeViewCTAGrantPermissionClicked: return "m_mac_dbp_home_view-cta-grant-permission-clicked" } } @@ -357,6 +371,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .scanningEventNewMatch, .scanningEventReAppearance, + .homeViewShowNoPermissionError, + .homeViewShowWebUI, + .homeViewShowBadPathError, + .homeViewCTAMoveApplicationClicked, + .homeViewCTAGrantPermissionClicked, .secureVaultInitError, .secureVaultError: @@ -486,6 +505,14 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Bool { + return doesHavePermissionValue + } + func isInCorrectDirectory() -> Bool { + return isInCorrectDirectoryValue + } + + func reset() { + doesHavePermissionValue = true + isInCorrectDirectoryValue = true + } +}