Skip to content

Commit

Permalink
Freemium PIR: Prerequisite Checks and Scheduled Scans (#3178)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1206488453854252/1208052452067290/f

**Description**: This PR (_to a feature branch_) implements two Freemium
PIR aspects:

1. When checking PIR prerequisites (see `GateKeeper..` `AgentStopper…`
types), it accounts for Freemium PIR users
2. When running scheduled operations, it only performs scans, and not
opt-outs
  • Loading branch information
aataraxiaa committed Sep 18, 2024
1 parent 4a50a69 commit e8daaf5
Show file tree
Hide file tree
Showing 29 changed files with 881 additions and 205 deletions.
6 changes: 6 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2747,6 +2747,8 @@
BDCB66D92C7CE1A700E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */; };
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 */; };
Expand Down Expand Up @@ -4552,6 +4554,7 @@
BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModel.swift; sourceTree = "<group>"; };
BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNMetadataCollector.swift; sourceTree = "<group>"; };
BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = "<group>"; };
C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreemiumDebugMenu.swift; sourceTree = "<group>"; };
C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = "<group>"; };
C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = "<group>"; };
C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -9028,6 +9031,7 @@
isa = PBXGroup;
children = (
C1858CD02C7C95E500C9BEAB /* PIR */,
C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */,
);
path = Freemium;
sourceTree = "<group>";
Expand Down Expand Up @@ -10791,6 +10795,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 */,
Expand Down Expand Up @@ -12879,6 +12884,7 @@
85589E8327BBB8630038AD11 /* HomePageViewController.swift in Sources */,
F17114852C7C9D28009836C1 /* Logger+Fire.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 */,
Expand Down
18 changes: 14 additions & 4 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import NetworkProtectionIPC
import DataBrokerProtection
import RemoteMessaging
import os.log
import Freemium

final class AppDelegate: NSObject, NSApplicationDelegate {

Expand Down Expand Up @@ -383,7 +384,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()

Expand Down Expand Up @@ -430,9 +437,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 {
Expand Down
31 changes: 14 additions & 17 deletions DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ import Common
import DataBrokerProtection
import Subscription
import os.log
import Freemium

protocol DataBrokerProtectionFeatureGatekeeper {
func isFeatureVisible() -> Bool
func disableAndDeleteForAllUsers()
func isPrivacyProEnabled() -> Bool
func arePrerequisitesSatisfied() async -> Bool
}

Expand All @@ -37,19 +36,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<DataBrokerProtectionPixels> = 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 {
Expand All @@ -70,28 +72,23 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature
return (regionCode ?? "US") == "US"
}

func isPrivacyProEnabled() -> Bool {
return subscriptionAvailability.isFeatureAvailable
}

func disableAndDeleteForAllUsers() {
featureDisabler.disableAndDelete()

Logger.dataBrokerProtection.debug("Disabling and removing DBP for all users")
}

/// 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
Expand Down
52 changes: 52 additions & 0 deletions DuckDuckGo/Freemium/FreemiumDebugMenu.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 5 additions & 5 deletions DuckDuckGo/HomePage/View/HomePageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? = {
Expand Down Expand Up @@ -71,7 +71,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
Expand All @@ -83,7 +83,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)
Expand Down Expand Up @@ -221,9 +221,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
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,8 @@ final class MainMenu: NSMenu {
NSMenuItem(title: "Personal Information Removal")
.submenu(DataBrokerProtectionDebugMenu())

FreemiumDebugMenu()

if case .normal = NSApp.runType {
NSMenuItem(title: "VPN")
.submenu(NetworkProtectionDebugMenu())
Expand Down
10 changes: 5 additions & 5 deletions DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,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
Expand All @@ -81,7 +81,7 @@ final class MoreOptionsMenu: NSMenu {
sharingMenu: NSMenu? = nil,
internalUserDecider: InternalUserDecider,
subscriptionManager: SubscriptionManager,
freemiumPIRUserState: FreemiumPIRUserState,
freemiumPIRUserStateManager: FreemiumPIRUserStateManager,
freemiumPIRFeature: FreemiumPIRFeature,
freemiumPIRPresenter: FreemiumPIRPresenter = DefaultFreemiumPIRPresenter(),
appearancePreferences: AppearancePreferences = .shared) {
Expand All @@ -93,7 +93,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
Expand Down Expand Up @@ -272,9 +272,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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand Down
Loading

0 comments on commit e8daaf5

Please sign in to comment.