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 authored Sep 3, 2024
1 parent 7520b68 commit 83b54ff
Show file tree
Hide file tree
Showing 29 changed files with 886 additions and 211 deletions.
6 changes: 6 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -4261,6 +4263,7 @@
BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = "<group>"; };
BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatterTests.swift; sourceTree = "<group>"; };
BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNLocationFormatter.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 @@ -8533,6 +8536,7 @@
isa = PBXGroup;
children = (
C1858CD02C7C95E500C9BEAB /* PIR */,
C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */,
);
path = Freemium;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
18 changes: 14 additions & 4 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import Subscription
import NetworkProtectionIPC
import DataBrokerProtection
import RemoteMessaging
import Freemium

final class AppDelegate: NSObject, NSApplicationDelegate {

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down
31 changes: 14 additions & 17 deletions DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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<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 @@ -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
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 @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
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 @@ -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
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 83b54ff

Please sign in to comment.