Skip to content

Commit

Permalink
Freemium DBP: Feature Branch to Main PR (#3426)
Browse files Browse the repository at this point in the history
  • Loading branch information
aataraxiaa authored Oct 28, 2024
1 parent 4469572 commit 45c1658
Show file tree
Hide file tree
Showing 81 changed files with 6,091 additions and 459 deletions.
172 changes: 172 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

45 changes: 40 additions & 5 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 @@ -97,13 +98,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
public let subscriptionManager: SubscriptionManager
public let subscriptionUIHandler: SubscriptionUIHandling

public let vpnSettings = VPNSettings(defaults: .netP)
// MARK: - Freemium DBP
public let freemiumDBPFeature: FreemiumDBPFeature
public let freemiumDBPPromotionViewCoordinator: FreemiumDBPPromotionViewCoordinator
private var freemiumDBPScanResultPolling: FreemiumDBPScanResultPolling?

var configurationStore = ConfigurationStore()
var configurationManager: ConfigurationManager

// MARK: - VPN

public let vpnSettings = VPNSettings(defaults: .netP)

private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler?

private var vpnXPCClient: VPNControllerXPCClient {
Expand Down Expand Up @@ -271,6 +277,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

// Update DBP environment and match the Subscription environment
DataBrokerProtectionSettings().alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment)

// Freemium DBP
let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp)

let experimentManager = FreemiumDBPPixelExperimentManager(subscriptionManager: subscriptionManager)
experimentManager.assignUserToCohort()

freemiumDBPFeature = DefaultFreemiumDBPFeature(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
experimentManager: experimentManager,
subscriptionManager: subscriptionManager,
accountManager: subscriptionManager.accountManager,
freemiumDBPUserStateManager: freemiumDBPUserStateManager)
freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager,
freemiumDBPFeature: freemiumDBPFeature)
}

func applicationWillFinishLaunching(_ notification: Notification) {
Expand All @@ -295,6 +315,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager,
tunnelController: tunnelController,
vpnUninstaller: vpnUninstaller)

// Freemium DBP
freemiumDBPFeature.subscribeToDependencyUpdates()
}

func applicationDidFinishLaunching(_ notification: Notification) {
Expand Down Expand Up @@ -386,7 +409,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
UNUserNotificationCenter.current().delegate = self

dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents()
DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: subscriptionManager.accountManager)).applicationDidFinishLaunching()

let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp)
let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager:
subscriptionManager.accountManager,
freemiumDBPUserStateManager: freemiumDBPUserStateManager)

DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching()

setUpAutoClearHandler()

Expand All @@ -408,6 +437,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
PixelKit.fire(GeneralPixel.crashOnCrashHandlersSetUp)
didCrashDuringCrashHandlersSetUp = false
}

freemiumDBPScanResultPolling = DefaultFreemiumDBPScanResultPolling(dataManager: DataBrokerProtectionManager.shared.dataManager, freemiumDBPUserStateManager: freemiumDBPUserStateManager)
freemiumDBPScanResultPolling?.startPollingOrObserving()
}

private func fireFailedCompilationsPixelIfNeeded() {
Expand All @@ -433,9 +465,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive()

DataBrokerProtectionAppEvents(featureGatekeeper:
DefaultDataBrokerProtectionFeatureGatekeeper(accountManager:
subscriptionManager.accountManager)).applicationDidBecomeActive()
let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp)
let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager:
subscriptionManager.accountManager,
freemiumDBPUserStateManager: freemiumDBPUserStateManager)

DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidBecomeActive()

subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in
if isSubscriptionActive {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Radar-Check-96x96.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.
42 changes: 42 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1352,5 +1352,47 @@ struct UserText {
static let syncPromoSidePanelTitle = NSLocalizedString("sync.promo.passwords.side.panel.title", value:"Setup", comment: "Title for the Sync Promotion in passwords side panel")
static let syncPromoSidePanelSubtitle = NSLocalizedString("sync.promo.passwords.side.panel.subtitle", value:"Sync & Backup", comment: "Subtitle for the Sync Promotion in passwords side panel")

// Key: "freemium.pir.menu.item"
// Comment: "Title for Freemium Personal Information Removal (Scan-Only) item in the options menu"
static let freemiumDBPOptionsMenuItem = "Free Personal Information Scan"

// Key: "home.page.promotion.freemium.dbp.text"
// Comment: "Text for the Freemium DBP Home Page Promotion"
static let homePagePromotionFreemiumDBPText = "Find your personal info on sites that sell it."

// Key: "home.page.promotion.freemium.dbp.button.title"
// Comment: "Title for the Freemium DBP Home Page Promotion Button"
static let homePagePromotionFreemiumDBPButtonTitle = "Free Scan"

// Key: "home.page.promotion.freemium.dbp.post.scan.engagement.result.single.match.text"
// Comment: "Text for the Freemium DBP Home Page Post Scan Engagement Promotion When Only One Record is Found"
static let homePagePromotionFreemiumDBPPostScanEngagementResultSingleMatchText = "Your free personal info scan found 1 record about you on 1 site."

/// Generates Text for the Freemium DBP Home Page Post Scan Engagement Promotion when records are found on a single broker site.
/// Key: "home.page.promotion.freemium.dbp.post.scan.engagement.result.single.broker.text"
///
/// - Parameter resultCount: The number of records found.
/// - Returns: A formatted string indicating the number of records found on 1 site.
static func homePagePromotionFreemiumDBPPostScanEngagementResultSingleBrokerText(resultCount: Int) -> String {
String(format: "Your free personal info scan found %d records about you on 1 site.", resultCount)
}

/// Generates Text for the Freemium DBP Home Page Post Scan Engagement Promotion when records are found on multiple broker sites.
/// Key: "home.page.promotion.freemium.dbp.post.scan.engagement.result.plural.text"
///
/// - Parameters:
/// - resultCount: The number of records found.
/// - brokerCount: The number of broker sites where records were found.
/// - Returns: A formatted string indicating the number of records found on multiple sites.
static func homePagePromotionFreemiumDBPPostScanEngagementResultPluralText(resultCount: Int, brokerCount: Int) -> String {
String(format: "Your free personal info scan found %d records about you on %d different sites.", resultCount, brokerCount)
}

// Key: "home.page.promotion.freemium.dbp.post.scan.engagement.no.results.text"
// Comment: "Text for the Freemium DBP Home Page Post Scan Engagement Promotion When There Are No Results"
static let homePagePromotionFreemiumDBPPostScanEngagementNoResultsText = "Good news, your free personal info scan didn't find any records about you. We'll keep checking periodically."

// Key: "home.page.promotion.freemium.dbp.post.scan.engagement.button.title"
// Comment: "Title for the Freemium DBP Home Page Post Scan Engagement Promotion Button"
static let homePagePromotionFreemiumDBPPostScanEngagementButtonTitle = "View Results"
}
3 changes: 3 additions & 0 deletions DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ public struct UserDefaultsWrapper<T> {
case homePageCustomBackground = "home.page.custom.background"
case homePageLastPickedCustomColor = "home.page.last.picked.custom.color"

case homePagePromotionVisible = "home.page.promotion.visible"
case homePagePromotionDidDismiss = "home.page.promotion.did.dismiss"

case appIsRelaunchingAutomatically = "app-relaunching-automatically"

case historyV5toV6Migration = "history.v5.to.v6.migration.2"
Expand Down
8 changes: 6 additions & 2 deletions DuckDuckGo/DBP/DBPHomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ final class DBPHomeViewController: NSViewController {
private let pixelHandler: EventMapping<DataBrokerProtectionPixels> = DataBrokerProtectionPixelsHandler()
private var currentChildViewController: NSViewController?
private var observer: NSObjectProtocol?
private var freemiumDBPFeature: FreemiumDBPFeature

private let prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier
private lazy var errorViewController: DataBrokerProtectionErrorViewController = {
Expand Down Expand Up @@ -70,9 +71,12 @@ final class DBPHomeViewController: NSViewController {
})
}()

init(dataBrokerProtectionManager: DataBrokerProtectionManager, prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier = DefaultDataBrokerPrerequisitesStatusVerifier()) {
init(dataBrokerProtectionManager: DataBrokerProtectionManager,
prerequisiteVerifier: DataBrokerPrerequisitesStatusVerifier = DefaultDataBrokerPrerequisitesStatusVerifier(),
freemiumDBPFeature: FreemiumDBPFeature) {
self.dataBrokerProtectionManager = dataBrokerProtectionManager
self.prerequisiteVerifier = prerequisiteVerifier
self.freemiumDBPFeature = freemiumDBPFeature
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -94,7 +98,7 @@ final class DBPHomeViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()

if !dataBrokerProtectionManager.isUserAuthenticated() {
if !dataBrokerProtectionManager.isUserAuthenticated() && !freemiumDBPFeature.isAvailable {
assertionFailure("This UI should never be presented if the user is not authenticated")
closeUI()
}
Expand Down
34 changes: 15 additions & 19 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 freemiumDBPUserStateManager: FreemiumDBPUserStateManager

init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager,
featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler(),
pixelHandler: EventMapping<DataBrokerProtectionPixels> = DataBrokerProtectionPixelsHandler(),
userDefaults: UserDefaults = .standard,
subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(),
accountManager: AccountManager) {
accountManager: AccountManager,
freemiumDBPUserStateManager: FreemiumDBPUserStateManager) {
self.privacyConfigurationManager = privacyConfigurationManager
self.featureDisabler = featureDisabler
self.pixelHandler = pixelHandler
self.userDefaults = userDefaults
self.subscriptionAvailability = subscriptionAvailability
self.accountManager = accountManager
self.freemiumDBPUserStateManager = freemiumDBPUserStateManager
}

var isUserLocaleAllowed: Bool {
Expand All @@ -70,28 +72,24 @@ 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 DBP prerequisites
///
/// Prerequisites are satisified if either:
/// 1. The user is an active freemium user (e.g has activated freemium and is not authenticated)
/// 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
}
let isAuthenticated = accountManager.isUserAuthenticated
if !isAuthenticated && freemiumDBPUserStateManager.didActivate { return true }

func arePrerequisitesSatisfied() async -> Bool {
let entitlements = await accountManager.hasEntitlement(forProductName: .dataBrokerProtection,
cachePolicy: .reloadIgnoringLocalCacheData)
var hasEntitlements: Bool
Expand All @@ -102,8 +100,6 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature
hasEntitlements = false
}

let isAuthenticated = accountManager.accessToken != nil

firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: hasEntitlements, isAuthenticatedResult: isAuthenticated)

return hasEntitlements && isAuthenticated
Expand Down
16 changes: 15 additions & 1 deletion DuckDuckGo/DBP/DataBrokerProtectionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import BrowserServicesKit
import DataBrokerProtection
import LoginItems
import Common
import Freemium

public final class DataBrokerProtectionManager {

Expand All @@ -30,8 +31,16 @@ public final class DataBrokerProtectionManager {
private let authenticationManager: DataBrokerProtectionAuthenticationManaging
private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker()

private lazy var freemiumDBPFirstProfileSavedNotifier: FreemiumDBPFirstProfileSavedNotifier = {
let freemiumDBPUserStateManager = DefaultFreemiumDBPUserStateManager(userDefaults: .dbp)
let accountManager = Application.appDelegate.subscriptionManager.accountManager
let freemiumDBPFirstProfileSavedNotifier = FreemiumDBPFirstProfileSavedNotifier(freemiumDBPUserStateManager: freemiumDBPUserStateManager,
accountManager: accountManager)
return freemiumDBPFirstProfileSavedNotifier
}()

lazy var dataManager: DataBrokerProtectionDataManager = {
let dataManager = DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag)
let dataManager = DataBrokerProtectionDataManager(profileSavedNotifier: freemiumDBPFirstProfileSavedNotifier, pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag)
dataManager.delegate = self
return dataManager
}()
Expand Down Expand Up @@ -63,6 +72,7 @@ public final class DataBrokerProtectionManager {
}

extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate {

public func dataBrokerProtectionDataManagerDidUpdateData() {
loginItemInterface.profileSaved()
}
Expand All @@ -74,4 +84,8 @@ extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate {
public func dataBrokerProtectionDataManagerWillOpenSendFeedbackForm() {
NotificationCenter.default.post(name: .OpenUnifiedFeedbackForm, object: nil, userInfo: UnifiedFeedbackSource.userInfo(source: .pir))
}

public func isAuthenticatedUser() -> Bool {
isUserAuthenticated()
}
}
6 changes: 3 additions & 3 deletions DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public enum FeatureFlag: String {
case appendAtbToSerpQueries

// https://app.asana.com/0/1206488453854252/1207136666798700/f
case freemiumPIR
case freemiumDBP

case contextualOnboarding

Expand All @@ -54,8 +54,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .remoteReleasable(.subfeature(SslCertificatesSubfeature.allowBypass))
case .unknownUsernameCategorization:
return .remoteReleasable(.subfeature(AutofillSubfeature.unknownUsernameCategorization))
case .freemiumPIR:
return .remoteDevelopment(.subfeature(DBPSubfeature.freemium))
case .freemiumDBP:
return .remoteReleasable(.subfeature(DBPSubfeature.freemium))
case .phishingDetectionErrorPage:
return .remoteReleasable(.subfeature(PhishingDetectionSubfeature.allowErrorPage))
case .phishingDetectionPreferences:
Expand Down
Loading

0 comments on commit 45c1658

Please sign in to comment.