Question

SDK 4.17.8: Correct way to @Publish up-to-date subscription status in SwiftUI using Swift concurrency

  • 20 March 2023
  • 1 reply
  • 255 views

Badge +3

I have an existing RevenueCat implementation in SwiftUI using the Purchases SDK 3.14.1 in production.

I am currently in the process of upgrading to the latest RevenueCat SDK 4.17.8 and am therefore reconsidering my design choices and looking for best practices / guidance on how to implement an ObservableObject for SwiftUI with Swift concurrency (async/await) using the latest RevenueCat SDK.

I have a single entitlement (“premium”) in my app for which I want to broadcast its subscription status through a @Published boolean variable in the ObservableObject, similar to the ‘MagicWeatherSwiftUI’ example provided by RevenueCat:

@MainActor
class PurchasesManager: NSObject, ObservableObject {
    static let shared = PurchasesManager()

    @Published var customerInfo: CustomerInfo? {
        didSet {
            subscriptionActive = customerInfo?.entitlements[Constants.RevenueCat.premiumEntitlement]?.isActive == true
        }
    }

    @Published var subscriptionActive = false

    private override init() {}

    // Other code ...
}

To update the customerInfo (and thereby the subscriptionActive variable through the didSet) I make use of the PurchasesDelegate as follows:

extension PurchasesManager: PurchasesDelegate {
    func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
        self.customerInfo = customerInfo
    }

    func purchases(_ purchases: Purchases, readyForPromotedProduct product: StoreProduct, purchase startPurchase: @escaping StartPurchaseBlock) {
        startPurchase { (transaction, customerInfo, error, cancelled) in
            if let customerInfo, error == nil, !cancelled {
                self.customerInfo = customerInfo
            }
        }
    }
}

What I am confused about is the following statements in the RevenueCat documentation:

The CustomerInfo object contains all of the purchase and subscription data available about the user. This object is updated whenever a purchase or restore occurs and periodically throughout the lifecycle of your app.

Whilst also stating:

The SDK will update the cache if it's older than 5 minutes, but only if you call getCustomerInfo(), make a purchase, or restore purchases, so it's a good idea to call getCustomerInfo() any time a user accesses premium content.

 

For the cases in which a purchase or restore is made, the result is clear: The PurchaseDelegate ‘receivedUpdated’ method will be called, the customerInfo updated and therefore the subscriptionActive boolean set. However, it is unclear what is meant with ‘periodically throughout the lifecycle of your app’. I noticed in the simulator that when the app moves between the background and foreground, and after the 5 minutes have passed, that the caches are cleared. I assume that in case something has changed in the caches / server-side, the PurchasesDelegate method will be called again, but this is not clear from the documentation.

Moreover, the documentation also clearly states that a call to ‘getCustomerInfo()’, i.e., ‘try await Purchases.shared.customerInfo()’ (when using the latest SDK concurrently), should be made to trigger an update to the cache and should therefore be used when checking subscription status instead. This implies perhaps that the PurchaseDelegate ‘receivedUpdate’ method will not be called periodically automatically after-all. That will then result in my current implementation and the ‘MagicWeatherSwiftUI’ example app being incomplete / incorrect.

If the latter is the case, why does the ‘MagicWeatherSwiftUI’ example app in SwiftUI not call the ‘customerInfo()’ method instead of using the current implementation with the ‘didSet’ method through the ‘customerInfo’ variable? Is the ‘didSet’ method to update the ‘subscriptionActive’ published boolean in combination with the PurchasesDelegate a correct implementation to keep the subscription status up-to-date throughout the lifecycle of the app?

Could you please provide some guidance, insights or best practices how to implement this common use-case for SwiftUI?

 

For reference, in my current implementation using the old v3 SDK I added a read-only variable ‘isPremiumActive’ to the ObservableObject that would call the ‘Purchases.shared.purchaseInfo’ method and in its closure return the (then updated) subscriptionActive variable again. This works fine, but means that there are now two variables inside the ObservableObject that would return the subscriptionStatus, whereas only the ‘isPremiumActive’ one would trigger an update if the assertion as mentioned about the lifecycle updating is correct. Only the ‘isPremiumActive’ method should then be called from models and views that need to check (and update) the status, whilst using the ‘subscriptionActive’ publisher in cases where the view needs to be updated using SwiftUI. This seems / feels like a wrong implementation. My old implementation is shown below for reference:

class PurchasesHelper: NSObject, ObservableObject {
    static let shared = PurchasesHelper()

    @Published var purchaserInfo: Purchases.PurchaserInfo? {
        didSet {
            subscriptionActive = purchaserInfo?.entitlements[Constants.premiumEntitlementId]?.isActive == true
        }
    }


    @Published var subscriptionActive: Bool = false

    var isPremiumActive: Bool {
        get {
            var premiumActive = false
            fetchPurchaserInfo {
                premiumActive = self.subscriptionActive
            }
            return premiumActive
        }
    }

    private override init() {}

    func fetchPurchaserInfo(completion: @escaping () -> Void) {
        Purchases.shared.purchaserInfo { (purchaserInfo, error) in
            self.purchaserInfo = purchaserInfo
            completion()
        }
    }

    // Other code ...
}

Thank you for your assistance.

Ramon


1 reply

Userlevel 6
Badge +8

Hey @FlyABB!

Generally, the mention of the delegate method being called periodically is referring to the cases where a purchase, or restore occurs, and when `getCustomerInfo` is called -- not that the delegate method is called on any sort of random timeline, i.e. it’s always invoked by another method.

I think in general your setup looks correct - you can implement an ObservableObject class with a Published var that’s updated when the `receivedUpdated` delegate method is called. You can probably avoid also setting the `customerInfo` property after the result of a promoted purchase (since the delegate method would be called anyway), but doesn’t hurt either.

Let me know if that helps!

Reply