On iOS 26 in Production (StoreKit 2), a transaction we have already finished keeps reappearing in Transaction.unfinished on later launches. We call await Transaction.finish() and confirm in the same session (by re-reading Transaction.unfinished) that it is removed - but on a later cold launch the SAME transactionId is yielded again. This makes our unfinished-purchase recovery UI fire repeatedly for customers who are already active, paying subscribers.
Related to existing thread 792933 (same symptom; iOS 18.4-18.5 and iOS 26). Feedback Assistant: FB23736625 (sysdiagnose attached).
Environment
- iOS 26.x (mostly 26.5). Negligible on iOS 17/18. Production. StoreKit 2.
- Built with Xcode 26.3. Not device-specific (iPhone 12-17). Seen in two apps.
What we observe
- Reappearing transaction: SAME transactionId, active (future expiresDate), not revoked, transactionReason = PURCHASE. Verified via App Store Server API (Get Transaction Info, Production).
- Immediately after finish(), the transaction is gone from Transaction.unfinished (verified by re-query). It reappears only on a later cold launch / sign-in. (We have not confirmed whether AppStore.sync() also triggers it.)
- Affects both non-original and first-purchase (id == originalId) transactions.
Scale (2-day analytics)
- 1,456 occurrences, 1,078 users. 230 users (21%) hit it 2+ times (up to 11).
- Among repeat users, 224/230 (97%) show the identical transactionId every time - i.e., re-presentation of the same finished transaction, not new ones.
How we finish (verified transactions, awaited)
// Enumerated from Transaction.unfinished (launch) and Transaction.updates (long-lived task).
private func finishAndVerify(_ transaction: Transaction) async {
await transaction.finish() // awaited
if await isStillUnfinished(transaction.id) == false { return } // confirmed GONE here
try? await Task.sleep(nanoseconds: 1_000_000_000)
await transaction.finish() // retry once
// Even after eviction is confirmed above, the SAME transactionId
// is yielded again by Transaction.unfinished on a later cold launch.
}
private func isStillUnfinished(_ txId: UInt64) async -> Bool {
for await result in Transaction.unfinished {
let id: UInt64
switch result {
case .verified(let t): id = t.id
case .unverified(let t, _): id = t.id
}
if id == txId { return true }
}
return false
}
Questions
- For an active auto-renewable subscription, is the current transaction expected to be re-presented in Transaction.unfinished across launches even after finish()? If so, what is the intended handling?
- Is there a guaranteed way to durably remove it so it does not reappear?
- Can AppStore.sync() / background re-sync reintroduce an already-finished transaction?
- Is this a regression in iOS 26?
Notes
- Not reproducible on demand; observed only in Production analytics across many users. sysdiagnose available (attached to FB23736625).
- The app also links legacy SKPaymentQueue (StoreKit 1) for older flows - could dual SK1/SK2 usage affect finished-state persistence?