xcodestorekit

StoreKit2: Current subscription and listening for updates


I'm currently hooking up StoreKit2 to my app. I worked my way through an older example that listened to products and added/removed ids as they came up. This example fell short using the StoreKit config file as all subscriptions don't have a revocation date set so my list of purchased ids just grew longer (as they weren't removed).

My app is only using a single subscription group and thus will only have one subscription live at once. I'm currently grabbing the current subscription using for await result in Transaction.currentEntitlements and I'm showing the subscriptions with StoreView(ids: arrayIds) and SubscriptionStoreView(groupID: subscriptionGroupID) depending where the user is at in my app. Currently when those views disappear I grab the current subscription again (thus updating it if it was changed). That seems cumbersome especially if it doesn't need to be updated and, with this way, I'm now getting the following error: Making a purchase without listening for transaction updates risks missing successful purchases. Create a Task to iterate Transaction.updates at launch.

I'm wondering if there's a way to just listen to the StoreKit Subscription ID or currentEntitlements in my main App entry point thus making the final error disappear and maybe, if I could update the type of subscription, removing my onDisappear subscription fetches throughout the rest of the app.

All the examples I come across seem to be either tied up with SwiftData (WWDC 2023) or older and maybe not applicable anymore. Everything seems to be embedded in StoreKit2 and it would be lovely if I could listen in one simple spot for the in-app and outside-app subscription changes.

Thanks :)


Solution

  • The next thing I think about is reading your code error message. I use such an approach in my StoreCoordinator:

    //MARK: LISTENER
    ///This functionality is responsible for listening for updates on App Store Connect or a local StoreKit file,
    ///which could occur on devices separate from the one you're on
    ///(i.e. if a family member upgrades to a family plan or when a guardian or bank approves a pending purchase,
    ///the app will listen for that update and automatically update your availability).
    private func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            ///Iterate through any transactions that don't come from a direct call to `purchase()`.
            for await result in SKTransaction.updates {
                do {
                    let transaction = try self.checkVerified(result)
                    
                    ///Deliver products to the user.
                    await self.updateCustomerProductStatus()
                    
                    ///Always finish a transaction.
                    await transaction.finish()
                } catch {
                    ///StoreKit has a transaction that fails verification. Don't deliver content to the user.
                    print("Transaction failed verification")
                }
            }
        }
    }
    
    typealias SKTransaction = StoreKit.Transaction
    
    public func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        ///Check whether the JWS passes StoreKit verification.
        switch result {
            ///StoreKit parses the JWS, but it fails verification.
        case .unverified:           
            throw SKError.failedVerification
            ///The result is verified. Return the unwrapped value.
        case .verified(let safe):   
            return safe
        }
    }
    

    ... and last code with currentEntitlements I use when update product state:

     //MARK: GET UPDATE
    @MainActor
    private func updateCustomerProductStatus() async {
        
        var purchasedSubscriptions: [Product] = []
        
        ///Iterate through all of the user's purchased products.
        for await result in SKTransaction.currentEntitlements {
            do { ... }
    

    ... Code example implementation where I use SubscriptionView:

            .onAppear {
            Task {
                //When this view appears, get the latest subscription status.
                await updateSubscriptionStatus()
            }
        }
        .onChange(of: store.purchasedSubscriptions) { _ in
            Task {
                //When `purchasedSubscriptions` changes, get the latest subscription status.
                await updateSubscriptionStatus()
            }