Skip to main content

Overview

Many apps use a paywall/subscription SDK (e.g., Superwall or RevenueCat) alongside Encore retention offers. This guide shows how to combine multiple sources into a single entitlement model for your app.
Core APIs used here: isActivePublisher, isActive, and EntitlementScope.

Purchase Routing

When using multiple subscription managers, you can route purchases inside your onPurchaseRequest handler based on product ID or configuration:
Encore.shared.onPurchaseRequest { purchaseRequest in
    if purchaseRequest.productId.hasPrefix("rc_") {
        // Route to RevenueCat
        let products = try await Purchases.shared.products([purchaseRequest.productId])
        guard let product = products.first else { return }
        try await Purchases.shared.purchase(product: product)
    } else {
        // Route to StoreKit directly
        guard let product = try await Product.products(for: [purchaseRequest.productId]).first else { return }
        let result = try await product.purchase()
    }
}
See onPurchaseRequest() for full handler documentation.

Superwall + RevenueCat

A minimal Combine setup that merges Superwall subscription state, RevenueCat entitlements, and Encore grants into one boolean.
import Foundation
import Encore
import Combine
import SuperwallKit
import RevenueCat

class EntitlementManager: NSObject, ObservableObject {
    @Published var hasProAccess: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    private let revenueCatStatusSubject = CurrentValueSubject<Bool, Never>(false)

    override init() {
        super.init()
        
        // 🔑 Set delegate so we get subscription updates
        Purchases.shared.delegate = self
        
        // Listen for RevenueCat events
        setupRevenueCatListener()

        // Combine IAP subscription status with Encore promotional rewards + Superwall
        Publishers.CombineLatest3(
            revenueCatStatusSubject.eraseToAnyPublisher(),
            Superwall.shared.$subscriptionStatus,
            Encore.shared.isActivePublisher(for: .freeTrial())
        )
        .map { revenuecatActive, superwallActive, encoreActive in
            // User has pro access if:
            // - They have any RevenueCat entitlements
            // - OR they have an active Superwall subscription
            // - OR they have an active Encore reward
            return revenuecatActive || (superwallActive != .inactive) || encoreActive
        }
        .assign(to: \.hasProAccess, on: self)
        .store(in: &cancellables)
    }
    
    private func setupRevenueCatListener() {
        // Fetch initial CustomerInfo
        Purchases.shared.getCustomerInfo { [weak self] customerInfo, error in
            guard let self = self else { return }
            if let customerInfo = customerInfo {
                self.updateRevenueCatStatus(from: customerInfo)
            } else if let error = error {
                print("Error fetching CustomerInfo: \(error)")
            }
        }
    }
    
    private func updateRevenueCatStatus(from customerInfo: CustomerInfo) {
        // Check for active entitlements
        let hasActiveEntitlement = !customerInfo.entitlements.active.isEmpty
        print("RevenueCat entitlements: \(customerInfo.entitlements.active.keys)")
        revenueCatStatusSubject.send(hasActiveEntitlement)
    }
}

// MARK: - PurchasesDelegate
extension EntitlementManager: PurchasesDelegate {
    func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
        print("RevenueCat CustomerInfo updated")
        updateRevenueCatStatus(from: customerInfo)
    }
}