Skip to main content
Full technical details in your project at /docs/modules/Payments.md

What You Get

  • RevenueCat wrapper - Clean abstraction, no RC types leak
  • Reactive state - AsyncStream for subscription changes
  • Purchase flows - Buy, restore, cancel with full error handling
  • Entitlement system - Single “pro” entitlement (expandable)
  • Paywall UI - Beautiful, themeable subscription screen
  • Thread-safe - Multi-subscriber support with state replay
Time saved: 16-24 hours of RevenueCat integration, state management, paywall UI, and testing.

Key Components

PaymentsClient Protocol

protocol PaymentsClient: Sendable {
    func configure(_ config: PaymentsConfig) async
    func purchase(productID: String) async throws
    func restorePurchases() async throws
    func currentState() async -> PaymentsState
    func subscriptionStates() -> AsyncStream<PaymentsState>
}

PaymentsState

struct PaymentsState: Sendable {
    let isSubscribed: Bool
    let expirationDate: Date?
    let activeEntitlementIDs: Set<String>
    let willRenew: Bool
}

Production Setup (From Real Code)

Here’s how the boilerplate actually sets up payments in CompositionRoot.swift:
// 1. Create RevenueCat client
let revenueCatClient = Payments.RevenueCatClient()

// 2. Configuration happens at app launch via AppDelegate
// Config comes from Secrets.xcconfig:
let paymentsConfig = PaymentsConfig(
    apiKey: ProcessInfo.processInfo.environment["REVENUECAT_API_KEY"]!,
    entitlementID: ProcessInfo.processInfo.environment["RC_ENTITLEMENT_ID"] ?? "pro"
)

revenueCatClient.configure(paymentsConfig)

// 3. Inject into composition
self.paymentsClient = revenueCatClient

// RevenueCat automatically:
// - Syncs with App Store
// - Tracks subscription status
// - Handles cross-platform receipts
// - Emits state changes via AsyncStream

Configuration Files

Config/Secrets.xcconfig:
# From RevenueCat Dashboard → Project Settings → API Keys
REVENUECAT_API_KEY = appl_YOUR_KEY

# Entitlement ID from RevenueCat Dashboard
RC_ENTITLEMENT_ID = pro
Complete setup: RevenueCat Setup Guide

Subscription Flow

Purchase

// In PaywallViewModel
func purchase(productID: String) async {
    do {
        try await paymentsClient.purchase(productID: productID)
        // Success! State updates automatically
    } catch {
        // Handle error (cancelled, failed, etc.)
    }
}

Restore

// Restore previous purchases
func restorePurchases() async {
    do {
        try await paymentsClient.restorePurchases()
        // Subscriptions restored
    } catch {
        // Handle error
    }
}

Check Entitlement

// Check if user has pro access
let state = await paymentsClient.currentState()
if state.isSubscribed {
    // Show premium features
} else {
    // Show paywall
}

Paywall UI

Beautiful paywall included in FeatureSettings:
// In SettingsView
if !viewModel.isSubscribed {
    Button("Go Premium") {
        showPaywall = true
    }
}
.sheet(isPresented: $showPaywall) {
    PaywallView(viewModel: paywallViewModel)
}

Customization Examples

Add New Subscription Tier

// 1. Create product in App Store Connect
// 2. Import to RevenueCat
// 3. Update paywall UI to show new tier

// In PaywallView, add:
PricingCard(
    name: "Basic",
    price: "$4.99",
    period: "month",
    features: [
        "100 messages/day",
        "GPT-3.5 Turbo"
    ],
    action: { 
        await viewModel.purchase(productID: "basic_monthly") 
    }
)

Add Usage Limits

// Check remaining usage
func canSendMessage() async -> Bool {
    let state = await paymentsClient.currentState()
    
    if state.isSubscribed {
        return true  // Unlimited for pro
    } else {
        let count = await messageRepository.todayCount()
        return count < 20  // Free tier limit
    }
}

Custom Entitlements

// Multiple entitlements
if state.activeEntitlementIDs.contains("pro") {
    // Pro features
}

if state.activeEntitlementIDs.contains("premium_support") {
    // Priority support
}

Testing

Sandbox Testing

  1. Create sandbox tester in App Store Connect
  2. Sign in on device with sandbox account
  3. Test purchases - all free in sandbox
  4. Test restore - verify works correctly

Mock Client

class MockPaymentsClient: PaymentsClient {
    // Simulates subscription states
    // Perfect for UI testing
    // No App Store Connect needed
}

Key Files

ComponentLocation
ProtocolPackages/Payments/Sources/Payments/Protocols/PaymentsClient.swift
RevenueCatPackages/Payments/Sources/Payments/RevenueCat/
Paywall UIPackages/FeatureSettings/Sources/FeatureSettings/Views/PaywallView.swift

Dependencies

  • Core - Error handling, logging

Used By

  • FeatureSettings - Paywall and subscription management
  • Profile - Subscription status display
  • All features - Entitlement checking

Best Practices

  • Show clear pricing
  • Include terms and privacy links
  • Handle cancellation gracefully
  • Provide restore option
  • Test thoroughly in sandbox
  • Check server-side (if possible)
  • Cache locally for offline
  • Update on app launch
  • Observe state changes
  • Make paywall beautiful
  • Highlight value proposition
  • Show feature comparison
  • Easy to dismiss
  • Clear cancellation policy

Learn More

Test Coverage

82%+ - Comprehensive subscription testing

Build with AI (fast)

You can customize this module in minutes using our ready-to-paste LLM prompts.

Example Prompt

Context: Packages/FeatureSettings/** Prompt: “Add a toggle in Settings to show/hide a discounted annual plan on the paywall. Update tests to verify pricing visibility.” See in project: docs/modules/Payments.md Tests include:
  • Purchase flows
  • Restore purchases
  • Entitlement checking
  • State management
  • Error scenarios
  • Subscription expiry
I