Skip to content

Commit

Permalink
Subscription refactoring (#2842)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/72649045549333/1206805455884775/f
Tech Design URL:
https://app.asana.com/0/481882893211075/1207147511614062/f

Subscription refactoring for allowing unit testing.
- DI
- Removal of all singletons
- Removal of all static functions use
  • Loading branch information
federicocappelli authored May 22, 2024
1 parent 1b90ac6 commit bb5b7fb
Show file tree
Hide file tree
Showing 61 changed files with 946 additions and 524 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ Configuration/ExternalDeveloper.xcconfig
scripts/assets

DuckDuckGoTests/NetworkProtectionVPNLocationViewModelTests.swift*.plist
*.profraw
74 changes: 57 additions & 17 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "a49bbac8aa58033981a5a946d220886366dd471b",
"version" : "145.3.3"
"revision" : "b01a7ba359b650f0c5c3ab00a756e298b1ae650c",
"version" : "146.0.0"
}
},
{
Expand Down Expand Up @@ -183,7 +183,7 @@
{
"identity" : "trackerradarkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/TrackerRadarKit",
"location" : "https://github.com/duckduckgo/TrackerRadarKit.git",
"state" : {
"revision" : "c01e6a59d000356b58ec77053e0a99d538be56a5",
"version" : "2.1.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
<Test
Identifier = "AppTrackingProtectionListModelTests/testWhenNewChangesAreWrittenToTheDatabase_ThenTheSectionsPropertyIsUpdated()">
</Test>
<Test
Identifier = "AutoconsentMessageProtocolTests/testEval()">
</Test>
</SkippedTests>
</TestableReference>
<TestableReference
Expand Down
8 changes: 4 additions & 4 deletions DuckDuckGo/AppDelegate+Waitlists.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,14 @@ extension AppDelegate {
func checkWaitlists() {

#if NETWORK_PROTECTION
if vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist() {
if AppDependencyProvider.shared.vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist() {
checkNetworkProtectionWaitlist()
}
#endif
}

#if NETWORK_PROTECTION
private func checkNetworkProtectionWaitlist() {
let accessController = NetworkProtectionAccessController()

VPNWaitlist.shared.fetchInviteCodeIfAvailable { [weak self] error in
guard error == nil else {
return
Expand All @@ -65,7 +63,9 @@ extension AppDelegate {
func fetchVPNWaitlistAuthToken(inviteCode: String) {
Task {
do {
try await NetworkProtectionCodeRedemptionCoordinator().redeem(inviteCode)
try await NetworkProtectionCodeRedemptionCoordinator(accountManager:
AppDependencyProvider.shared.subscriptionManager.accountManager
).redeem(inviteCode)
VPNWaitlist.shared.sendInviteCodeAvailableNotification()
} catch {}
}
Expand Down
77 changes: 28 additions & 49 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,10 @@ import WebKit

// swiftlint:disable file_length
// swiftlint:disable type_body_length

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// swiftlint:enable type_body_length

private static let ShowKeyboardOnLaunchThreshold = TimeInterval(20)
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
// swiftlint:enable type_body_length

private static let ShowKeyboardOnLaunchThreshold = TimeInterval(20)
private struct ShortcutKey {
static let clipboard = "com.duckduckgo.mobile.ios.clipboard"
static let passwords = "com.duckduckgo.mobile.ios.passwords"
Expand All @@ -69,7 +66,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
#if NETWORK_PROTECTION
private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel()
private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults
lazy var vpnFeatureVisibility = DefaultNetworkProtectionVisibility()
#endif

private var autoClear: AutoClear?
Expand All @@ -91,9 +87,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

@UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil)
private var privacyConfigCustomURL: String?

@UserDefaultsWrapper(key: .privacyProEnvironment, defaultValue: SubscriptionPurchaseEnvironment.ServiceEnvironment.default.description)
private var privacyProEnvironment: String

var accountManager: AccountManaging {
AppDependencyProvider.shared.accountManager
}

// swiftlint:disable:next function_body_length cyclomatic_complexity
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Expand Down Expand Up @@ -223,7 +220,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
PixelExperiment.install()

// MARK: Sync initialisation

#if DEBUG
let defaultEnvironment = ServerEnvironment.development
#else
Expand Down Expand Up @@ -323,12 +319,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

#if NETWORK_PROTECTION
widgetRefreshModel.beginObservingVPNStatus()
NetworkProtectionAccessController().refreshNetworkProtectionAccess()
AppDependencyProvider.shared.networkProtectionAccessController.refreshNetworkProtectionAccess()
#endif

setupSubscriptionsEnvironment()

if vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist() {
if AppDependencyProvider.shared.vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist() {
clearDebugWaitlistState()
}

Expand Down Expand Up @@ -406,7 +400,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

private func presentExpiredEntitlementNotificationIfNeeded() {
let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator(
settings: VPNSettings(defaults: .networkProtectionGroupDefaults),
settings: AppDependencyProvider.shared.vpnSettings,
defaults: .networkProtectionGroupDefaults,
wrappee: NetworkProtectionUNNotificationPresenter()
)
Expand All @@ -431,21 +425,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}


private func setupSubscriptionsEnvironment() {
Task {
#if ALPHA || DEBUG
let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.staging
#else
let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.production
#endif
let environment = SubscriptionPurchaseEnvironment.ServiceEnvironment(rawValue: privacyProEnvironment) ?? defaultEnvironment
SubscriptionPurchaseEnvironment.currentServiceEnvironment = environment
VPNSettings(defaults: .networkProtectionGroupDefaults).selectedEnvironment = (environment == .production) ? .production : .staging
SubscriptionPurchaseEnvironment.current = .appStore
}
}

private func reportAdAttribution() {
guard AdAttributionPixelReporter.isAdAttributionReportingEnabled else { return }

Expand Down Expand Up @@ -526,53 +505,54 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

private func stopTunnelAndShowThankYouMessagingIfNeeded() {
if AccountManager().isUserAuthenticated {

if accountManager.isUserAuthenticated {
tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown = true
return
}

if vpnFeatureVisibility.shouldShowThankYouMessaging() && !tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown {
if AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowThankYouMessaging()
&& !tunnelDefaults.vpnEarlyAccessOverAlertAlreadyShown {
Task {
await self.stopAndRemoveVPN(with: "thank-you-dialog")
}
} else if vpnFeatureVisibility.isPrivacyProLaunched() && !AccountManager().isUserAuthenticated {
} else if AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched()
&& !accountManager.isUserAuthenticated {
Task {
await self.stopAndRemoveVPN(with: "subscription-check")
}
}
}

private func stopAndRemoveVPN(with reason: String) async {
let controller = NetworkProtectionTunnelController()
guard await controller.isInstalled else {
guard await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else {
return
}

let isConnected = await controller.isConnected
let isConnected = await AppDependencyProvider.shared.networkProtectionTunnelController.isConnected

DailyPixel.fireDailyAndCount(pixel: .privacyProVPNBetaStoppedWhenPrivacyProEnabled, withAdditionalParameters: [
"reason": reason,
"vpn-connected": String(isConnected)
])

await controller.stop()
await controller.removeVPN()
await AppDependencyProvider.shared.networkProtectionTunnelController.stop()
await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN()
}

func updateSubscriptionStatus() {
Task {
let accountManager = AccountManager()

guard let token = accountManager.accessToken else { return }

if case .success(let subscription) = await SubscriptionService.getSubscription(accessToken: token,
var subscriptionService: SubscriptionService {
AppDependencyProvider.shared.subscriptionManager.subscriptionService
}
if case .success(let subscription) = await subscriptionService.getSubscription(accessToken: token,
cachePolicy: .reloadIgnoringLocalCacheData) {
if subscription.isActive {
DailyPixel.fire(pixel: .privacyProSubscriptionActive)
}
}

_ = await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData)
await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData)
}
}

Expand Down Expand Up @@ -912,12 +892,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
@MainActor
func refreshShortcuts() async {
#if NETWORK_PROTECTION
guard vpnFeatureVisibility.shouldShowVPNShortcut() else {
guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else {
UIApplication.shared.shortcutItems = nil
return
}

if case .success(true) = await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .returnCacheDataDontLoad) {
if case .success(true) = await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .returnCacheDataDontLoad) {
let items = [
UIApplicationShortcutItem(type: ShortcutKey.openVPNSettings,
localizedTitle: UserText.netPOpenVPNQuickAction,
Expand Down Expand Up @@ -993,7 +973,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
presentNetworkProtectionStatusSettingsModal()
}

if vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist(), identifier == VPNWaitlist.notificationIdentifier {
if AppDependencyProvider.shared.vpnFeatureVisibility.shouldKeepVPNAccessViaWaitlist(), identifier == VPNWaitlist.notificationIdentifier {
presentNetworkProtectionWaitlistModal()
}
#endif
Expand All @@ -1012,7 +992,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate {

func presentNetworkProtectionStatusSettingsModal() {
Task {
let accountManager = AccountManager()
if case .success(let hasEntitlements) = await accountManager.hasEntitlement(for: .networkProtection),
hasEntitlements {
if #available(iOS 15, *) {
Expand Down
102 changes: 96 additions & 6 deletions DuckDuckGo/AppDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import BrowserServicesKit
import DDGSync
import Bookmarks
import Subscription
import Common
import NetworkProtection

protocol DependencyProvider {

Expand All @@ -41,7 +43,14 @@ protocol DependencyProvider {
var toggleProtectionsCounter: ToggleProtectionsCounter { get }
var userBehaviorMonitor: UserBehaviorMonitor { get }
var subscriptionFeatureAvailability: SubscriptionFeatureAvailability { get }

var subscriptionManager: SubscriptionManaging { get }
var accountManager: AccountManaging { get }
var vpnFeatureVisibility: DefaultNetworkProtectionVisibility { get }
var networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore { get }
var networkProtectionAccessController: NetworkProtectionAccessController { get }
var networkProtectionTunnelController: NetworkProtectionTunnelController { get }
var connectionObserver: ConnectionStatusObserver { get }
var vpnSettings: VPNSettings { get }
}

/// Provides dependencies for objects that are not directly instantiated
Expand All @@ -54,10 +63,7 @@ class AppDependencyProvider: DependencyProvider {
let variantManager: VariantManager = DefaultVariantManager()

let internalUserDecider: InternalUserDecider = ContentBlocking.shared.privacyConfigurationManager.internalUserDecider
lazy var featureFlagger: FeatureFlagger = DefaultFeatureFlagger(
internalUserDecider: internalUserDecider,
privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager
)
let featureFlagger: FeatureFlagger

let remoteMessagingStore: RemoteMessagingStore = RemoteMessagingStore()
lazy var homePageConfiguration: HomePageConfiguration = HomePageConfiguration(variantManager: variantManager,
Expand All @@ -72,9 +78,93 @@ class AppDependencyProvider: DependencyProvider {

let toggleProtectionsCounter: ToggleProtectionsCounter = ContentBlocking.shared.privacyConfigurationManager.toggleProtectionsCounter
let userBehaviorMonitor = UserBehaviorMonitor()

let subscriptionFeatureAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(
privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
purchasePlatform: .appStore)

// Subscription
let subscriptionManager: SubscriptionManaging
var accountManager: AccountManaging {
subscriptionManager.accountManager
}
let vpnFeatureVisibility: DefaultNetworkProtectionVisibility
let networkProtectionKeychainTokenStore: NetworkProtectionKeychainTokenStore
let networkProtectionAccessController: NetworkProtectionAccessController
let networkProtectionTunnelController: NetworkProtectionTunnelController

let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs)

let connectionObserver: ConnectionStatusObserver = ConnectionStatusObserverThroughSession()
let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults)

// swiftlint:disable:next function_body_length
init() {
featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider,
privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager)

// MARK: - Configure Subscription
let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)!
let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults)
vpnSettings.alignTo(subscriptionEnvironment: subscriptionEnvironment)

let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults,
key: UserDefaultsCacheKey.subscriptionEntitlements,
settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20)))
let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup)))
let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment)
let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment)
let accountManager = AccountManager(accessTokenStorage: accessTokenStorage,
entitlementsCache: entitlementsCache,
subscriptionService: subscriptionService,
authService: authService)
if #available(iOS 15.0, *) {
subscriptionManager = SubscriptionManager(storePurchaseManager: StorePurchaseManager(),
accountManager: accountManager,
subscriptionService: subscriptionService,
authService: authService,
subscriptionEnvironment: subscriptionEnvironment)
} else {
// This is used just for iOS <15, it's a sort of mocked environment that will not be used.
subscriptionManager = SubscriptionManageriOS14(accountManager: accountManager)
}

let subscriptionFeatureAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(
privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
purchasePlatform: .appStore)
let accessTokenProvider: () -> String? = {
func isSubscriptionEnabled() -> Bool {
if let subscriptionOverrideEnabled = UserDefaults.networkProtectionGroupDefaults.subscriptionOverrideEnabled {
#if ALPHA || DEBUG
return subscriptionOverrideEnabled
#else
return false
#endif
}
return subscriptionFeatureAvailability.isFeatureAvailable
}

if isSubscriptionEnabled() {
return { accountManager.accessToken }
}
return { nil }
}()
networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified),
serviceName: "\(Bundle.main.bundleIdentifier!).authToken",
errorEvents: .networkProtectionAppDebugEvents,
isSubscriptionEnabled: accountManager.isUserAuthenticated,
accessTokenProvider: accessTokenProvider)
networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager,
tokenStore: networkProtectionKeychainTokenStore)
networkProtectionAccessController = NetworkProtectionAccessController(featureFlagger: featureFlagger,
internalUserDecider: internalUserDecider,
accountManager: subscriptionManager.accountManager,
tokenStore: networkProtectionKeychainTokenStore,
networkProtectionTunnelController: networkProtectionTunnelController)
vpnFeatureVisibility = DefaultNetworkProtectionVisibility(
networkProtectionTokenStore: networkProtectionKeychainTokenStore,
networkProtectionAccessManager: networkProtectionAccessController,
featureFlagger: featureFlagger,
accountManager: accountManager)
}
}
Loading

0 comments on commit bb5b7fb

Please sign in to comment.