Hey y'all,
I'm reaching out because of an observed issue that I am experience both in sandbox and in production environments. This issue does not occur when using the Local StoreKit configurations.
For context, my app only implements auto-renewing subscriptions. I'm trying to track with my own analytics every time a successful purchase is made, whether in the app or externally through Subscription Settings. I'm seeming too many events for just one purchase.
My app is observing Transaction.updates. When I make a purchase with Product.purchase(_:), I successfully handle the purchase result. After about 10-20 seconds, I receive 2-3 new transactions in my Transaction.updates, even though I already handled and finished the Purchase result. This happens on production, where renewals are one week. This also happens in Sandbox, where at minimum renewals are every 3 minutes.
The transactions do not differ in transactionId, revocationDate, expirationDate, nor isUpgraded... so not sure why they're coming in through Transaction.updates if there are no "updates" to be processing.
For purchases made outside the app, I get the same issue. Transaction gets handled in updates several times.
Note that this is not an issue if a subscription renews. I use `Transaction.reason
I want to assume that StoreKit is a perfect API and can do no wrong (I know, a poor assumption but hear me out)... so where am I going wrong?
My current thought is a Swift concurrency issue. This is a contrived example:
// Assume Task is on MainActor
Task(priority: .background) { @MainActor in
for await result in Transaction.updates in {
// We suspend current process,
// so will we go to next item in the `for-await-in` loop?
// Because we didn't finish the first transaction, will we see it again in the updates queue?
await self.handle(result)
}
}
@MainActor
func handle(result) async {
...
await Analytics.sendEvent("purchase_success")
transaction.finish()
}
Thank you for your reply and going over the exploration path to find the issue. That's an excellent and very insightful breakdown. I just checked the SKDemo's approach of await transaction.finish() for subscriptions in the process method is the correct pattern. But then again. I’m not an expert in subscriptions nor that API, hope someone from that team can jump in this thread as I believe, unlike consumables or non-consumables, where a single transaction often directly grants an entitlement, subscriptions have a lifecycle. They can renew, expire, be upgraded, downgraded.
Looking at that sample I believe this is why the SKDemo suggests: "Check the subscription status each time before unlocking a premium subscription feature."
This is the robust and recommended pattern for handling StoreKit 2 subscriptions. It separates the concerns of transaction receipt from entitlement management, leading to a more reliable and maintainable purchase flow in your app.
Great work on the digging and connecting the dots! But if you think the documentation is not good enough, I would recommend to request and enhance for that documentation, I’m sure that team will be more than happy to improve that documentation.
Once you file the request, please post the FB number here.
If you're not familiar with how to file enhancement requests, take a look at Bug Reporting: How and Why?
Albert Pascual Worldwide Developer Relations.