[StoreKit 2] finish() does not durably remove an active subscription transaction on iOS 26 - same transactionId reappears in Transaction.unfinished

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

  1. 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?
  2. Is there a guaranteed way to durably remove it so it does not reappear?
  3. Can AppStore.sync() / background re-sync reintroduce an already-finished transaction?
  4. 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?
[StoreKit 2] finish() does not durably remove an active subscription transaction on iOS 26 - same transactionId reappears in Transaction.unfinished
 
 
Q