From 75a5c9a2034e92b01e4ecadf083fe0cd506bafac Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Wed, 12 Jun 2024 17:33:45 +0200 Subject: [PATCH] Check DBP Prerequisites in App and Disable Login Item If Necessary (#2850) Task/Issue URL: https://app.asana.com/0/1206488453854252/1207501562611619/f **Description**: This PR introduces changes to check DBP prerequisites (user is authenticated and has valid entitlements) from the App when the app becomes active. When prerequisites are not satisfied, the app disables the login item. This is to prevent a scenario where the background agent checks prerequisites, exits due to failed prerequisites, and then is automatically re-started by the login item. --- DuckDuckGo.xcodeproj/project.pbxproj | 60 +++-- DuckDuckGo/Application/AppDelegate.swift | 19 +- .../Surveys/SurveyRemoteMessaging.swift | 1 - .../DBP/DataBrokerProtectionAppEvents.swift | 63 ++--- .../DBP/DataBrokerProtectionDebugMenu.swift | 37 --- .../DataBrokerProtectionFeatureDisabler.swift | 2 +- ...taBrokerProtectionFeatureGatekeeper.swift} | 104 +++++--- .../DataBrokerProtectionPixelsHandler.swift | 5 +- .../Model/HomePageContinueSetUpModel.swift | 40 +--- .../MainWindow/MainViewController.swift | 1 - DuckDuckGo/Menus/MainMenu.swift | 2 +- .../NavigationBar/View/MoreOptionsMenu.swift | 18 +- .../View/NavigationBarViewController.swift | 4 +- .../NetworkProtectionAppEvents.swift | 12 +- .../NetworkProtectionNavBarButtonModel.swift | 6 +- ...NetworkProtectionIPCTunnelController.swift | 8 +- .../Model/PreferencesSidebarModel.swift | 14 +- .../Model/VPNPreferencesModel.swift | 2 +- .../View/PreferencesViewController.swift | 2 +- ...ility.swift => VPNFeatureGatekeeper.swift} | 6 +- .../WaitlistThankYouPromptPresenter.swift | 2 +- DuckDuckGo/Waitlist/Waitlist.swift | 36 --- .../Pixels/DataBrokerProtectionPixels.swift | 18 +- .../DataBrokerProtectionVisibilityTests.swift | 149 ------------ .../DBP/Mocks/DataBrokerProtectionMocks.swift | 82 +++++++ ...okerPrerequisitesStatusVerifierTests.swift | 0 ...okerProtectionFeatureGatekeeperTests.swift | 223 ++++++++++++++++++ UnitTests/Menus/MoreOptionsMenuTests.swift | 8 +- 28 files changed, 519 insertions(+), 405 deletions(-) rename DuckDuckGo/DBP/{DataBrokerProtectionFeatureVisibility.swift => DataBrokerProtectionFeatureGatekeeper.swift} (72%) rename DuckDuckGo/Waitlist/{NetworkProtectionFeatureVisibility.swift => VPNFeatureGatekeeper.swift} (96%) delete mode 100644 UnitTests/DBP/DataBrokerProtectionVisibilityTests.swift create mode 100644 UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift rename UnitTests/DBP/{ => Tests}/DataBrokerPrerequisitesStatusVerifierTests.swift (100%) create mode 100644 UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ba2c846099..61cbca3faf 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -161,7 +161,7 @@ 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */; }; 311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B262628E73E0A00FD181A /* TabShadowConfig.swift */; }; - 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */; }; + 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */; }; 31267C6A2B640C4B00FEF811 /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; 31267C6B2B640C5200FEF811 /* DataBrokerProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */; }; 3129788A2B64131200B67619 /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 312978892B64131200B67619 /* DataBrokerProtection */; }; @@ -170,7 +170,7 @@ 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; }; 3158B14D2B0BF74D00AF130C /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; 3158B1532B0BF75700AF130C /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; }; - 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */; }; + 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */; }; 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */; }; 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */; }; 315A023F2B6421AE00BFA577 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 315A023E2B6421AE00BFA577 /* Networking */; }; @@ -192,8 +192,8 @@ 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317295D12AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift */; }; 3184AC6D288F29D800C35E4B /* BadgeNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6C288F29D800C35E4B /* BadgeNotificationAnimationModel.swift */; }; 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6E288F2A1100C35E4B /* CookieNotificationAnimationModel.swift */; }; - 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; - 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */; }; + 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; + 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 31A3A4E22B0C115F0021063C /* DataBrokerProtection */; }; 31A83FB52BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; 31A83FB62BE28D7D00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; @@ -1199,8 +1199,8 @@ 4B677442255DBEEA00025BD8 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.swift */; }; 4B6785472AA8DE68008A5004 /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */; }; 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */; }; - 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; - 4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; + 4B67854A2AA8DE75008A5004 /* VPNFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */; }; + 4B67854B2AA8DE76008A5004 /* VPNFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */; }; 4B6B64842BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; 4B6B64852BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B64832BA930420009FF9F /* WaitlistThankYouPromptPresenter.swift */; }; 4B70C00127B0793D000386ED /* DuckDuckGo-ExampleCrash.ips in Resources */ = {isa = PBXBuildFile; fileRef = 4B70BFFF27B0793D000386ED /* DuckDuckGo-ExampleCrash.ips */; }; @@ -2504,6 +2504,8 @@ C17CA7B32B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */; }; C1B1CBE12BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */; }; + C1D8BE452C1739E70057E426 /* DataBrokerProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */; }; + C1D8BE462C1739EC0057E426 /* DataBrokerProtectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */; }; C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1DAF3B62B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */; }; C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; @@ -2951,12 +2953,12 @@ 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPHomeViewController.swift; sourceTree = ""; }; 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureDisabler.swift; sourceTree = ""; }; 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAppEvents.swift; sourceTree = ""; }; - 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionVisibilityTests.swift; sourceTree = ""; }; + 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeperTests.swift; sourceTree = ""; }; 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+DBP.swift"; sourceTree = ""; }; 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemPixels.swift; sourceTree = ""; }; 31B4AF522901A4F20013585E /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 31C3CE0128EDC1E70002C24A /* CustomRoundedCornersShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRoundedCornersShape.swift; sourceTree = ""; }; - 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureVisibility.swift; sourceTree = ""; }; + 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeper.swift; sourceTree = ""; }; 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 = ""; }; @@ -3425,8 +3427,8 @@ 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNControllerXPCClient+ConvenienceInitializers.swift"; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; + 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNFeatureGatekeeper.swift; sourceTree = ""; }; 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIPCResources.swift; sourceTree = ""; }; - 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; @@ -4093,6 +4095,7 @@ C17CA7AC2B9B52E6008EC3C1 /* NavigationBarPopoversTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopoversTests.swift; sourceTree = ""; }; C17CA7B12B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillPopoverPresenter.swift; sourceTree = ""; }; C1B1CBE02BE1915100B6049C /* DataImportShortcutsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportShortcutsViewModel.swift; sourceTree = ""; }; + C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionMocks.swift; sourceTree = ""; }; C1DAF3B42B9A44860059244F /* AutofillPopoverPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPopoverPresenter.swift; sourceTree = ""; }; C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionPresenter.swift; sourceTree = ""; }; C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionExecutor.swift; sourceTree = ""; }; @@ -4643,7 +4646,7 @@ 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */, BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, - 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, + 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */, F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */, 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */, 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */, @@ -4659,8 +4662,8 @@ 31A2FD152BAB419400D0E741 /* DBP */ = { isa = PBXGroup; children = ( - 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift */, - 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, + C1D8BE432C1739BF0057E426 /* Tests */, + C1D8BE422C1739BA0057E426 /* Mocks */, ); path = DBP; sourceTree = ""; @@ -5512,7 +5515,7 @@ 4B9DB0062A983B23000927DB /* Waitlist */ = { isa = PBXGroup; children = ( - 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */, + 7BD8679A2A9E9E000063B9F7 /* VPNFeatureGatekeeper.swift */, 4B6785432AA8DE1F008A5004 /* VPNUninstaller.swift */, 7BFF35722C10D75000F89673 /* IPCServiceLauncher.swift */, 7B7FCD0E2BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift */, @@ -8202,6 +8205,23 @@ path = Mocks; sourceTree = ""; }; + C1D8BE422C1739BA0057E426 /* Mocks */ = { + isa = PBXGroup; + children = ( + C1D8BE442C1739E70057E426 /* DataBrokerProtectionMocks.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + C1D8BE432C1739BF0057E426 /* Tests */ = { + isa = PBXGroup; + children = ( + 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */, + 31DC2F202BD6DE65001354EF /* DataBrokerPrerequisitesStatusVerifierTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; C1E961E62B879E2A001760E1 /* Mocks */ = { isa = PBXGroup; children = ( @@ -9855,7 +9875,7 @@ 3706FB4C293F65D500E42796 /* SharingMenu.swift in Sources */, 3706FB4D293F65D500E42796 /* GrammarFeaturesManager.swift in Sources */, 3706FB50293F65D500E42796 /* SafariFaviconsReader.swift in Sources */, - 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureVisibility.swift in Sources */, + 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */, 3706FB51293F65D500E42796 /* NSScreenExtension.swift in Sources */, EEC4A66A2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, 3706FB52293F65D500E42796 /* NSBezierPathExtension.swift in Sources */, @@ -9907,7 +9927,7 @@ 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, B69A14F32B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, - 4B67854B2AA8DE76008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, + 4B67854B2AA8DE76008A5004 /* VPNFeatureGatekeeper.swift in Sources */, C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, @@ -10513,6 +10533,7 @@ 3706FE33293F661700E42796 /* FireTests.swift in Sources */, B60C6F8229B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */, + C1D8BE462C1739EC0057E426 /* DataBrokerProtectionMocks.swift in Sources */, 317295D32AF058D3002C3206 /* MockWaitlistTermsAndConditionsActionHandler.swift in Sources */, 9F0FFFB52BCCAE37007C87DD /* BookmarkAllTabsDialogCoordinatorViewModelTests.swift in Sources */, 3706FE34293F661700E42796 /* PermissionStoreTests.swift in Sources */, @@ -10540,7 +10561,7 @@ 3706FE44293F661700E42796 /* GeolocationServiceTests.swift in Sources */, 1DA6D1032A1FFA3B00540406 /* HTTPCookieTests.swift in Sources */, 3706FE45293F661700E42796 /* ProgressEstimationTests.swift in Sources */, - 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */, + 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */, B6619F072B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 3706FE46293F661700E42796 /* EncryptedValueTransformerTests.swift in Sources */, 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, @@ -11272,7 +11293,7 @@ 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, - 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, + 4B67854A2AA8DE75008A5004 /* VPNFeatureGatekeeper.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, 85D438B6256E7C9E00F3BAF8 /* ContextMenuUserScript.swift in Sources */, B693955526F04BEC0015B914 /* NSSavePanelExtension.swift in Sources */, @@ -11349,7 +11370,7 @@ 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, B6BCC53B2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, - 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, + 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */, 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, B6B5F5842B03580A008DB58A /* RequestFilePermissionView.swift in Sources */, 4B1E6EEE27AB5E5100F51793 /* PasswordManagementListSection.swift in Sources */, @@ -11791,7 +11812,7 @@ 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */, B6619EF62B10DFF700CD9186 /* InstructionsFormatParserTests.swift in Sources */, 569277C429DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */, - 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionVisibilityTests.swift in Sources */, + 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */, 85F1B0C925EF9759004792B6 /* URLEventHandlerTests.swift in Sources */, 4B9292BD2667103100AD2C21 /* BookmarkOutlineViewDataSourceTests.swift in Sources */, C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */, @@ -11965,6 +11986,7 @@ BDA7648D2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift in Sources */, 85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */, B626A7642992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + C1D8BE452C1739E70057E426 /* DataBrokerProtectionMocks.swift in Sources */, AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, 859E7D6D274548F2009C2B69 /* BookmarksExporterTests.swift in Sources */, B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index fd69a8fc1a..fe67b31e4c 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -339,7 +339,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { networkProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() - NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidFinishLaunching() + NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidFinishLaunching() UNUserNotificationCenter.current().delegate = self #if DBP @@ -347,7 +347,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif #if DBP - DataBrokerProtectionAppEvents().applicationDidFinishLaunching() + DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: accountManager)).applicationDidFinishLaunching() #endif setUpAutoClearHandler() @@ -375,9 +375,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() - NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() + NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() #if DBP - DataBrokerProtectionAppEvents().applicationDidBecomeActive() + DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: accountManager)).applicationDidBecomeActive() #endif AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.toggleProtectionsCounter.sendEventsIfNeeded() @@ -677,17 +677,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - -#if DBP - if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { - DispatchQueue.main.async { - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) - } - } -#endif - } - completionHandler() } diff --git a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift index 51c1496884..93b0e2f5d8 100644 --- a/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift +++ b/DuckDuckGo/Common/Surveys/SurveyRemoteMessaging.swift @@ -73,7 +73,6 @@ final class DefaultSurveyRemoteMessaging: SurveyRemoteMessaging { subscriptionFetcher: SurveyRemoteMessageSubscriptionFetching, vpnActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), pirActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp), - networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), minimumRefreshInterval: TimeInterval, userDefaults: UserDefaults = .standard ) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index a86c213dc4..4c98934cc4 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -24,30 +24,41 @@ import Common import DataBrokerProtection struct DataBrokerProtectionAppEvents { - let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + + private let featureGatekeeper: DataBrokerProtectionFeatureGatekeeper + private let pixelHandler: EventMapping + private let loginItemsManager: LoginItemsManaging + private let loginItemInterface: DataBrokerProtectionLoginItemInterface enum WaitlistNotificationSource { case localPush case cardUI } - func applicationDidFinishLaunching() { - let loginItemsManager = LoginItemsManager() - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - let loginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface + init(featureGatekeeper: DataBrokerProtectionFeatureGatekeeper, + pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), + loginItemsManager: LoginItemsManaging = LoginItemsManager(), + loginItemInterface: DataBrokerProtectionLoginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface) { + self.featureGatekeeper = featureGatekeeper + self.pixelHandler = pixelHandler + self.loginItemsManager = loginItemsManager + self.loginItemInterface = loginItemInterface + } - guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + func applicationDidFinishLaunching() { + guard !featureGatekeeper.cleanUpDBPForPrivacyProIfNecessary() else { return } /// If the user is not in the waitlist and Privacy Pro flag is false, we want to remove the data for waitlist users /// since the waitlist flag might have been turned off - if !featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() { - featureVisibility.disableAndDeleteForWaitlistUsers() + if !featureGatekeeper.isFeatureVisible() && !featureGatekeeper.isPrivacyProEnabled() { + featureGatekeeper.disableAndDeleteForWaitlistUsers() return } - Task { - try? await DataBrokerProtectionWaitlist().redeemDataBrokerProtectionInviteCodeIfAvailable() + let loginItemsManager = LoginItemsManager() + let loginItemInterface = DataBrokerProtectionManager.shared.loginItemInterface + Task { // If we don't have profileQueries it means there's no user profile saved in our DB // In this case, let's disable the agent and delete any left-over data because there's nothing for it to do if let profileQueriesCount = try? DataBrokerProtectionManager.shared.dataManager.profileQueriesCount(), @@ -58,27 +69,31 @@ struct DataBrokerProtectionAppEvents { try await Task.sleep(nanoseconds: 1_000_000_000) loginItemInterface.appLaunched() } else { - featureVisibility.disableAndDeleteForWaitlistUsers() + featureGatekeeper.disableAndDeleteForWaitlistUsers() } } } func applicationDidBecomeActive() { - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility() - guard !featureVisibility.cleanUpDBPForPrivacyProIfNecessary() else { return } + // Check feature prerequisites and disable the login item if they are not satisfied + Task { @MainActor in + let prerequisitesMet = await featureGatekeeper.arePrerequisitesSatisfied() + guard prerequisitesMet else { + loginItemsManager.disableLoginItems([LoginItem.dbpBackgroundAgent]) + return + } + } + + guard !featureGatekeeper.cleanUpDBPForPrivacyProIfNecessary() else { return } /// If the user is not in the waitlist and Privacy Pro flag is false, we want to remove the data for waitlist users /// since the waitlist flag might have been turned off - if !featureVisibility.isFeatureVisible() && !featureVisibility.isPrivacyProEnabled() { - featureVisibility.disableAndDeleteForWaitlistUsers() + if !featureGatekeeper.isFeatureVisible() && !featureGatekeeper.isPrivacyProEnabled() { + featureGatekeeper.disableAndDeleteForWaitlistUsers() return } - - Task { - try? await DataBrokerProtectionWaitlist().redeemDataBrokerProtectionInviteCodeIfAvailable() - } } @MainActor @@ -95,16 +110,6 @@ struct DataBrokerProtectionAppEvents { } } - func windowDidBecomeMain() { - sendActiveDataBrokerProtectionWaitlistUserPixel() - } - - private func sendActiveDataBrokerProtectionWaitlistUserPixel() { - if DefaultDataBrokerProtectionFeatureVisibility().waitlistIsOngoing { - DataBrokerProtectionExternalWaitlistPixels.fire(pixel: GeneralPixel.dataBrokerProtectionWaitlistUserActive, frequency: .daily) - } - } - private func restartBackgroundAgent(loginItemsManager: LoginItemsManager) { DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerResetLoginItemDaily, frequency: .daily) loginItemsManager.disableLoginItems([LoginItem.dbpBackgroundAgent]) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 39eb3062a3..8abf7cf443 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -37,7 +37,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { private let waitlistTimestampItem = NSMenuItem(title: "Waitlist Timestamp:") private let waitlistInviteCodeItem = NSMenuItem(title: "Waitlist Invite Code:") private let waitlistTermsAndConditionsAcceptedItem = NSMenuItem(title: "T&C Accepted:") - private let waitlistBypassItem = NSMenuItem(title: "Bypass Waitlist", action: #selector(DataBrokerProtectionDebugMenu.toggleBypassWaitlist)) private let productionURLMenuItem = NSMenuItem(title: "Use Production URL", action: #selector(DataBrokerProtectionDebugMenu.useWebUIProductionURL)) @@ -67,14 +66,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Send Notification", action: #selector(DataBrokerProtectionDebugMenu.sendWaitlistAvailableNotification)) .targetting(self) - NSMenuItem(title: "Fetch Invite Code", action: #selector(DataBrokerProtectionDebugMenu.fetchInviteCode)) - .targetting(self) - NSMenuItem.separator() - waitlistBypassItem - .targetting(self) - NSMenuItem.separator() waitlistTokenItem @@ -172,7 +165,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { // MARK: - Menu State Update override func update() { - updateWaitlistItems() updateWebUIMenuItemsState() updateEnvironmentMenu() updateShowStatusMenuIconMenu() @@ -311,10 +303,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("DBP waitlist state cleaned", log: .dataBrokerProtection) } - @objc private func toggleBypassWaitlist() { - DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist.toggle() - } - @objc private func resetTermsAndConditionsAcceptance() { UserDefaults().removeObject(forKey: UserDefaultsWrapper.Key.dataBrokerProtectionTermsAndConditionsAccepted.rawValue) NotificationCenter.default.post(name: .dataBrokerProtectionWaitlistAccessChanged, object: nil) @@ -327,14 +315,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("DBP waitlist notification sent", log: .dataBrokerProtection) } - @objc private func fetchInviteCode() { - os_log("Fetching invite code...", log: .dataBrokerProtection) - - Task { - try? await DataBrokerProtectionWaitlist().redeemDataBrokerProtectionInviteCodeIfAvailable() - } - } - @objc private func toggleShowStatusMenuItem() { settings.showInMenuBar.toggle() } @@ -386,23 +366,6 @@ final class DataBrokerProtectionDebugMenu: NSMenu { return menuItem } - private func updateWaitlistItems() { - let waitlistStorage = WaitlistKeychainStore(waitlistIdentifier: DataBrokerProtectionWaitlist.identifier, keychainAppGroup: Bundle.main.appGroup(bundle: .dbp)) - waitlistTokenItem.title = "Waitlist Token: \(waitlistStorage.getWaitlistToken() ?? "N/A")" - waitlistInviteCodeItem.title = "Waitlist Invite Code: \(waitlistStorage.getWaitlistInviteCode() ?? "N/A")" - - if let timestamp = waitlistStorage.getWaitlistTimestamp() { - waitlistTimestampItem.title = "Waitlist Timestamp: \(String(describing: timestamp))" - } else { - waitlistTimestampItem.title = "Waitlist Timestamp: N/A" - } - - let accepted = UserDefaults().bool(forKey: UserDefaultsWrapper.Key.dataBrokerProtectionTermsAndConditionsAccepted.rawValue) - waitlistTermsAndConditionsAcceptedItem.title = "T&C Accepted: \(accepted ? "Yes" : "No")" - - waitlistBypassItem.state = DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist ? .on : .off - } - private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment guard environmentMenu.items.count == 3 else { return } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index 18a1c89b86..1e81a281e8 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -41,7 +41,7 @@ struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling } func disableAndDelete() { - if !DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist { + if !DefaultDataBrokerProtectionFeatureGatekeeper.bypassWaitlist { do { try dataManager.removeAllData() diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift similarity index 72% rename from DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift rename to DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift index 051b2e4e71..c2c4d65768 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift @@ -1,5 +1,5 @@ // -// DataBrokerProtectionFeatureVisibility.swift +// DataBrokerProtectionFeatureGatekeeper.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -24,31 +24,27 @@ import Common import DataBrokerProtection import Subscription -protocol DataBrokerProtectionFeatureVisibility { +protocol DataBrokerProtectionFeatureGatekeeper { + var waitlistIsOngoing: Bool { get } func isFeatureVisible() -> Bool func disableAndDeleteForAllUsers() func disableAndDeleteForWaitlistUsers() func isPrivacyProEnabled() -> Bool func isEligibleForThankYouMessage() -> Bool + func cleanUpDBPForPrivacyProIfNecessary() -> Bool + func arePrerequisitesSatisfied() async -> Bool } -struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeatureVisibility { +struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeatureGatekeeper { private let privacyConfigurationManager: PrivacyConfigurationManaging private let featureDisabler: DataBrokerProtectionFeatureDisabling private let pixelHandler: EventMapping private let userDefaults: UserDefaults private let waitlistStorage: WaitlistStorage private let subscriptionAvailability: SubscriptionFeatureAvailability + private let accountManager: AccountManaging private let dataBrokerProtectionKey = "data-broker-protection.cleaned-up-from-waitlist-to-privacy-pro" - private var dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro: Bool { - get { - return userDefaults.bool(forKey: dataBrokerProtectionKey) - } - nonmutating set { - userDefaults.set(newValue, forKey: dataBrokerProtectionKey) - } - } /// Temporary code to use while we have both redeem flow for diary study users. Should be removed later static var bypassWaitlist = false @@ -58,13 +54,15 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), userDefaults: UserDefaults = .standard, waitlistStorage: WaitlistStorage = DataBrokerProtectionWaitlist().waitlistStorage, - subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability()) { + subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(), + accountManager: AccountManaging) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler self.userDefaults = userDefaults self.waitlistStorage = waitlistStorage self.subscriptionAvailability = subscriptionAvailability + self.accountManager = accountManager } var waitlistIsOngoing: Bool { @@ -89,26 +87,6 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature return (regionCode ?? "US") == "US" } - private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - - private var isWaitlistBetaActive: Bool { - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlistBetaActive) - } - - private var isWaitlistEnabled: Bool { - return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlist) - } - - private var isWaitlistUser: Bool { - waitlistStorage.isWaitlistUser - } - - private var wasWaitlistUser: Bool { - waitlistStorage.getWaitlistInviteCode() != nil - } - func isPrivacyProEnabled() -> Bool { return subscriptionAvailability.isFeatureAvailable } @@ -159,5 +137,67 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature return isWaitlistEnabled && isWaitlistBetaActive } } + + func arePrerequisitesSatisfied() async -> Bool { + let entitlements = await accountManager.hasEntitlement(for: .dataBrokerProtection, + cachePolicy: .reloadIgnoringLocalCacheData) + var hasEntitlements: Bool + switch entitlements { + case .success(let value): + hasEntitlements = value + case .failure: + hasEntitlements = false + } + + let isAuthenticated = accountManager.accessToken != nil + + firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: hasEntitlements, isAuthenticatedResult: isAuthenticated) + + return hasEntitlements && isAuthenticated + } +} + +private extension DefaultDataBrokerProtectionFeatureGatekeeper { + + var dataBrokerProtectionCleanedUpFromWaitlistToPrivacyPro: Bool { + get { + return userDefaults.bool(forKey: dataBrokerProtectionKey) + } + nonmutating set { + userDefaults.set(newValue, forKey: dataBrokerProtectionKey) + } + } + + var isInternalUser: Bool { + NSApp.delegateTyped.internalUserDecider.isInternalUser + } + + var isWaitlistBetaActive: Bool { + return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlistBetaActive) + } + + var isWaitlistEnabled: Bool { + return privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DBPSubfeature.waitlist) + } + + var isWaitlistUser: Bool { + waitlistStorage.isWaitlistUser + } + + var wasWaitlistUser: Bool { + waitlistStorage.getWaitlistInviteCode() != nil + } + + func firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: Bool, isAuthenticatedResult: Bool) { + if !hasEntitlements { + pixelHandler.fire(.gatekeeperEntitlementsInvalid) + os_log("🔴 DBP feature Gatekeeper: Entitlement check failed", log: .dataBrokerProtection) + } + + if !isAuthenticatedResult { + pixelHandler.fire(.gatekeeperNotAuthenticated) + os_log("🔴 DBP feature Gatekeeper: Authentication check failed", log: .dataBrokerProtection) + } + } } #endif diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index a073fa9aa5..5c257d71d4 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -97,7 +97,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping { @@ -44,12 +44,12 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent], privacyConfigurationManager: PrivacyConfigurationManaging, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + vpnGatekeeper: VPNFeatureGatekeeper = DefaultVPNFeatureGatekeeper(subscriptionManager: Application.appDelegate.subscriptionManager), vpnTunnelIPCClient: VPNControllerXPCClient = .shared ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs - self.vpnVisibility = vpnVisibility + self.vpnGatekeeper = vpnGatekeeper self.vpnTunnelIPCClient = vpnTunnelIPCClient resetTabSelectionIfNeeded() @@ -81,12 +81,12 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility, + vpnGatekeeper: VPNFeatureGatekeeper, includeDuckPlayer: Bool, userDefaults: UserDefaults = .netP ) { let loadSections = { - let includingVPN = vpnVisibility.isInstalled + let includingVPN = vpnGatekeeper.isInstalled return PreferencesSection.defaultSections( includingDuckPlayer: includeDuckPlayer, @@ -99,13 +99,13 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: privacyConfigurationManager, syncService: syncService, - vpnVisibility: vpnVisibility) + vpnGatekeeper: vpnGatekeeper) } // MARK: - Setup private func setupVPNPaneVisibility() { - vpnVisibility.onboardStatusPublisher + vpnGatekeeper.onboardStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 4d14f35440..22d95b8051 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -69,7 +69,7 @@ final class VPNPreferencesModel: ObservableObject { private var onboardingStatus: OnboardingStatus { didSet { - showUninstallVPN = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager).isInstalled + showUninstallVPN = DefaultVPNFeatureGatekeeper(subscriptionManager: Application.appDelegate.subscriptionManager).isInstalled } } diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 967bc49768..7520ff043a 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -35,7 +35,7 @@ final class PreferencesViewController: NSViewController { init(syncService: DDGSyncing, duckPlayer: DuckPlayer = DuckPlayer.shared) { model = PreferencesSidebarModel(syncService: syncService, - vpnVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + vpnGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: Application.appDelegate.subscriptionManager), includeDuckPlayer: duckPlayer.isAvailable) super.init(nibName: nil, bundle: nil) } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift similarity index 96% rename from DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift rename to DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift index e88a74b340..1771c828ae 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionFeatureVisibility.swift +// VPNFeatureGatekeeper.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -26,7 +26,7 @@ import LoginItems import PixelKit import Subscription -protocol NetworkProtectionFeatureVisibility { +protocol VPNFeatureGatekeeper { var isInstalled: Bool { get } func canStartVPN() async throws -> Bool @@ -37,7 +37,7 @@ protocol NetworkProtectionFeatureVisibility { var onboardStatusPublisher: AnyPublisher { get } } -struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { +struct DefaultVPNFeatureGatekeeper: VPNFeatureGatekeeper { private static var subscriptionAuthTokenPrefix: String { "ddg:" } private let vpnUninstaller: VPNUninstalling private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation diff --git a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift index 75af13e0a9..fe79b7cc96 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistThankYouPromptPresenter.swift @@ -33,7 +33,7 @@ final class WaitlistThankYouPromptPresenter { convenience init() { self.init(isPIRBetaTester: { - return DefaultDataBrokerProtectionFeatureVisibility().isEligibleForThankYouMessage() + false }) } diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index b8bc8e1ba7..4ed31b5b08 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -199,42 +199,6 @@ struct DataBrokerProtectionWaitlist: Waitlist { self.redeemAuthenticationRepository = redeemAuthenticationRepository } - func redeemDataBrokerProtectionInviteCodeIfAvailable() async throws { - if DefaultDataBrokerProtectionFeatureVisibility.bypassWaitlist || DefaultDataBrokerProtectionFeatureVisibility().isPrivacyProEnabled() { - return - } - - do { - guard waitlistStorage.getWaitlistToken() != nil else { - os_log("User not in DBP waitlist, returning...", log: .default) - return - } - - guard redeemAuthenticationRepository.getAccessToken() == nil else { - os_log("Invite code already redeemed, returning...", log: .default) - return - } - - var inviteCode = waitlistStorage.getWaitlistInviteCode() - - if inviteCode == nil { - os_log("No DBP invite code found, fetching...", log: .default) - inviteCode = try await fetchInviteCode() - } - - if let code = inviteCode { - try await redeemInviteCode(code) - } else { - os_log("No DBP invite code available") - throw WaitlistInviteCodeFetchError.noCodeAvailable - } - - } catch { - os_log("DBP Invite code error: %{public}@", log: .error, error.localizedDescription) - throw error - } - } - private func fetchInviteCode() async throws -> String { // First check if we have it stored locally diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 7d2f3fcd30..27693288cd 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -178,12 +178,17 @@ public enum DataBrokerProtectionPixels { case entitlementCheckValid case entitlementCheckInvalid case entitlementCheckError + // Measure success/failure rate of Personal Information Removal Pixels // https://app.asana.com/0/1204006570077678/1206889724879222/f case globalMetricsWeeklyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) case globalMetricsMonthlyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) case dataBrokerMetricsWeeklyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int) case dataBrokerMetricsMonthlyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int) + + // Feature Gatekeeper + case gatekeeperNotAuthenticated + case gatekeeperEntitlementsInvalid } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -294,10 +299,15 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .entitlementCheckValid: return "m_mac_dbp_macos_entitlement_valid" case .entitlementCheckInvalid: return "m_mac_dbp_macos_entitlement_invalid" case .entitlementCheckError: return "m_mac_dbp_macos_entitlement_error" + case .globalMetricsWeeklyStats: return "m_mac_dbp_weekly_stats" case .globalMetricsMonthlyStats: return "m_mac_dbp_monthly_stats" case .dataBrokerMetricsWeeklyStats: return "m_mac_dbp_databroker_weekly_stats" case .dataBrokerMetricsMonthlyStats: return "m_mac_dbp_databroker_monthly_stats" + + // Feature Gatekeeper + case .gatekeeperNotAuthenticated: return "m_mac_dbp_gatekeeper_not_authenticated" + case .gatekeeperEntitlementsInvalid: return "m_mac_dbp_gatekeeper_entitlements_invalid" } } @@ -398,7 +408,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .secureVaultInitError, .secureVaultKeyStoreReadError, .secureVaultKeyStoreUpdateError, - .secureVaultError: + .secureVaultError, + .gatekeeperNotAuthenticated, + .gatekeeperEntitlementsInvalid: return [:] case .ipcServerProfileSavedCalledByApp, .ipcServerProfileSavedReceivedByAgent, @@ -537,7 +549,9 @@ public class DataBrokerProtectionPixelsHandler: EventMapping UserDefaults { - UserDefaults(suiteName: "testing_\(UUID().uuidString)")! - } - - override func setUpWithError() throws { - mockFeatureDisabler = MockFeatureDisabler() - mockFeatureAvailability = MockFeatureAvailability() - waitlistStorage = MockWaitlistStorage() - } - - override func tearDownWithError() throws { - mockFeatureDisabler.reset() - mockFeatureAvailability.reset() - waitlistStorage.deleteWaitlistState() - } - - /// Waitlist is OFF, Not redeemed - /// PP flag is OF - func testWhenWaitlistHasNoInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is OFF, Not redeemed - /// PP flag is ON - func testWhenWaitlistHasNoInviteCodeAndFeatureEnabled_thenCleanUpIsNotCalled() throws { - mockFeatureAvailability.mockFeatureAvailable = true - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is ON, redeemed - /// PP flag is OFF - func testWhenWaitlistHasInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { - waitlistStorage.store(waitlistToken: "potato") - waitlistStorage.store(inviteCode: "banana") - waitlistStorage.store(waitlistTimestamp: 123) - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is ON, redeemed - /// PP flag is ON - func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalled() throws { - waitlistStorage.store(waitlistToken: "potato") - waitlistStorage.store(inviteCode: "banana") - waitlistStorage.store(waitlistTimestamp: 123) - mockFeatureAvailability.mockFeatureAvailable = true - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertTrue(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) - } - - /// Waitlist is ON, redeemed - /// PP flag is ON - func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalledTwice() throws { - waitlistStorage.store(waitlistToken: "potato") - waitlistStorage.store(inviteCode: "banana") - waitlistStorage.store(waitlistTimestamp: 123) - mockFeatureAvailability.mockFeatureAvailable = true - - let featureVisibility = DefaultDataBrokerProtectionFeatureVisibility(featureDisabler: mockFeatureDisabler, - userDefaults: userDefaults(), - waitlistStorage: waitlistStorage, - subscriptionAvailability: mockFeatureAvailability) - - XCTAssertTrue(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) - - XCTAssertFalse(featureVisibility.cleanUpDBPForPrivacyProIfNecessary()) - } -} - -private class MockFeatureDisabler: DataBrokerProtectionFeatureDisabling { - var disableAndDeleteWasCalled = false - - func disableAndDelete() { - disableAndDeleteWasCalled = true - } - - func reset() { - disableAndDeleteWasCalled = false - } -} - -private class MockFeatureAvailability: SubscriptionFeatureAvailability { - var mockFeatureAvailable: Bool = false - var mockSubscriptionPurchaseAllowed: Bool = false - - var isFeatureAvailable: Bool { mockFeatureAvailable } - var isSubscriptionPurchaseAllowed: Bool { mockSubscriptionPurchaseAllowed } - - func reset() { - mockFeatureAvailable = false - mockSubscriptionPurchaseAllowed = false - } -} diff --git a/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift b/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift new file mode 100644 index 0000000000..43ef8327f4 --- /dev/null +++ b/UnitTests/DBP/Mocks/DataBrokerProtectionMocks.swift @@ -0,0 +1,82 @@ +// +// DataBrokerProtectionMocks.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription +@testable import DuckDuckGo_Privacy_Browser + +final class MockAccountManager: AccountManaging { + var hasEntitlementResult: Result = .success(true) + + var delegate: AccountManagerKeychainAccessDelegate? + + var isUserAuthenticated = false + + var accessToken: String? = "" + + var authToken: String? + + var email: String? + + var externalID: String? + + func storeAuthToken(token: String) { + } + + func storeAccount(token: String, email: String?, externalID: String?) { + } + + func signOut(skipNotification: Bool) { + } + + func signOut() { + } + + func migrateAccessTokenToNewStore() throws { + } + + func hasEntitlement(for entitlement: Entitlement.ProductName, cachePolicy: CachePolicy) async -> Result { + hasEntitlementResult + } + + func hasEntitlement(for entitlement: Entitlement.ProductName) async -> Result { + hasEntitlementResult + } + + func updateCache(with entitlements: [Entitlement]) { + } + + func fetchEntitlements(cachePolicy: CachePolicy) async -> Result<[Entitlement], any Error> { + .success([]) + } + + func exchangeAuthTokenToAccessToken(_ authToken: String) async -> Result { + .success("") + } + + func fetchAccountDetails(with accessToken: String) async -> Result { + .success(AccountDetails(email: "", externalID: "")) + } + + func refreshSubscriptionAndEntitlements() async { + } + + func checkForEntitlements(wait waitTime: Double, retry retryCount: Int) async -> Bool { + true + } +} diff --git a/UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift b/UnitTests/DBP/Tests/DataBrokerPrerequisitesStatusVerifierTests.swift similarity index 100% rename from UnitTests/DBP/DataBrokerPrerequisitesStatusVerifierTests.swift rename to UnitTests/DBP/Tests/DataBrokerPrerequisitesStatusVerifierTests.swift diff --git a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift new file mode 100644 index 0000000000..7f70a0e805 --- /dev/null +++ b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift @@ -0,0 +1,223 @@ +// +// DataBrokerProtectionFeatureGatekeeperTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import BrowserServicesKit +import Subscription + +@testable import DuckDuckGo_Privacy_Browser + +final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { + + private var sut: DefaultDataBrokerProtectionFeatureGatekeeper! + private var mockFeatureDisabler: MockFeatureDisabler! + private var mockFeatureAvailability: MockFeatureAvailability! + private var waitlistStorage: MockWaitlistStorage! + private var mockAccountManager: MockAccountManager! + + private func userDefaults() -> UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + override func setUpWithError() throws { + mockFeatureDisabler = MockFeatureDisabler() + mockFeatureAvailability = MockFeatureAvailability() + waitlistStorage = MockWaitlistStorage() + mockAccountManager = MockAccountManager() + } + + /// Waitlist is OFF, Not redeemed + /// PP flag is OF + func testWhenWaitlistHasNoInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is OFF, Not redeemed + /// PP flag is ON + func testWhenWaitlistHasNoInviteCodeAndFeatureEnabled_thenCleanUpIsNotCalled() throws { + mockFeatureAvailability.mockFeatureAvailable = true + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is ON, redeemed + /// PP flag is OFF + func testWhenWaitlistHasInviteCodeAndFeatureDisabled_thenCleanUpIsNotCalled() throws { + waitlistStorage.store(waitlistToken: "potato") + waitlistStorage.store(inviteCode: "banana") + waitlistStorage.store(waitlistTimestamp: 123) + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertFalse(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is ON, redeemed + /// PP flag is ON + func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalled() throws { + waitlistStorage.store(waitlistToken: "potato") + waitlistStorage.store(inviteCode: "banana") + waitlistStorage.store(waitlistTimestamp: 123) + mockFeatureAvailability.mockFeatureAvailable = true + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertTrue(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) + } + + /// Waitlist is ON, redeemed + /// PP flag is ON + func testWhenWaitlistHasInviteCodeAndFeatureEnabled_thenCleanUpIsCalledTwice() throws { + waitlistStorage.store(waitlistToken: "potato") + waitlistStorage.store(inviteCode: "banana") + waitlistStorage.store(waitlistTimestamp: 123) + mockFeatureAvailability.mockFeatureAvailable = true + + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + XCTAssertTrue(sut.cleanUpDBPForPrivacyProIfNecessary()) + XCTAssertTrue(mockFeatureDisabler.disableAndDeleteWasCalled) + + XCTAssertFalse(sut.cleanUpDBPForPrivacyProIfNecessary()) + } + + func testWhenNoAccessTokenIsFound_butEntitlementIs_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = nil + mockAccountManager.hasEntitlementResult = .success(true) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertFalse(result) + } + + func testWhenAccessTokenIsFound_butNoEntitlementIs_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = "token" + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertFalse(result) + } + + func testWhenAccessTokenAndEntitlementAreNotFound_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = nil + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertFalse(result) + } + + func testWhenAccessTokenAndEntitlementAreFound_thenFeatureIsEnabled() async { + // Given + mockAccountManager.accessToken = "token" + mockAccountManager.hasEntitlementResult = .success(true) + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + waitlistStorage: waitlistStorage, + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertTrue(result) + } +} + +private enum MockError: Error { + case someError +} + +private class MockFeatureDisabler: DataBrokerProtectionFeatureDisabling { + var disableAndDeleteWasCalled = false + + func disableAndDelete() { + disableAndDeleteWasCalled = true + } + + func reset() { + disableAndDeleteWasCalled = false + } +} + +private class MockFeatureAvailability: SubscriptionFeatureAvailability { + var mockFeatureAvailable: Bool = false + var mockSubscriptionPurchaseAllowed: Bool = false + + var isFeatureAvailable: Bool { mockFeatureAvailable } + var isSubscriptionPurchaseAllowed: Bool { mockSubscriptionPurchaseAllowed } + + func reset() { + mockFeatureAvailable = false + mockSubscriptionPurchaseAllowed = false + } +} diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 0140a43a9f..a0b8b38f64 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -47,7 +47,7 @@ final class MoreOptionsMenuTests: XCTestCase { networkProtectionVisibilityMock = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: networkProtectionVisibilityMock, + vpnFeatureGatekeeper: networkProtectionVisibilityMock, sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, accountManager: accountManager) @@ -68,7 +68,7 @@ final class MoreOptionsMenuTests: XCTestCase { func testThatMoreOptionMenuHasTheExpectedItemsAuthenticated() { moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), + vpnFeatureGatekeeper: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, accountManager: accountManager) @@ -100,7 +100,7 @@ final class MoreOptionsMenuTests: XCTestCase { accountManager = AccountManagerMock(isUserAuthenticated: false) moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), + vpnFeatureGatekeeper: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, accountManager: accountManager) @@ -164,7 +164,7 @@ final class MoreOptionsMenuTests: XCTestCase { } -final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { +final class NetworkProtectionVisibilityMock: VPNFeatureGatekeeper { var onboardStatusPublisher: AnyPublisher { Just(.default).eraseToAnyPublisher()