Transaction.currentEntitlements is not consistent

I've recently published an app, and while developing it, I could always get consistent entitlements from Transaction.currentEntitlements. But now I see some inconsistent behaviour for a subscribed device in the AppStore version. It looks like sometimes the entitlements do not emit value for the subscriptions.

It usually happens on the first couple tries when the device goes offline, or on the first couple tries when the device goes online. But it also happens randomly at other times as well.

Can there be a problem with Transaction.currentEntitlements when the connectivity was just changed?

Of course my implementation may also be broken. I will give you the details of my implementation below.

I have a SubscriptionManager that is observable (irrelevant parts of the entity is omitted):

final class SubscriptionManager: NSObject, ObservableObject {
    private let productIds = ["yearly", "monthly"]
    private(set) var purchasedProductIDs = Set<String>()
    
    var hasUnlockedPro: Bool {
        return !self.purchasedProductIDs.isEmpty
    }

    @MainActor
    func updatePurchasedProducts() async {
        var purchasedProductIDs = Set<String>()
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }
            if transaction.revocationDate == nil {
                purchasedProductIDs.insert(transaction.productID)
            } else {
                purchasedProductIDs.remove(transaction.productID)
            }
        }

        // only update if changed to avoid unnecessary published triggers
        if purchasedProductIDs != self.purchasedProductIDs {
            self.purchasedProductIDs = purchasedProductIDs
        }
    }
}

And I call the updatePurchasedProducts() when the app first launches in AppDelegate, before returning true on didFinishLaunchingWithOptions as:

Task(priority: .high) {
            await DependencyContainer.shared.subscriptionManager.updatePurchasedProducts()
        }

You may be wondering maybe the request is not finished yet and I fail to refresh my UI, but it is not the case. Because later on, every time I do something related to a subscribed content, I check the hasUnlockedPro computed property of the subscription manager, which still returns false, meaning the purchasedProductIDs is empty.

You may also be curious about the dependency container approach, but I ensured by testing multiple times that there is only one instance of the SubscriptionManager at all times in the app.

Which makes me think maybe there is something wrong with Transaction.currentEntitlements

I would appreciate any help regarding this problem, or would like to know if anyone else experienced similar problems.

It actually never happens if I wait 5 seconds or longer between killing the app and relaunching, but it is happening if I only wait 2-3 seconds before relaunching it. Maybe there is a problem with emitting values from Transaction.currentEntitlements when an app is killed and relaunched right after, sometimes.

/// Sorry, it's not answer for your issue... ///

What I've not seen in your transaction:

///Always finish a transaction.
await transaction.finish()

Next code looks like extra:

if purchasedProductIDs != self.purchasedProductIDs { self.purchasedProductIDs = purchasedProductIDs }

I use this, Set uses only unique values:

if transaction.revocationDate == nil {
    self.purchasedSubscriptions.insert(subscription)
} else {
    self.purchasedSubscriptions.remove(subscription)
}

Typically, Task is used as follows:

Task(priority: .background) {}

The value var hasUnlockedPro: Bool is better stored in @AppStorage.

This issue can be reproduced even with Apple's sample project from https://developer.apple.com/documentation/storekit/in-app_purchase/implementing_a_store_in_your_app_using_the_storekit_api

Just buy 1 non-consumable IAP and spam re-run app (with 1-2 seconds breaks), 1 in 10 times you will have issue like yours, where it cannot even read Transaction.currentEntitlements.

I noticed that when this bug occurs, there is also bug with UIDevice.current.identifierForVendorand AppStore.deviceVerificationID, both have temporary changed values (only for this bugged run, they get back to old values when you re-run app and there is no bug).

Probably the source of the bug is underlying bug with identifiers that are used to verify transactions.

In my experience, it's entirely unreliable.

I have employed a combination of the StoreKit API (async update streams from your example) and the SwiftUI modifiers (.currentEntitleTask(for: ProductId)) to manage the state of just a single subscription and even that's not enough.

I can refund a subscription and it won't register on the update stream. Then I'll re-run the app and I can no longer re-subscribe because the updates don't come through at all. Deleting transactions does nothing at all.

This is just local dev with Xcode and the StoreKit Transaction Manager. I have no idea what the production experience would be like.

Apple's examples exhibit the same issues. Every third party tutorial on the internet (including the one you seem to have drawn inspiration from) exhibits the same issues.

I have no idea how anyone properly implements in-app purchases without bugs.

It seems impossible.

All I want is something like this:

StoreKit
  .forDummies
  .activeSubscriptionsPublisher
  .map(\.productID)
  .assign(to: &$activeSubscriptionProductIds)

😭

Transaction.currentEntitlements is not consistent
 
 
Q