--- source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/subscription-architecture.mdc confidence: 0.9 namespace: work last_synced: 2026-04-28 alwaysApply: false --- # Subscription Architecture & Implementation ## Overview DuckDuckGo's subscription system provides access to premium features including VPN (Network Protection), Personal Information Removal (PIR), Identity Theft Restoration (ITR), and AI Chat. The system supports multiple purchase platforms and cross-platform activation. ## Core Architecture ### Shared Foundation: BrowserServicesKit All subscription logic is centralized in `BrowserServicesKit/Sources/Subscription/`: ```swift // ✅ CORRECT - Use BrowserServicesKit for core subscription logic import BrowserServicesKit final class SubscriptionViewModel: ObservableObject { private let subscriptionManager: SubscriptionManager init(subscriptionManager: SubscriptionManager = SubscriptionManager.shared) { self.subscriptionManager = subscriptionManager } } // ❌ INCORRECT - Don't duplicate subscription logic in platform code final class SubscriptionViewModel: ObservableObject { func checkSubscriptionStatus() { // Don't reimplement subscription logic } } ``` ### Platform-Specific Purchase Methods #### iOS - **Purchase Method**: App Store only (StoreKit) - **Geographic Coverage**: Global - **Cross-Platform**: Can activate Stripe subscriptions from other platforms #### macOS App Store Build - **Purchase Method**: App Store only (StoreKit) - **Geographic Coverage**: Global - **Cross-Platform**: Can activate Stripe subscriptions #### macOS Direct Download Build - **US Users**: Stripe web purchases - **Non-US Users**: Redirected to iOS App Store - **Cross-Platform**: Primary platform for Stripe purchases ### Version Management ALWAYS use V2 implementations for new code: ```swift // ✅ CORRECT - Use V2 implementations let subscriptionManager = SubscriptionManagerV2() let purchaseManager = StorePurchaseManagerV2() let purchaseFlow = AppStorePurchaseFlowV2() // ❌ INCORRECT - Don't use V1 implementations let subscriptionManager = SubscriptionManager() // Legacy let purchaseFlow = AppStorePurchaseFlow() // Legacy ``` ## Premium Features Implementation ### Feature Entitlements ```swift // ✅ CORRECT - Check entitlements through SubscriptionManager final class FeatureViewModel: ObservableObject { private let subscriptionManager: SubscriptionManager var isFeatureEnabled: Bool { subscriptionManager.hasEntitlement(for: .networkProtection) } var availableFeatures: [SubscriptionFeature] { subscriptionManager.entitlements.compactMap { entitlement in switch entitlement { case .networkProtection: return .vpn case .dataBrokerProtection: return .personalInformationRemoval case .identityTheftRestoration: return .identityTheftRestoration default: return nil } } } } ``` ### VPN Integration ```swift // ✅ CORRECT - VPN entitlement integration final class VPNManager: ObservableObject { private let subscriptionManager: SubscriptionManager func enableVPN() async { guard subscriptionManager.hasEntitlement(for: .networkProtection) else { await showSubscriptionPrompt() return } // Enable VPN functionality await startVPNConnection() } } ``` ### Personal Information Removal (PIR) ```swift // ✅ CORRECT - PIR implementation with freemium support final class PIRManager: ObservableObject { private let subscriptionManager: SubscriptionManager var isFreemiumEligible: Bool { // Check feature flag and eligibility FeatureFlags.shared.isEnabled(.freemiumPIR) && !subscriptionManager.isUserSubscribed && isUSUser } func performScan() async { if subscriptionManager.hasEntitlement(for: .dataBrokerProtection) { await performFullScan() } else if isFreemiumEligible { await performLimitedScan() } else { await showSubscriptionPrompt() } } } ``` ## Purchase Flow Implementation ### Free Trial Support ```swift // ✅ CORRECT - Free trial implementation final class SubscriptionPurchaseViewModel: ObservableObject { @Published var isTrialEligible = false @Published var trialPeriod: String = "" func checkTrialEligibility() async { guard FeatureFlags.shared.isEnabled(.privacyProFreeTrial) else { isTrialEligible = false return } // Check server-side eligibility let eligible = await subscriptionManager.checkFreshFreeTrialEligibility() await MainActor.run { isTrialEligible = eligible if let product = subscriptionManager.currentProduct, let offer = product.introductoryOffer { trialPeriod = offer.localizedPeriod } } } } ``` ### Platform-Specific Purchase ```swift // ✅ CORRECT - Platform-aware purchase flow final class PurchaseFlowCoordinator { private let subscriptionManager: SubscriptionManager func initiatePurchase() async { #if os(iOS) // iOS always uses App Store await purchaseViaAppStore() #elseif os(macOS) if Bundle.main.isMacAppStore { await purchaseViaAppStore() } else { // Direct download build if isUSUser { await purchaseViaStripe() } else { await redirectToiOSApp() } } #endif } } ``` ## Cross-Platform Activation ### URL Handling ```swift // ✅ CORRECT - Subscription URL handling final class SubscriptionURLHandler { func handleSubscriptionURL(_ url: URL) { guard url.scheme == "duckduckgo", url.host == "subscription" else { return } let components = URLComponents(url: url, resolvingAgainstBaseURL: false) if let token = components?.queryItems?.first(where: { $0.name == "token" })?.value { Task { await subscriptionManager.activateSubscription(with: token) } } } } ``` ### Authentication Bridge ```swift // ✅ CORRECT - V1 to V2 authentication migration final class AuthenticationManager { func migrateToV2() async { let bridge = SubscriptionAuthV1toV2Bridge() if let v1Token = await bridge.extractV1Token() { await subscriptionManager.migrateFromV1(token: v1Token) } } } ``` ## Testing Patterns ### Mock Subscription Manager ```swift // ✅ CORRECT - Mock for testing final class MockSubscriptionManager: SubscriptionManager { var mockEntitlements: [SubscriptionEntitlement] = [] var mockSubscriptionStatus: Bool = false override var isUserSubscribed: Bool { mockSubscriptionStatus } override var entitlements: [SubscriptionEntitlement] { mockEntitlements } override func hasEntitlement(for feature: SubscriptionFeature) -> Bool { mockEntitlements.contains(where: { $0.feature == feature }) } } ``` ### Test Subscription States ```swift // ✅ CORRECT - Test different subscription states final class SubscriptionViewModelTests: XCTestCase { private var viewModel: SubscriptionViewModel! private var mockManager: MockSubscriptionManager! func testSubscribedUser() { // Given mockManager.mockSubscriptionStatus = true mockManager.mockEntitlements = [.networkProtection, .dataBrokerProtection] // When viewModel.checkSubscriptionStatus() // Then XCTAssertTrue(viewModel.isSubscribed) XCTAssertTrue(viewModel.hasVPNAccess) XCTAssertTrue(viewModel.hasPIRAccess) } func testFreeTrialEligibility() async { // Given FeatureFlags.shared.enable(.privacyProFreeTrial) mockManager.mockTrialEligibility = true // When await viewModel.checkTrialEligibility() // Then XCTAssertTrue(viewModel.isTrialEligible) } } ``` ## Feature Flags and Configuration ### Subscription Feature Flags ```swift // ✅ CORRECT - Feature flag usage enum SubscriptionFeatureFlag: String, CaseIterable { case privacyProFreeTrial = "privacyProFreeTrial" case iosStripeSubscriptions = "iosStripeSubscriptions" case freemiumPIR = "DBPSubfeature.freemium" var isEnabled: Bool { FeatureFlags.shared.isEnabled(self) } } ``` ### Environment Configuration ```swift // ✅ CORRECT - Environment-based configuration extension SubscriptionEnvironment { static var `default`: SubscriptionEnvironment { #if os(iOS) // iOS always uses App Store return .appStore #elseif os(macOS) if Bundle.main.isMacAppStore { return .appStore } else { return FeatureFlags.shared.isEnabled(.iosStripeSubscriptions) ? .stripe : .appStore } #endif } } ``` ## Analytics and Tracking ### Subscription Pixels ```swift // ✅ CORRECT - Analytics implementation final class SubscriptionAnalytics { func trackPurchaseFlow(origin: SubscriptionFunnelOrigin) { PixelKit.fire( pixel: .subscriptionPurchaseFlowStarted, parameters: [ "origin": origin.rawValue, "platform": currentPlatform.rawValue ] ) } func trackTrialEligibility(eligible: Bool) { PixelKit.fire( pixel: .subscriptionTrialEligibilityCheck, parameters: [ "eligible": eligible.description ] ) } } ``` ## Important Implementation Notes ### Security Considerations - Store authentication tokens in Keychain only - Use HTTPS for all subscription API calls - Validate receipts server-side - Implement proper token refresh logic ### Performance Optimization - Cache subscription status locally - Use background queues for API calls - Implement offline capability for cached states - Minimize UI blocking operations ### User Experience - Provide clear trial information - Handle purchase failures gracefully - Support subscription restoration - Maintain consistent UI across platforms ### Common Pitfalls to Avoid - Don't hardcode subscription URLs - Don't bypass entitlement checks - Don't duplicate subscription logic across platforms - Don't ignore V1 to V2 migration paths - Don't forget to handle cross-platform activation