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
Create sandbox tester in App Store Connect
Sign in on device with sandbox account
Test purchases - all free in sandbox
Test restore - verify works correctly
Mock Client
class MockPaymentsClient : PaymentsClient {
// Simulates subscription states
// Perfect for UI testing
// No App Store Connect needed
}
Key Files
Component Location Protocol Packages/Payments/Sources/Payments/Protocols/PaymentsClient.swiftRevenueCat Packages/Payments/Sources/Payments/RevenueCat/Paywall UI Packages/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