Migrating from RevenueCat
This guide helps you migrate your iOS app from RevenueCat to AppActor. The concepts are similar โ offerings, packages, entitlements โ but the API surface is different.
Concept Mappingโ
| RevenueCat | AppActor | Notes |
|---|---|---|
Purchases.configure(withAPIKey:) | AppActor.configure(apiKey:) | Same concept, different API |
Purchases.shared | AppActor.shared | Singleton access |
Offerings | Offerings | Same hierarchy |
Offering | Offering | Named group of packages |
Package | Package | Type + product pair |
CustomerInfo | CustomerInfo | Entitlement state |
EntitlementInfo | EntitlementInfo | Individual entitlement |
StoreProduct | AppActorProduct | Product wrapper |
Purchases.logIn() | AppActor.shared.logIn(newAppUserId:) | User identity |
Purchases.logOut() | AppActor.shared.logOut() | Create new anon user |
Step-by-Step Migrationโ
1. Replace the SDKโ
Remove RevenueCat:
// Remove from Package.swift or Xcode SPM
// - .package(url: "https://github.com/RevenueCat/purchases-ios.git", from: "4.0.0")
Add AppActor:
// Add to Package.swift or via Xcode
.package(url: "https://github.com/appactor/appactor-ios.git", from: "1.0.0")
2. Update Configurationโ
Before (RevenueCat):
import RevenueCat
Purchases.logLevel = .debug
Purchases.configure(withAPIKey: "appl_xxxxx")
After (AppActor):
import AppActor
AppActor.configure(
apiKey: "pk_live_your_key_here",
options: .init(logLevel: .debug)
)
3. Update User Identityโ
Before:
Purchases.shared.logIn("user_123") { customerInfo, created, error in
// Handle result
}
After:
try await AppActor.shared.identify(appUserId: "user_123")
// or
try await AppActor.shared.logIn(newAppUserId: "user_123")
4. Update Offeringsโ
Before:
Purchases.shared.getOfferings { offerings, error in
if let current = offerings?.current {
let monthly = current.monthly?.storeProduct
}
}
After:
let offerings = try await AppActor.shared.offerings()
let monthly = offerings.current?.monthly
let price = monthly?.localizedPrice
5. Update Purchasesโ
Before:
Purchases.shared.purchase(package: package) { transaction, customerInfo, error, userCancelled in
if userCancelled { return }
if let error = error { /* handle */ }
if customerInfo?.entitlements["premium"]?.isActive == true {
// Unlock
}
}
After:
let result = try await AppActor.shared.purchase(package: package)
switch result {
case .success(let customerInfo):
if customerInfo?.hasActiveEntitlement("premium") == true {
// Unlock
}
case .pendingServerValidation:
// Receipt queued for automatic retry
break
case .failedServerValidation(let errorCode, let message, let requestId):
print("Validation failed [\(errorCode ?? "unknown")] requestId=\(requestId ?? "n/a")")
print(message ?? "")
break
case .cancelled:
break
case .pending:
break
}
6. Update Entitlement Checksโ
Before:
Purchases.shared.getCustomerInfo { customerInfo, error in
if customerInfo?.entitlements["premium"]?.isActive == true {
// Premium
}
}
After:
let info = try await AppActor.shared.customerInfo(forceRefresh: true)
if info.hasActiveEntitlement("premium") {
// Premium
}
7. Update Restoreโ
Before:
Purchases.shared.restorePurchases { customerInfo, error in
// Handle
}
After:
let info = try await AppActor.shared.restorePurchases()
8. Update SwiftUI Data Flowโ
Before:
// RevenueCat uses a delegate or Combine publisher
Purchases.shared.delegate = self
After:
// Payment mode: fetch explicitly and store in @State
struct ContentView: View {
@State private var info: AppActorServerCustomerInfo?
var body: some View {
Group {
if info?.hasActiveEntitlement("premium") == true {
PremiumView()
} else {
PaywallView()
}
}
.task {
info = try? await AppActor.shared.customerInfo(forceRefresh: false)
}
}
}
If you're migrating to local mode instead of payment mode, AppActor.shared remains an ObservableObject and you can observe customerInfo directly:
struct LocalModeContentView: View {
@ObservedObject var appActor = AppActor.shared
var body: some View {
if appActor.customerInfo.entitlements["premium"]?.isActive == true {
PremiumView()
} else {
PaywallView()
}
}
}
Key Differencesโ
| Feature | RevenueCat | AppActor |
|---|---|---|
| API style | Callback-based (with async wrappers) | Async/await native |
| SwiftUI | Requires delegate or Combine | Payment mode: explicit fetch. Local mode: native ObservableObject |
| Local mode | Not available | Fully client-side option |
| Dependencies | Multiple (incl. networking) | Zero (Apple frameworks only) |
| Minimum iOS | 13.0 | 15.0 |
| StoreKit | StoreKit 1 + 2 | StoreKit 2 only |
| Remote Config | Separate product | Built-in |
| Experiments | Separate product | Built-in |
| ASA Attribution | Separate product | Built-in |
Server-Side Migrationโ
- Webhooks โ Update your Apple/Google webhook URLs to point to AppActor
- API keys โ Generate new
pk_*andsk_*keys from the AppActor dashboard - Entitlements โ Recreate your entitlement mappings in AppActor
- Offerings โ Recreate your offerings configuration
- User data โ Contact AppActor support for assistance with historical data migration
Next Stepsโ
- Quickstart โ Set up the payment flow end-to-end
- API Authentication โ Configure key usage and security