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.

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)
    }
}