Files

10 KiB

source, confidence, namespace, last_synced, alwaysApply
source confidence namespace last_synced alwaysApply
~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/subscription-architecture.mdc 0.9 work 2026-04-28 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/:

// ✅ 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:

// ✅ 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

// ✅ 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

// ✅ 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)

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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