diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ac0ec260b8..36fdb71a2c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2571,6 +2571,8 @@ BDADBDCD2BD2BC5700421B9B /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + C126B35A2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */; }; + C126B35B2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */; }; C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; @@ -4261,6 +4263,7 @@ BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = ""; }; BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatterTests.swift; sourceTree = ""; }; BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNLocationFormatter.swift; sourceTree = ""; }; + C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreemiumDebugMenu.swift; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; @@ -8533,6 +8536,7 @@ isa = PBXGroup; children = ( C1858CD02C7C95E500C9BEAB /* PIR */, + C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */, ); path = Freemium; sourceTree = ""; @@ -10207,6 +10211,7 @@ 3706FB2C293F65D500E42796 /* UserDefaultsWrapper.swift in Sources */, 3706FB2D293F65D500E42796 /* PasswordManagementPopover.swift in Sources */, 3706FB2F293F65D500E42796 /* HomePageRecentlyVisitedModel.swift in Sources */, + C126B35B2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */, 3707C718294B5D0F00682A9F /* AdClickAttributionTabExtension.swift in Sources */, 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */, 3706FEBA293F6EFF00E42796 /* BWStatus.swift in Sources */, @@ -12150,6 +12155,7 @@ B6C00ECD292F89D9009C73A6 /* FindInPageTabExtension.swift in Sources */, 85589E8327BBB8630038AD11 /* HomePageViewController.swift in Sources */, 5614B3A12BBD639D009B5031 /* ZoomPopover.swift in Sources */, + C126B35A2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */, B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */, 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */, 1D36F4242A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 567082b8b1..6c332649a0 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -39,6 +39,7 @@ import Subscription import NetworkProtectionIPC import DataBrokerProtection import RemoteMessaging +import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { @@ -375,7 +376,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { UNUserNotificationCenter.current().delegate = self dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() - DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: subscriptionManager.accountManager)).applicationDidFinishLaunching() + + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: + subscriptionManager.accountManager, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) + + DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching() setUpAutoClearHandler() @@ -422,9 +429,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() - DataBrokerProtectionAppEvents(featureGatekeeper: - DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: - subscriptionManager.accountManager)).applicationDidBecomeActive() + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: + subscriptionManager.accountManager, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) + + DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidBecomeActive() subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in if isSubscriptionActive { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift index 491ca77a52..fea47cee1f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift @@ -21,11 +21,10 @@ import BrowserServicesKit import Common import DataBrokerProtection import Subscription +import Freemium protocol DataBrokerProtectionFeatureGatekeeper { - func isFeatureVisible() -> Bool func disableAndDeleteForAllUsers() - func isPrivacyProEnabled() -> Bool func arePrerequisitesSatisfied() async -> Bool } @@ -36,19 +35,22 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature private let userDefaults: UserDefaults private let subscriptionAvailability: SubscriptionFeatureAvailability private let accountManager: AccountManager + private let freemiumPIRUserStateManager: FreemiumPIRUserStateManager init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler(), pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), userDefaults: UserDefaults = .standard, subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(), - accountManager: AccountManager) { + accountManager: AccountManager, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler self.userDefaults = userDefaults self.subscriptionAvailability = subscriptionAvailability self.accountManager = accountManager + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager } var isUserLocaleAllowed: Bool { @@ -69,28 +71,23 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature return (regionCode ?? "US") == "US" } - func isPrivacyProEnabled() -> Bool { - return subscriptionAvailability.isFeatureAvailable - } - func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() os_log("Disabling and removing DBP for all users", log: .dataBrokerProtection) } - /// If we want to prevent new users from joining the waitlist while still allowing waitlist users to continue using it, - /// we should set isWaitlistEnabled to false and isWaitlistBetaActive to true. - /// To remove it from everyone, isWaitlistBetaActive should be set to false - func isFeatureVisible() -> Bool { - // only US locale should be available - guard isUserLocaleAllowed else { return false } + /// Checks PIR prerequisites + /// + /// Prerequisites are satisified if either: + /// 1. The user is an active freemium user + /// 2. The user has a subscription with valid entitlements + /// + /// - Returns: Bool indicating prerequisites are satisfied + func arePrerequisitesSatisfied() async -> Bool { - // US internal users should have it available by default - return isInternalUser - } + if freemiumPIRUserStateManager.isActiveUser { return true } - func arePrerequisitesSatisfied() async -> Bool { let entitlements = await accountManager.hasEntitlement(forProductName: .dataBrokerProtection, cachePolicy: .reloadIgnoringLocalCacheData) var hasEntitlements: Bool diff --git a/DuckDuckGo/Freemium/FreemiumDebugMenu.swift b/DuckDuckGo/Freemium/FreemiumDebugMenu.swift new file mode 100644 index 0000000000..7e46510a4f --- /dev/null +++ b/DuckDuckGo/Freemium/FreemiumDebugMenu.swift @@ -0,0 +1,52 @@ +// +// FreemiumDebugMenu.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 Freemium + +final class FreemiumDebugMenu: NSMenuItem { + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public init() { + super.init(title: "Freemium", action: nil, keyEquivalent: "") + self.submenu = makeSubmenu() + } + + private func makeSubmenu() -> NSMenu { + let menu = NSMenu(title: "") + + menu.addItem(NSMenuItem(title: "Set Freemium PIR Onboarded State TRUE", action: #selector(setFreemiumPIROnboardStateEnabled), target: self)) + menu.addItem(NSMenuItem(title: "Set Freemium PIR Onboarded State FALSE", action: #selector(setFreemiumPIROnboardStateDisabled), target: self)) + menu.addItem(.separator()) + + return menu + } + + @objc + func setFreemiumPIROnboardStateEnabled() { + DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: Application.appDelegate.subscriptionManager.accountManager).didOnboard = true + } + + @objc + func setFreemiumPIROnboardStateDisabled() { + DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: Application.appDelegate.subscriptionManager.accountManager).didOnboard = false + } +} diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index dfdffe8968..16bd86647b 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -33,7 +33,7 @@ final class HomePageViewController: NSViewController { private let fireViewModel: FireViewModel private let onboardingViewModel: OnboardingViewModel private let freemiumPIRFeature: FreemiumPIRFeature - private var freemiumPIRUserState: FreemiumPIRUserState + private var freemiumPIRUserStateManager: FreemiumPIRUserStateManager private let freemiumPIRPresenter: FreemiumPIRPresenter private(set) lazy var faviconsFetcherOnboarding: FaviconsFetcherOnboarding? = { @@ -70,7 +70,7 @@ final class HomePageViewController: NSViewController { appearancePreferences: AppearancePreferences = AppearancePreferences.shared, defaultBrowserPreferences: DefaultBrowserPreferences = DefaultBrowserPreferences.shared, freemiumPIRFeature: FreemiumPIRFeature, - freemiumPIRUserState: FreemiumPIRUserState, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager, freemiumPIRPresenter: FreemiumPIRPresenter = DefaultFreemiumPIRPresenter()) { self.tabCollectionViewModel = tabCollectionViewModel @@ -82,7 +82,7 @@ final class HomePageViewController: NSViewController { self.appearancePreferences = appearancePreferences self.defaultBrowserPreferences = defaultBrowserPreferences self.freemiumPIRFeature = freemiumPIRFeature - self.freemiumPIRUserState = freemiumPIRUserState + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager self.freemiumPIRPresenter = freemiumPIRPresenter super.init(nibName: nil, bundle: nil) @@ -218,9 +218,9 @@ final class HomePageViewController: NSViewController { private func createPromotionModel() -> PromotionViewModel { return PromotionViewModel.freemiumPIRPromotion { [weak self] in // TODO: Remove this - self?.freemiumPIRUserState.didOnboard = true + self?.freemiumPIRUserStateManager.didOnboard = true // ------ - self?.freemiumPIRPresenter.showFreemiumPIR(didOnboard: self?.freemiumPIRUserState.didOnboard ?? false, + self?.freemiumPIRPresenter.showFreemiumPIR(didOnboard: self?.freemiumPIRUserStateManager.didOnboard ?? false, windowControllerManager: WindowControllersManager.shared) self?.appearancePreferences.didDismissHomePagePromotion = true } closeAction: { [weak self] in diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 23f9878674..f610ad129b 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -623,6 +623,8 @@ final class MainMenu: NSMenu { NSMenuItem(title: "Personal Information Removal") .submenu(DataBrokerProtectionDebugMenu()) + FreemiumDebugMenu() + if case .normal = NSApp.runType { NSMenuItem(title: "VPN") .submenu(NetworkProtectionDebugMenu()) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index a830985e4e..4ff01bd32e 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -59,7 +59,7 @@ final class MoreOptionsMenu: NSMenu { private lazy var sharingMenu: NSMenu = SharingMenu(title: UserText.shareMenuItem) private var accountManager: AccountManager { subscriptionManager.accountManager } private let subscriptionManager: SubscriptionManager - private var freemiumPIRUserState: FreemiumPIRUserState + private var freemiumPIRUserStateManager: FreemiumPIRUserStateManager private let freemiumPIRFeature: FreemiumPIRFeature private let freemiumPIRPresenter: FreemiumPIRPresenter private let appearancePreferences: AppearancePreferences @@ -79,7 +79,7 @@ final class MoreOptionsMenu: NSMenu { sharingMenu: NSMenu? = nil, internalUserDecider: InternalUserDecider, subscriptionManager: SubscriptionManager, - freemiumPIRUserState: FreemiumPIRUserState, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager, freemiumPIRFeature: FreemiumPIRFeature, freemiumPIRPresenter: FreemiumPIRPresenter = DefaultFreemiumPIRPresenter(), appearancePreferences: AppearancePreferences = .shared) { @@ -91,7 +91,7 @@ final class MoreOptionsMenu: NSMenu { self.subscriptionFeatureAvailability = subscriptionFeatureAvailability self.internalUserDecider = internalUserDecider self.subscriptionManager = subscriptionManager - self.freemiumPIRUserState = freemiumPIRUserState + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager self.freemiumPIRFeature = freemiumPIRFeature self.freemiumPIRPresenter = freemiumPIRPresenter self.appearancePreferences = appearancePreferences @@ -261,9 +261,9 @@ final class MoreOptionsMenu: NSMenu { @objc func openFreemiumPIR(_ sender: NSMenuItem) { // TODO: Remove this - freemiumPIRUserState.didOnboard = true + freemiumPIRUserStateManager.didOnboard = true // ------ - freemiumPIRPresenter.showFreemiumPIR(didOnboard: freemiumPIRUserState.didOnboard, windowControllerManager: WindowControllersManager.shared) + freemiumPIRPresenter.showFreemiumPIR(didOnboard: freemiumPIRUserStateManager.didOnboard, windowControllerManager: WindowControllersManager.shared) appearancePreferences.isHomePagePromotionVisible = false } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index f8df8c580d..de5af94bd6 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -272,14 +272,14 @@ final class NavigationBarViewController: NSViewController { @IBAction func optionsButtonAction(_ sender: NSButton) { let internalUserDecider = NSApp.delegateTyped.internalUserDecider - let freemiumPIRUserState = DefaultFreemiumPIRUserState(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) let freemiumPIRFeature = DefaultFreemiumPIRFeature(subscriptionManager: subscriptionManager, accountManager: subscriptionManager.accountManager) let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: PasswordManagerCoordinator.shared, vpnFeatureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager), internalUserDecider: internalUserDecider, subscriptionManager: subscriptionManager, - freemiumPIRUserState: freemiumPIRUserState, + freemiumPIRUserStateManager: freemiumPIRUserStateManager, freemiumPIRFeature: freemiumPIRFeature) menu.actionDelegate = self diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift index 33c0f09473..21298d2df7 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift @@ -137,7 +137,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let deprecatedRemoteMessageStorage = DefaultSurveyRemoteMessagingStorage.surveys() - let freemiumPIRUserState = DefaultFreemiumPIRUserState(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) return RemoteMessagingConfigMatcher( appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, @@ -164,7 +164,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr hasCustomHomePage: startupPreferencesPersistor().launchToCustomHomePage, isDuckPlayerOnboarded: duckPlayerPreferencesPersistor.youtubeOverlayAnyButtonPressed, isDuckPlayerEnabled: duckPlayerPreferencesPersistor.duckPlayerModeBool != false, - isCurrentFreemiumPIRUser: freemiumPIRUserState.isActiveUser, + isCurrentFreemiumPIRUser: freemiumPIRUserStateManager.isActiveUser, dismissedDeprecatedMacRemoteMessageIds: deprecatedRemoteMessageStorage.dismissedMessageIDs() ), percentileStore: RemoteMessagingPercentileUserDefaultsStore(keyValueStore: UserDefaults.standard), diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 2739fee59c..06502beee9 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -676,10 +676,10 @@ final class BrowserTabViewController: NSViewController { return homePageViewController ?? { let subscriptionManager = Application.appDelegate.subscriptionManager let freemiumPIRFeature = DefaultFreemiumPIRFeature(subscriptionManager: subscriptionManager, accountManager: subscriptionManager.accountManager) - let freemiumPIRUserState = DefaultFreemiumPIRUserState(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) let homePageViewController = HomePageViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager, freemiumPIRFeature: freemiumPIRFeature, - freemiumPIRUserState: freemiumPIRUserState) + freemiumPIRUserStateManager: freemiumPIRUserStateManager) self.homePageViewController = homePageViewController return homePageViewController }() diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 96fc3c0878..b9d200edbc 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -102,7 +102,8 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele authenticationRepository: KeychainAuthenticationData()) let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager) + manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager, + accountManager: subscriptionManager.accountManager) manager?.agentFinishedLaunching() setupStatusBarMenu() diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 871782e9ca..4a727b8406 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -32,6 +32,7 @@ let package = Package( .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), + .package(path: "../Freemium"), ], targets: [ .target( @@ -41,6 +42,7 @@ let package = Package( .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), .byName(name: "XPCHelper"), .product(name: "PixelKit", package: "BrowserServicesKit"), + .product(name: "Freemium", package: "Freemium"), ], resources: [.process("Resources")], swiftSettings: [ @@ -52,6 +54,7 @@ let package = Package( dependencies: [ "DataBrokerProtection", "BrowserServicesKit", + "Freemium", ], resources: [ .process("Resources") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 22ba75d0af..7e6ebaca3d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -132,7 +132,8 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! - }) } + }) + } public func runAllOptOuts(showWebView: Bool) { xpc.execute(call: { server in diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 5004e9c9da..71740509b2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -38,7 +38,8 @@ struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { } enum OperationType { - case scan + case manualScan + case scheduledScan case optOut case all } @@ -119,7 +120,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { switch operationType { case .optOut: operationsData = brokerProfileQueriesData.flatMap { $0.optOutJobData } - case .scan: + case .manualScan, .scheduledScan: operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanJobData } case .all: operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } @@ -179,7 +180,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { runner: operationDependencies.runnerProvider.getJobRunner(), pixelHandler: operationDependencies.pixelHandler, showWebView: showWebView, - isImmediateOperation: operationType == .scan, + isImmediateOperation: operationType == .manualScan, userNotificationService: operationDependencies.userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift index 7720725d85..953b68e824 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift @@ -26,9 +26,9 @@ public struct DataBrokerExecutionConfig { private let concurrentOperationsOnManualScans: Int = 6 func concurrentOperationsFor(_ operation: OperationType) -> Int { switch operation { - case .all, .optOut: + case .all, .optOut, .scheduledScan: return concurrentOperationsDifferentBrokers - case .scan: + case .manualScan: return concurrentOperationsOnManualScans } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index d6b4da3b93..0e085fac9a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -20,11 +20,14 @@ import Foundation import Common import BrowserServicesKit import PixelKit +import Freemium +import Subscription // This is to avoid exposing all the dependancies outside of the DBP package public class DataBrokerProtectionAgentManagerProvider { - public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging) -> DataBrokerProtectionAgentManager { + public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging, + accountManager: AccountManager) -> DataBrokerProtectionAgentManager { let pixelHandler = DataBrokerProtectionPixelsHandler() let executionConfig = DataBrokerExecutionConfig() @@ -72,10 +75,13 @@ public class DataBrokerProtectionAgentManagerProvider { emailService: emailService, captchaService: captchaService) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: accountManager) + let agentstopper = DefaultDataBrokerProtectionAgentStopper(dataManager: dataManager, entitlementMonitor: DataBrokerProtectionEntitlementMonitor(), authenticationManager: authenticationManager, - pixelHandler: pixelHandler) + pixelHandler: pixelHandler, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) let operationDependencies = DefaultDataBrokerOperationDependencies( database: dataManager.database, @@ -93,7 +99,8 @@ public class DataBrokerProtectionAgentManagerProvider { dataManager: dataManager, operationDependencies: operationDependencies, pixelHandler: pixelHandler, - agentStopper: agentstopper) + agentStopper: agentstopper, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) } } @@ -107,6 +114,7 @@ public final class DataBrokerProtectionAgentManager { private let operationDependencies: DataBrokerOperationDependencies private let pixelHandler: EventMapping private let agentStopper: DataBrokerProtectionAgentStopper + private let freemiumPIRUserStateManager: FreemiumPIRUserStateManager // Used for debug functions only, so not injected private lazy var browserWindowManager = BrowserWindowManager() @@ -120,7 +128,8 @@ public final class DataBrokerProtectionAgentManager { dataManager: DataBrokerProtectionDataManaging, operationDependencies: DataBrokerOperationDependencies, pixelHandler: EventMapping, - agentStopper: DataBrokerProtectionAgentStopper + agentStopper: DataBrokerProtectionAgentStopper, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager ) { self.userNotificationService = userNotificationService self.activityScheduler = activityScheduler @@ -130,6 +139,7 @@ public final class DataBrokerProtectionAgentManager { self.operationDependencies = operationDependencies self.pixelHandler = pixelHandler self.agentStopper = agentStopper + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager self.activityScheduler.delegate = self self.ipcServer.serverDelegate = self @@ -147,11 +157,11 @@ public final class DataBrokerProtectionAgentManager { activityScheduler.startScheduler() didStartActivityScheduler = true fireMonitoringPixels() - queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + startFreemiumOrSubscriptionScheduledOperations(showWebView: false, operationDependencies: operationDependencies, completion: nil) /// Monitors entitlement changes every 60 minutes to optimize system performance and resource utilization by avoiding unnecessary operations when entitlement is invalid. /// While keeping the agent active with invalid entitlement has no significant risk, setting the monitoring interval at 60 minutes is a good balance to minimize backend checks. - agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: .minutes(60)) + agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: .minutes(60)) } } } @@ -177,11 +187,29 @@ extension DataBrokerProtectionAgentManager { } } +private extension DataBrokerProtectionAgentManager { + + /// Starts either Freemium (scan-only) or Subscription (scan and opt-out) scheduled operations + /// - Parameters: + /// - showWebView: Whether to show the web view or not + /// - operationDependencies: Operation dependencies + /// - completion: Completion handler + func startFreemiumOrSubscriptionScheduledOperations(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + if freemiumPIRUserStateManager.isActiveUser { + queueManager.startScheduledScanOperationsIfPermitted(showWebView: showWebView, operationDependencies: operationDependencies, completion: completion) + } else { + queueManager.startScheduledAllOperationsIfPermitted(showWebView: showWebView, operationDependencies: operationDependencies, completion: completion) + } + } +} + extension DataBrokerProtectionAgentManager: DataBrokerProtectionBackgroundActivitySchedulerDelegate { public func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtection.DataBrokerProtectionBackgroundActivityScheduler, completion: (() -> Void)?) { fireMonitoringPixels() - queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { _ in + startFreemiumOrSubscriptionScheduledOperations(showWebView: false, operationDependencies: operationDependencies) { _ in completion?() } } @@ -194,7 +222,7 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { userNotificationService.requestNotificationPermission() fireMonitoringPixels() - queueManager.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in + queueManager.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in guard let self = self else { return } if let errors = errors { @@ -202,15 +230,15 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { switch oneTimeError { case DataBrokerProtectionQueueError.interrupted: self.pixelHandler.fire(.ipcServerImmediateScansInterrupted) - os_log("Interrupted during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Interrupted during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateScanOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) default: self.pixelHandler.fire(.ipcServerImmediateScansFinishedWithError(error: oneTimeError)) - os_log("Error during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Error during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateScanOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateScanOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } @@ -230,7 +258,7 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { public func appLaunched() { fireMonitoringPixels() - queueManager.startScheduledOperationsIfPermitted(showWebView: false, + startFreemiumOrSubscriptionScheduledOperations(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in guard let self = self else { return } @@ -240,18 +268,18 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { switch oneTimeError { case DataBrokerProtectionQueueError.interrupted: self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansInterrupted) - os_log("Interrupted during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Interrupted during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledAllOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) case DataBrokerProtectionQueueError.cannotInterrupt: self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansBlocked) - os_log("Cannot interrupt during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted()") + os_log("Cannot interrupt during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledAllOperationsIfPermitted()") default: self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansFinishedWithError(error: oneTimeError)) - os_log("Error during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Error during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledAllOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateScanOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } @@ -281,13 +309,13 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentDebugComman } public func startImmediateOperations(showWebView: Bool) { - queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, + queueManager.startImmediateScanOperationsIfPermitted(showWebView: showWebView, operationDependencies: operationDependencies, completion: nil) } public func startScheduledOperations(showWebView: Bool) { - queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, + startFreemiumOrSubscriptionScheduledOperations(showWebView: showWebView, operationDependencies: operationDependencies, completion: nil) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index 297b3bc094..cac816428c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -73,12 +73,15 @@ protocol DataBrokerProtectionQueueManager { brokerUpdater: DataBrokerProtectionBrokerUpdater?, pixelHandler: EventMapping) - func startImmediateOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) - func startScheduledOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startImmediateScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledAllOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) var debugRunningStatusString: String { get } @@ -118,13 +121,13 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa self.pixelHandler = pixelHandler } - func startImmediateOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + func startImmediateScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { let newMode = DataBrokerProtectionQueueMode.immediate(completion: completion) startOperationsIfPermitted(forNewMode: newMode, - type: .scan, + type: .manualScan, showWebView: showWebView, operationDependencies: operationDependencies) { [weak self] errors in completion?(errors) @@ -132,15 +135,22 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa } } - func startScheduledOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - let newMode = DataBrokerProtectionQueueMode.scheduled(completion: completion) - startOperationsIfPermitted(forNewMode: newMode, - type: .all, - showWebView: showWebView, - operationDependencies: operationDependencies, - completion: completion) + func startScheduledAllOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + startScheduleOperationsIfPermitted(withOperationType: .all, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func startScheduledScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + startScheduleOperationsIfPermitted(withOperationType: .scheduledScan, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) } func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) { @@ -157,6 +167,18 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa private extension DefaultDataBrokerProtectionQueueManager { + func startScheduleOperationsIfPermitted(withOperationType operationType: OperationType, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + let newMode = DataBrokerProtectionQueueMode.scheduled(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: operationType, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + func startOperationsIfPermitted(forNewMode newMode: DataBrokerProtectionQueueMode, type: OperationType, showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift index fe6c7734be..8012ad94cc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift @@ -18,14 +18,15 @@ import Foundation import Common +import Freemium protocol DataBrokerProtectionAgentStopper { - /// Validates if the user has profile data, is authenticated, and has valid entitlement. If any of these conditions are not met, the agent will be stopped. + /// Validates if the user is an active freemium user, OR if they have profile data, is authenticated, and has valid entitlement. If any of these conditions are not met, the agent will be stopped. func validateRunPrerequisitesAndStopAgentIfNecessary() async - /// Monitors the entitlement package. If the entitlement check returns false, the agent will be stopped. + /// Monitors the entitlement package. If the entitlement check returns false, and the user is NOT an active freemium user, the agent will be stopped. /// This function ensures that the agent is stopped if the user's subscription has expired, even if the browser is not active. Regularly checking for entitlement is required since notifications are not posted to agents. - func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) + func monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: TimeInterval) } struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper { @@ -34,44 +35,58 @@ struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let pixelHandler: EventMapping private let stopAction: DataProtectionStopAction + private let freemiumPIRUserStateManager: FreemiumPIRUserStateManager init(dataManager: DataBrokerProtectionDataManaging, entitlementMonitor: DataBrokerProtectionEntitlementMonitoring, authenticationManager: DataBrokerProtectionAuthenticationManaging, pixelHandler: EventMapping, - stopAction: DataProtectionStopAction = DefaultDataProtectionStopAction()) { + stopAction: DataProtectionStopAction = DefaultDataProtectionStopAction(), + freemiumPIRUserStateManager: FreemiumPIRUserStateManager) { self.dataManager = dataManager self.entitlementMonitor = entitlementMonitor self.authenticationManager = authenticationManager self.pixelHandler = pixelHandler self.stopAction = stopAction + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager } + /// Checks PIR prerequisites and stops the agent if necessary + /// + /// Prerequisites are satisified if either: + /// 1. The user is an active freemium user + /// 2. The user has a subscription with valid entitlements public func validateRunPrerequisitesAndStopAgentIfNecessary() async { + do { - guard try dataManager.fetchProfile() != nil, - authenticationManager.isUserAuthenticated else { + let hasProfile = try dataManager.fetchProfile() != nil + let isAuthenticated = authenticationManager.isUserAuthenticated + let isFreemium = freemiumPIRUserStateManager.isActiveUser + + if !hasProfile || (!isAuthenticated && !isFreemium) { os_log("Prerequisites are invalid", log: .dataBrokerProtection) stopAgent() return } - os_log("Prerequisites are valid", log: .dataBrokerProtection) - } catch { - os_log("Error validating prerequisites, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - stopAgent() - } - do { - let result = try await authenticationManager.hasValidEntitlement() - stopAgentBasedOnEntitlementCheckResult(result ? .enabled : .disabled) + if !isAuthenticated && isFreemium { + os_log("User is Freemium", log: .dataBrokerProtection) + return + } + + let hasValidEntitlement = try await authenticationManager.hasValidEntitlement() + stopAgentBasedOnEntitlementCheckResult(hasValidEntitlement ? .enabled : .disabled) + } catch { + os_log("Error validating prerequisites or checking entitlement, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) stopAgentBasedOnEntitlementCheckResult(.error) } } - public func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) { + public func monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: TimeInterval) { entitlementMonitor.start(checkEntitlementFunction: authenticationManager.hasValidEntitlement, interval: interval) { result in + guard !self.freemiumPIRUserStateManager.isActiveUser else { return } stopAgentBasedOnEntitlementCheckResult(result) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift index cb54af211b..1ed238590a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift @@ -25,11 +25,17 @@ final class DataBrokerExecutionConfigTests: XCTestCase { private let sut = DataBrokerExecutionConfig() func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { - let value = sut.concurrentOperationsFor(.scan) + let value = sut.concurrentOperationsFor(.manualScan) let expectedValue = 6 XCTAssertEqual(value, expectedValue) } + func testWhenOperationIsScheduledScans_thenConcurrentOperationsBetweenBrokersIsTwo() { + let value = sut.concurrentOperationsFor(.scheduledScan) + let expectedValue = 2 + XCTAssertEqual(value, expectedValue) + } + func testWhenOperationIsAll_thenConcurrentOperationsBetweenBrokersIsTwo() { let value = sut.concurrentOperationsFor(.all) let expectedValue = 2 diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift index a19e16863a..8c839fc327 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -70,7 +70,7 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { mockDatabase.brokerProfileQueryDataToReturn = dataBrokerProfileQueries // When - let result = try! sut.operations(forOperationType: .scan, + let result = try! sut.operations(forOperationType: .manualScan, withPriorityDate: Date(), showWebView: false, errorDelegate: MockDataBrokerOperationErrorDelegate(), diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift index 185e21f2f8..8f79b47a55 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift @@ -32,6 +32,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { private var mockDependencies: DefaultDataBrokerOperationDependencies! private var mockProfile: DataBrokerProtectionProfile! private var mockAgentStopper: MockAgentStopper! + private var mockFreemiumPIRUserState: MockFreemiumPIRUserState! override func setUpWithError() throws { @@ -66,9 +67,50 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { addresses: [], phones: [], birthYear: 1992) + + mockFreemiumPIRUserState = MockFreemiumPIRUserState() + } + + func testWhenAgentStart_andProfileExists_andUserIsNotFreemium_thenActivityIsScheduled_andScheduledAllOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = false + + let schedulerStartedExpectation = XCTestExpectation(description: "Scheduler started") + var schedulerStarted = false + mockActivityScheduler.startSchedulerCompletion = { + schedulerStarted = true + schedulerStartedExpectation.fulfill() + } + + let scanCalledExpectation = XCTestExpectation(description: "Scan called") + var startScheduledScansCalled = false + mockQueueManager.startScheduledAllOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + scanCalledExpectation.fulfill() + } + + // When + sut.agentFinishedLaunching() + + // Then + await fulfillment(of: [scanCalledExpectation, schedulerStartedExpectation], timeout: 1.0) + XCTAssertTrue(schedulerStarted) + XCTAssertTrue(startScheduledScansCalled) } - func testWhenAgentStart_andProfileExists_thenActivityIsScheduled_andSheduledOpereationsRun() async throws { + func testWhenAgentStart_andProfileExists_andUserIsFreemium_thenActivityIsScheduled_andScheduledScanOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -78,9 +120,11 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = true let schedulerStartedExpectation = XCTestExpectation(description: "Scheduler started") var schedulerStarted = false @@ -91,7 +135,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { let scanCalledExpectation = XCTestExpectation(description: "Scan called") var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startScheduledScanOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true scanCalledExpectation.fulfill() } @@ -105,14 +149,14 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(startScheduledScansCalled) } - func testWhenAgentStart_andProfileDoesNotExist_thenActivityIsNotScheduled_andStopAgentIsCalled() async throws { + func testWhenAgentStart_andProfileDoesNotExist_andUserIsFreemium_thenActivityIsNotScheduled_andStopAgentIsCalled() async throws { // Given let mockStopAction = MockDataProtectionStopAction() let agentStopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: DataBrokerProtectionEntitlementMonitor(), authenticationManager: MockAuthenticationManager(), pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, freemiumPIRUserStateManager: MockFreemiumPIRUserState()) sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, activityScheduler: mockActivityScheduler, @@ -121,9 +165,11 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: agentStopper) + agentStopper: agentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = true let stopAgentExpectation = XCTestExpectation(description: "Stop agent expectation") @@ -153,7 +199,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = nil @@ -180,7 +227,35 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(monitorEntitlementWasCalled) } - func testWhenActivitySchedulerTriggers_thenSheduledOpereationsRun() async throws { + func testWhenActivitySchedulerTriggers_andUserIsNotFreemium_thenScheduledAllOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = false + + var startScheduledScansCalled = false + mockQueueManager.startScheduledAllOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + mockActivityScheduler.triggerDelegateCall() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenActivitySchedulerTriggers_andUserIsFreemium_thenScheduledScanOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -190,12 +265,14 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = true var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startScheduledScanOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true } @@ -206,7 +283,35 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(startScheduledScansCalled) } - func testWhenProfileSaved_thenImmediateOpereationsRun() async throws { + func testWhenProfileSaved_andUserIsNotFreemium_thenImmediateOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = false + + var startImmediateScansCalled = false + mockQueueManager.startImmediateScanOperationsIfPermittedCalledCompletion = { _ in + startImmediateScansCalled = true + } + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(startImmediateScansCalled) + } + + func testWhenProfileSaved_andUserIsFreemium_thenImmediateOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -216,12 +321,14 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = true var startImmediateScansCalled = false - mockQueueManager.startImmediateOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startImmediateScanOperationsIfPermittedCalledCompletion = { _ in startImmediateScansCalled = true } @@ -242,7 +349,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() @@ -263,7 +371,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() @@ -284,10 +393,11 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() - mockQueueManager.startImmediateOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) + mockQueueManager.startImmediateScanOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) // When sut.profileSaved() @@ -306,7 +416,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = true @@ -328,7 +439,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = false @@ -340,7 +452,34 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertFalse(mockNotificationService.checkInNotificationWasScheduled) } - func testWhenAppLaunched_thenSheduledOpereationsRun() async throws { + func testWhenAppLaunched_andUserIsNotFreemium_thenScheduledAllOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockFreemiumPIRUserState.isActiveUser = false + + var startScheduledScansCalled = false + mockQueueManager.startScheduledAllOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.appLaunched() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenAppLaunched_andUserIsFreemium_thenScheduledScanOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -350,10 +489,13 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockFreemiumPIRUserState.isActiveUser = true var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startScheduledScanOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift index 3271a31f69..a605d74d6d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift @@ -23,11 +23,13 @@ import Common @testable import DataBrokerProtection final class DataBrokerProtectionAgentStopperTests: XCTestCase { - private var mockPixelHandler: EventMapping! - private var mockAuthenticationManager: MockAuthenticationManager! - private var mockEntitlementMonitor: DataBrokerProtectionEntitlementMonitor! - private var mockDataManager: MockDataBrokerProtectionDataManager! - private var mockStopAction: MockDataProtectionStopAction! + + private var mockPixelHandler: EventMapping! + private var mockAuthenticationManager: MockAuthenticationManager! + private var mockEntitlementMonitor: DataBrokerProtectionEntitlementMonitor! + private var mockDataManager: MockDataBrokerProtectionDataManager! + private var mockStopAction: MockDataProtectionStopAction! + private var mockFreemiumPIRUserState: MockFreemiumPIRUserState! private var fakeProfile: DataBrokerProtectionProfile { let name = DataBrokerProtectionProfile.Name(firstName: "John", lastName: "Doe") @@ -44,6 +46,8 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { mockDataManager = MockDataBrokerProtectionDataManager(pixelHandler: mockPixelHandler, fakeBrokerFlag: DataBrokerDebugFlagFakeBroker()) mockStopAction = MockDataProtectionStopAction() + mockFreemiumPIRUserState = MockFreemiumPIRUserState() + mockFreemiumPIRUserState.isActiveUser = false } override func tearDown() { @@ -55,51 +59,176 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { mockStopAction = nil } - func testNoProfile_thenStopAgentIsCalled() async { + func testNoProfile_andUserIsNotAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = false + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testNoProfile_andUserIsNotAuthenticated_andUserIsFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testNoProfile_andUserIsAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertTrue(mockStopAction.wasStopCalled) } - func testInvalidEntitlement_thenStopAgentIsCalled() async { + func testNoProfile_andUserIsAuthenticated_andUserIsFreemium_thenStopAgentIsCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_andUserIsNotAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false mockAuthenticationManager.hasValidEntitlementValue = false mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertTrue(mockStopAction.wasStopCalled) } - func testUserNotAuthenticated_thenStopAgentIsCalled() async { + func testInvalidEntitlement_andUserIsNotAuthenticated_andUserIsFreemium_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_andUserIsAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_andUserIsAuthenticated_andUserIsFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testUserNotAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = false mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertTrue(mockStopAction.wasStopCalled) } + func testUserNotAuthenticated_andUserIsFreemium_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + func testErrorEntitlement_thenStopAgentIsNotCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.shouldThrowEntitlementError = true @@ -109,40 +238,62 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + + func testValidEntitlement_andUserIsNotFreemium_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertFalse(mockStopAction.wasStopCalled) } - func testValidEntitlement_thenStopAgentIsNotCalled() async { + func testValidEntitlement_andUserIsFreemium_thenStopAgentIsNotCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertFalse(mockStopAction.wasStopCalled) } - func testEntitlementMonitorWithValidResult_thenStopAgentIsNotCalled() { + func testEntitlementMonitorWithValidResult_andUserIsNotFreemium_thenStopAgentIsNotCalled() { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertFalse(mockStopAction.wasStopCalled) @@ -152,19 +303,45 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { wait(for: [expectation], timeout: 3) } - func testEntitlementMonitorWithInValidResult_thenStopAgentIsCalled() { + func testEntitlementMonitorWithValidResult_andUserIsFreemium_thenStopAgentIsNotCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in + XCTAssertFalse(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + } + + func testEntitlementMonitorWithInValidResult_andUserIsNotFreemium_thenStopAgentIsCalled() { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = false mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertTrue(mockStopAction.wasStopCalled) @@ -174,6 +351,30 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { wait(for: [expectation], timeout: 3) } + func testEntitlementMonitorWithInValidResult_andUserIsFreemium_thenStopAgentIsNotCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in + XCTAssertFalse(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + } + func testEntitlementMonitorWithErrorResult_thenStopAgentIsNotCalled() { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.shouldThrowEntitlementError = true @@ -183,10 +384,11 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertFalse(mockStopAction.wasStopCalled) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift index e69d984c64..592f6c88a9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift @@ -53,6 +53,57 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { userNotificationService: mockUserNotification) } + func testWhenStartImmediateScanOperations_thenCreatorIsCalledWithManualScanOperationType() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + + // When + sut.startImmediateScanOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies, + completion: nil) + + // Then + XCTAssertEqual(mockOperationsCreator.createdType, .manualScan) + } + + func testWhenStartScheduledAllOperations_thenCreatorIsCalledWithAllOperationType() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + + // When + sut.startScheduledAllOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies, + completion: nil) + + // Then + XCTAssertEqual(mockOperationsCreator.createdType, .all) + } + + func testWhenStartScheduledScanOperations_thenCreatorIsCalledWithScheduledScanOperationType() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + + // When + sut.startScheduledScanOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies, + completion: nil) + + // Then + XCTAssertEqual(mockOperationsCreator.createdType, .scheduledScan) + } + func testWhenStartImmediateScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, @@ -60,15 +111,15 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) - let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .manualScan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .manualScan, errorDelegate: sut, shouldError: true) mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] let expectation = expectation(description: "Expected errors to be returned in completion") var errorCollection: DataBrokerProtectionAgentErrorCollection! - let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.scan) + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.manualScan) // When - sut.startImmediateOperationsIfPermitted(showWebView: false, + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors expectation.fulfill() @@ -83,22 +134,52 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) } - func testWhenStartScheduledScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { + func testWhenStartScheduledAllOperations_andOperationsCompleteWithErrors_thenCompletionIsCalledWithErrors() async throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, operationsCreator: mockOperationsCreator, mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) - let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .all, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .all, errorDelegate: sut, shouldError: true) mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] let expectation = expectation(description: "Expected errors to be returned in completion") var errorCollection: DataBrokerProtectionAgentErrorCollection! let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.all) // When - sut.startScheduledOperationsIfPermitted(showWebView: false, + sut.startScheduledAllOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNotNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartScheduledScanOperations_andOperationsCompleteWithErrors_thenCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scheduledScan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scheduledScan, errorDelegate: sut, shouldError: true) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.scheduledScan) + + // When + sut.startScheduledScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors expectation.fulfill() @@ -120,13 +201,13 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startScheduledAllOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors } @@ -136,11 +217,11 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssert(mockQueue.operationCount == 2) // Given - mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperations // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } // Then XCTAssert(errorCollection.operationErrors?.count == 2) @@ -158,13 +239,13 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors } @@ -174,11 +255,11 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssert(mockQueue.operationCount == 2) // Given - mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperations // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } // Then XCTAssert(errorCollection.operationErrors?.count == 2) @@ -196,13 +277,13 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - var mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + var mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations var errorCollectionFirst: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollectionFirst = errors } @@ -213,12 +294,12 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { // Given var errorCollectionSecond: DataBrokerProtectionAgentErrorCollection! - mockOperationsWithError = (5...6).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - mockOperations = (7...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsWithError = (5...6).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + mockOperations = (7...8).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollectionSecond = errors } @@ -230,31 +311,76 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssert(mockQueue.didCallCancelCount == 1) } - func testWhenStartScheduledScan_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { + func testWhenStartScheduledAllOperations_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } + var mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, + operationType: .manualScan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(mockQueue.operationCount == 10) + + // Given + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } + mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, + operationType: .manualScan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + let expectedError = DataBrokerProtectionQueueError.cannotInterrupt + var completionCalled = false + + // When + sut.startScheduledAllOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + completionCalled.toggle() + } + + // Then + XCTAssert(mockQueue.didCallCancelCount == 0) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 10) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count == 0) + XCTAssertEqual((errorCollection.oneTimeError as? DataBrokerProtectionQueueError), expectedError) + XCTAssert(completionCalled) + } + + func testWhenStartScheduledScanOperations_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, operationsCreator: mockOperationsCreator, mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } var mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, - operationType: .scan, + operationType: .manualScan, errorDelegate: sut, shouldError: true) } mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } // Then XCTAssert(mockQueue.operationCount == 10) // Given - mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, - operationType: .scan, + operationType: .manualScan, errorDelegate: sut, shouldError: true) } mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError @@ -262,7 +388,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { var completionCalled = false // When - sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startScheduledScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors completionCalled.toggle() } @@ -287,7 +413,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors expectation.fulfill() @@ -306,7 +432,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.optOut) - XCTAssert(mockOperationsCreator.createdType == .scan) + XCTAssert(mockOperationsCreator.createdType == .manualScan) // When sut.execute(.startOptOutOperations(showWebView: false, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 27874b9905..fccaf51383 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -22,6 +22,7 @@ import Common import Foundation import GRDB import SecureStorage +import Freemium @testable import DataBrokerProtection @@ -1189,24 +1190,31 @@ extension DataBroker { final class MockDataBrokerProtectionOperationQueueManager: DataBrokerProtectionQueueManager { var debugRunningStatusString: String { return "" } - var startImmediateOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? - var startScheduledOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startImmediateScanOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startScheduledAllOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startScheduledScanOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? - var startImmediateOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? - var startScheduledOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startImmediateScanOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startScheduledAllOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startScheduledScanOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? init(operationQueue: DataBrokerProtection.DataBrokerProtectionOperationQueue, operationsCreator: DataBrokerProtection.DataBrokerOperationsCreator, mismatchCalculator: DataBrokerProtection.MismatchCalculator, brokerUpdater: DataBrokerProtection.DataBrokerProtectionBrokerUpdater?, pixelHandler: Common.EventMapping) { } - func startImmediateOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - completion?(startImmediateOperationsIfPermittedCompletionError) - startImmediateOperationsIfPermittedCalledCompletion?(startImmediateOperationsIfPermittedCompletionError) + func startImmediateScanOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startImmediateScanOperationsIfPermittedCompletionError) + startImmediateScanOperationsIfPermittedCalledCompletion?(startImmediateScanOperationsIfPermittedCompletionError) } - func startScheduledOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - completion?(startScheduledOperationsIfPermittedCompletionError) - startScheduledOperationsIfPermittedCalledCompletion?(startScheduledOperationsIfPermittedCompletionError) + func startScheduledAllOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startScheduledAllOperationsIfPermittedCompletionError) + startScheduledAllOperationsIfPermittedCalledCompletion?(startScheduledAllOperationsIfPermittedCompletionError) + } + + func startScheduledScanOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startScheduledScanOperationsIfPermittedCompletionError) + startScheduledScanOperationsIfPermittedCalledCompletion?(startScheduledScanOperationsIfPermittedCompletionError) } func execute(_ command: DataBrokerProtection.DataBrokerProtectionQueueManagerDebugCommand) { @@ -1481,7 +1489,7 @@ final class MockDataBrokerOperationsCreator: DataBrokerOperationsCreator { var operationCollections: [DataBrokerOperation] = [] var shouldError = false var priorityDate: Date? - var createdType: OperationType = .scan + var createdType: OperationType = .manualScan init(operationCollections: [DataBrokerOperation] = []) { self.operationCollections = operationCollections @@ -1575,7 +1583,7 @@ final class MockAgentStopper: DataBrokerProtectionAgentStopper { validateRunPrerequisitesCompletion?() } - func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) { + func monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: TimeInterval) { monitorEntitlementCompletion?() } } @@ -1929,3 +1937,8 @@ struct MockMigrationsProvider: DataBrokerProtectionDatabaseMigrationsProvider { return { _ in } } } + +final class MockFreemiumPIRUserState: FreemiumPIRUserStateManager { + var didOnboard = false + var isActiveUser = false +} diff --git a/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserState.swift b/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserStateManager.swift similarity index 82% rename from LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserState.swift rename to LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserStateManager.swift index 8a2ea1443a..315eab91b2 100644 --- a/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserState.swift +++ b/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserStateManager.swift @@ -1,5 +1,5 @@ // -// FreemiumPIRUserState.swift +// FreemiumPIRUserStateManager.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,14 +19,16 @@ import Foundation import Subscription -/// `FreemiumPIRUserState` types provide access to Freemium PIR-related state -public protocol FreemiumPIRUserState { +/// `FreemiumPIRUserStateManager` types provide access to Freemium PIR-related state +public protocol FreemiumPIRUserStateManager { var didOnboard: Bool { get set } + + /// `isActiveUser` implementations`should only return `true` if the current user DOES NOT have a subscription var isActiveUser: Bool { get } } -/// Default implementation of `FreemiumPIRUserState`. `UserDefaults` is used as underlying storage. -public final class DefaultFreemiumPIRUserState: FreemiumPIRUserState { +/// Default implementation of `FreemiumPIRUserStateManager`. `UserDefaults` is used as underlying storage. +public final class DefaultFreemiumPIRUserStateManager: FreemiumPIRUserStateManager { private enum Keys { static let didOnboard = "macos.browser.freemium.pir.did.onboard" diff --git a/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateTests.swift b/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateManagerTests.swift similarity index 79% rename from LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateTests.swift rename to LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateManagerTests.swift index c8543e0269..d17f2e0676 100644 --- a/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateTests.swift +++ b/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateManagerTests.swift @@ -1,5 +1,5 @@ // -// FreemiumPIRUserStateTests.swift +// FreemiumPIRUserStateManagerTests.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -21,21 +21,21 @@ import XCTest import Subscription import SubscriptionTestingUtilities -final class FreemiumPIRUserStateTests: XCTestCase { +final class FreemiumPIRUserStateManagerTests: XCTestCase { private static let testSuiteName = "test.defaults.freemium.user.state.tests" private let pir = "macos.browser.freemium.pir.did.onboard" - private let testUserDefaults = UserDefaults(suiteName: FreemiumPIRUserStateTests.testSuiteName)! + private let testUserDefaults = UserDefaults(suiteName: FreemiumPIRUserStateManagerTests.testSuiteName)! private var mockAccountManager: AccountManagerMock! override func setUpWithError() throws { mockAccountManager = AccountManagerMock() - testUserDefaults.removePersistentDomain(forName: FreemiumPIRUserStateTests.testSuiteName) + testUserDefaults.removePersistentDomain(forName: FreemiumPIRUserStateManagerTests.testSuiteName) } func testSetsHasFreemiumPIR() throws { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(testUserDefaults.bool(forKey: pir)) // When @@ -47,7 +47,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testGetsHasFreemiumPIR() throws { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(true, forKey: pir) XCTAssertTrue(testUserDefaults.bool(forKey: pir)) @@ -61,7 +61,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testIsCurrentFreemiumPIRUser_WhenDidOnboardIsTrueAndUserIsNotAuthenticated_ShouldReturnTrue() { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(true, forKey: pir) mockAccountManager.accessToken = nil @@ -76,7 +76,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testIsCurrentFreemiumPIRUser_WhenDidOnboardIsTrueAndUserIsAuthenticated_ShouldReturnFalse() { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(true, forKey: pir) mockAccountManager.accessToken = "some_token" @@ -91,7 +91,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testIsCurrentFreemiumPIRUser_WhenDidOnboardIsFalse_ShouldReturnFalse() { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(false, forKey: pir) mockAccountManager.accessToken = "some_token" diff --git a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift index f85d03a145..dd0f276105 100644 --- a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift +++ b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift @@ -28,6 +28,7 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { private var mockFeatureDisabler: MockFeatureDisabler! private var mockFeatureAvailability: MockFeatureAvailability! private var mockAccountManager: MockAccountManager! + private var mockFreemiumPIRUserState: MockFreemiumPIRUserState! private func userDefaults() -> UserDefaults { UserDefaults(suiteName: "testing_\(UUID().uuidString)")! @@ -37,16 +38,19 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { mockFeatureDisabler = MockFeatureDisabler() mockFeatureAvailability = MockFeatureAvailability() mockAccountManager = MockAccountManager() + mockFreemiumPIRUserState = MockFreemiumPIRUserState() + mockFreemiumPIRUserState.isActiveUser = false } - func testWhenNoAccessTokenIsFound_butEntitlementIs_thenFeatureIsDisabled() async { + func testWhenNoAccessTokenIsFound_butEntitlementIs_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given mockAccountManager.accessToken = nil mockAccountManager.hasEntitlementResult = .success(true) sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() @@ -55,14 +59,16 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { XCTAssertFalse(result) } - func testWhenAccessTokenIsFound_butNoEntitlementIs_thenFeatureIsDisabled() async { + func testWhenAccessTokenIsFound_butNoEntitlementIs_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given mockAccountManager.accessToken = "token" mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() @@ -71,14 +77,34 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { XCTAssertFalse(result) } - func testWhenAccessTokenAndEntitlementAreNotFound_thenFeatureIsDisabled() async { + func testWhenAccessTokenIsFound_butNoEntitlementIs_andIsActiveFreemiumUser_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = "token" + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = true + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertTrue(result) + } + + func testWhenAccessTokenAndEntitlementAreNotFound_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given mockAccountManager.accessToken = nil mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() @@ -87,14 +113,34 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { XCTAssertFalse(result) } - func testWhenAccessTokenAndEntitlementAreFound_thenFeatureIsEnabled() async { + func testWhenAccessTokenAndEntitlementAreFound_andIsNotActiveFreemiumUser_thenFeatureIsEnabled() async { // Given mockAccountManager.accessToken = "token" mockAccountManager.hasEntitlementResult = .success(true) + mockFreemiumPIRUserState.isActiveUser = false + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertTrue(result) + } + + func testWhenAccessTokenAndEntitlementAreNotFound_andIsActiveFreemiumUser_thenFeatureIsEnabled() async { + // Given + mockAccountManager.accessToken = nil + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = true sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index aa06d02224..775ae57226 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -92,7 +92,7 @@ final class MoreOptionsMenuTests: XCTestCase { sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, subscriptionManager: subscriptionManager, - freemiumPIRUserState: mockFreemiumPIRUserState, + freemiumPIRUserStateManager: mockFreemiumPIRUserState, freemiumPIRFeature: freemiumPIRFeature, freemiumPIRPresenter: mockFreemiumPIRPresenter) diff --git a/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift b/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift index f6e357c572..2a8ed992bf 100644 --- a/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift +++ b/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift @@ -30,7 +30,7 @@ struct MockRemoteMessagingStoreProvider: RemoteMessagingStoreProviding { } } -struct MockFreemiumPIRUserState: FreemiumPIRUserState { +struct MockFreemiumPIRUserState: FreemiumPIRUserStateManager { var didOnboard = false var isActiveUser = false }