Skip to main content

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โ€‹

RevenueCatAppActorNotes
Purchases.configure(withAPIKey:)AppActor.configure(apiKey:)Same concept, different API
Purchases.sharedAppActor.sharedSingleton access
OfferingsOfferingsSame hierarchy
OfferingOfferingNamed group of packages
PackagePackageType + product pair
CustomerInfoCustomerInfoEntitlement state
EntitlementInfoEntitlementInfoIndividual entitlement
StoreProductAppActorProductProduct 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โ€‹

FeatureRevenueCatAppActor
API styleCallback-based (with async wrappers)Async/await native
SwiftUIRequires delegate or CombinePayment mode: explicit fetch. Local mode: native ObservableObject
Local modeNot availableFully client-side option
DependenciesMultiple (incl. networking)Zero (Apple frameworks only)
Minimum iOS13.015.0
StoreKitStoreKit 1 + 2StoreKit 2 only
Remote ConfigSeparate productBuilt-in
ExperimentsSeparate productBuilt-in
ASA AttributionSeparate productBuilt-in

Server-Side Migrationโ€‹

  1. Webhooks โ€” Update your Apple/Google webhook URLs to point to AppActor
  2. API keys โ€” Generate new pk_* and sk_* keys from the AppActor dashboard
  3. Entitlements โ€” Recreate your entitlement mappings in AppActor
  4. Offerings โ€” Recreate your offerings configuration
  5. User data โ€” Contact AppActor support for assistance with historical data migration

Next Stepsโ€‹