I recently upgraded to Xcode 16.2 and now appear to be having issues with testing .subscriptionStatusTask using the simulator (I haven't tried the sandbox yet). According to Apple for .subscriptionStatusTask, "before a view modified with this method appears, a task will start in the background to get the subscription status. While the view is presented, the task will call action whenever the status changes or the task’s state changes". I took this to mean that I will automatically see when the status changes - ie. from grace period, to billing retry, to expired. When I had Xcode 15, this worked and my app would update and show these statuses correctly as I tested with transaction manager, both in my app AND in transaction manager.
Now in Xcode 16, according to release notes, there is a known issue with transaction manager and a supposed workaround ->
"The StoreKit Transaction Manager might be unresponsive when performing actions while an app is running in a debug session. The affected actions include: create a new purchase, send a purchase intent, and edit an active or expired subscription. (126700294) Workaround: Close the transaction manager, detach the app from the current debug session by stopping the process in Xcode, then re-open the transaction manager. You can open and use the app as normal on your device or simulator and perform actions in the transaction manager as long as there is no active debug session."
I've tried the workaround for transaction manager and it will proceed to show the next transaction in the cycle (for example, grace period) but it never progresses to "billing retry" either in the transaction manager or in my app. It stays on grace period no matter how many times I open and close transaction manager. So, now I'm not sure if .subscriptionStatusTask is even picking up status changes automatically anymore.
The following is a basic representation of my code for .subscriptionStatusTask for reference. There's more to it than this but basically, once a subscription status change is detected, it's reflected in my app and the user is informed if they need to do something (like re-subscribe). I tried to simplify the following code to make it more readable, etc. NOTE: Everything seemed to be working prior to Xcode 16.
.subscriptionStatusTask(for: "12345678", priority: TaskPriority.high) {
taskState in
switch taskState {
case .success(let statuses):
print("8888 Success...")
if let status = statuses.first {
if status != self.currentSubcriptionStatus {
self.currentSubcriptionStatus = status
print("8888 currentSubscriptionStatus2 = /(currentSubcriptionStatus?.state.localizedDescription)")
await handleSubscriptionStatusTask(taskState)
}
}
case .failure(let error):
print("8888 Failed... to fetch subscription status: \(error)")
model.isPro = false
case .loading:
print("8888 Loading...")
model.isPro = false
@unknown default:
print("8888 Unknown...")
model.isPro = false
}
}
func handleSubscriptionStatusTask(_ taskState: EntitlementTaskState<[Product.SubscriptionInfo.Status]>) async {
if let value = taskState.value {
print("value.count = \(value.count)")
//.. check to see if there was EVER a subscription; for .manageSubscriptionSheet presentation
if value.count > 0 {
everHadASubscription = true
} else {
everHadASubscription = false
}
let renewal: [()] = value.map { status in
let a = status.transaction
let b = try? a.payloadValue
if let myProdId = b?.productID {
if myProdId == "com.id.myapp.monthly" {
model.subscription = "monthly"
} else if myProdId == "com.id.myapp.yearly" {
model.subscription = "yearly"
} else {
model.subscription = "error with subscription."
}
} else {
model.subscription = "no subscription"
}
print("my model subscription is =>>> \(model.subscription)")
let renewalState = status.state
print("\nrenewalState (status.state) = \(renewalState)")
print("renewalState (status.state) localizedDescription = \(renewalState.localizedDescription)\n...\n")
switch renewalState {
case .revoked:
print("\nrenewalState = revoked")
model.isPro = false
case .expired:
print("\nrenewalState = expired")
model.isPro = false
case .subscribed:
print("\nrenewalState = subscribed")
model.isPro = true
case .inBillingRetryPeriod:
print("\nrenewalState = inBillingRetryPeriod")
//.. they shouldn't get app features unless grace period
model.isPro = false
case .inGracePeriod:
print("\nrenewalState = inGracePeriod")
model.isPro = true
default:
print("\nrenewalState = idk what the status is...")
model.isPro = false
}
}
}
}
What I want to know is:
- Is .subscriptionStatusTask intended to automatically detect subscription status changes so that our apps can take whatever action? That's what I interpreted Apple's doc to mean.
- Is the correct use/flow for StoreKit2 to ONLY use SubscriptionStoreView (to present the initial view for purchasing a brand new subscription), .manageSubscriptionSheet (presented when the user has or has ALREADY had a subscription), and .subscriptionStatusTask (for monitoring the status of a users subscriptions in real time)? In other words, are these 3 things the ONLY things we need to effectively implement StoreKit2 properly for auto-renewing subscriptions (ie. NOT needing to listen for Transactions and use transaction.currentEntitlements, transaction.updates, or transaction.finish)?
- Given this "bug" in Xcode16 with transaction manager, how can I accurately test StoreKit2 process/flow? Apple's workaround doesn't seem to actually work effectively and show a smooth transition while testing (at least not in the simulator).
Update on 2-9-25: I've now tested this in Sandbox and I can see that .subscriptionStatusTask is automatically detecting subscription status changes and reflecting them correctly in my app. So, this appears to me to be an issue with Xcode 16.2, the simulator, and the transaction manager. Furthermore, Apple's stated workaround does not appear to fix the flow/transition process for testing. For example, when running from the simulator, the app gets "stuck" on "grace period" and doesn't transition to "billing retry". It doesn't transition in the transaction manager and also, more importantly, .subscriptionStatusTask does NOT detect the change either. Only if I exit the app and come back in, does it notice the change and correctly display "billing retry" or "expired" depending on what I set in my .storekit configuration settings. In the previous version of Xcode, it did work. The bottom line for Xcode 16, is that you probably need to test in Sandbox to accurately see what's happening for StoreKit2 auto-renewing subscriptions.