Skip to main content

Presenting Offers with RevenueCat

Present Encore retention offers when users dismiss your RevenueCat paywall without converting.

Option 1: Using PaywallView

If you’re using RevenueCat’s PaywallView component, use the onRequestDismiss callback to trigger Encore:
import RevenueCat
import RevenueCatUI
import Encore

PaywallView(
    placement: "your_placement",
    params: [String: Any]?,
    paywallOverrides: PaywallOverrides?,
    onRequestDismiss: { paywallInfo, paywallResult in
        // User dismissed without purchasing - trigger Encore
        Encore.placement().show()
    },
    feature: (() -> Void)?
)

Option 2: Using presentPaywallIfNeeded Modifier

If you’re using the .presentPaywallIfNeeded modifier, use the onDismiss callback:
import RevenueCat
import RevenueCatUI
import Encore

YourView()
    .presentPaywallIfNeeded(
        presentationMode: {.fullScreen, .sheet, .default},
        onDismiss: {
            // User dismissed without purchasing - trigger Encore
            Encore.placement().show()
        }
    )
Use the same fluent API documented here: placement()

Tracking Entitlements with RevenueCat

Simple async check

Combine Encore’s entitlement status with your current RevenueCat state for a single evaluation:
// Assumes you already have a current CustomerInfo instance (delegate or fetched)
Task {
  let hasEncoreTrial = await Encore.shared.isActive(.freeTrial())
  let hasProAccess = hasEncoreTrial || !customerInfo.entitlements.active.isEmpty
}

Reactive check (Combine)

To keep hasProAccess updated automatically, create a publisher for RevenueCat events and combine with Encore’s publisher:
import Foundation
import Encore
import Combine
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.CombineLatest(
            revenueCatStatusSubject.eraseToAnyPublisher(),
            Encore.shared.isActivePublisher(for: .freeTrial())
        )
        .map { revenuecatActive, encoreActive in
            // User has pro access if:
            // - They have any RevenueCat entitlements
            // - OR they have an active Encore reward
            return revenuecatActive || 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)
    }
}
The RevenueCat status is determined by checking if customerInfo.entitlements.active is not empty, which indicates the user has at least one active entitlement.