While testing IAP before release, we hit a classic but very confusing issue:

  • User taps “Continue Purchase”
  • Button switches to “Processing”
  • A few seconds later it returns to “Continue Purchase”
  • No App Store purchase confirmation dialog appears

This article documents the full investigation path, key logs, root cause, and a practical fix strategy you can apply before launch.

1. Symptom

The product list loaded correctly on the purchase screen, but after tapping purchase:

  • purchase() was called
  • No system dialog appeared
  • UI eventually returned to the not-purchased state

At first glance, it looked like the purchase flow never started, but logs showed a different story.

2. First pass: verify whether the purchase flow actually runs

We added logs to all critical checkpoints in the purchase flow:

  • Button click parameters
  • purchaseSelectedProduct() entry and guard branches
  • Before/after product.purchase()
  • Transaction.updates
  • refreshEntitlements()
  • Transaction.currentEntitlements
  • Transaction.latest(for:)

Result:

  • purchase() returned .success
  • Transaction verification succeeded, and transaction.finish() was executed
  • But entitledIDs remained empty

This means: it was not “purchase didn’t start,” but “entitlements weren’t recognized after purchase.”

3. Key findings after deeper investigation

After extending logs, several decisive signals appeared:

  1. Transaction.currentEntitlements iteration count was 0
  2. Transaction.latest(for: monthly) had a valid record, but it was expired
  3. More importantly, old transactions existed in Transaction.unfinished before purchase
  4. The current purchase() returned a transaction from an old chain, not from a new interactive purchase flow

That explains why there was no popup:

  • StoreKit first returned a processable old transaction (unfinished / historical chain)
  • Your code finished it via finish()
  • The request looked “successful,” but it never went through the new purchase confirmation path users expect

4. Root cause summary

This wasn’t a single-point failure, but a combination of two factors:

  • Unfinished transactions interfering with the purchase flow
  • Sandbox subscription chain being in an expired state (even with a latest transaction, it may not be an active entitlement)

Final behavior: no popup, apparent success, entitlements still empty.

5. Fix strategy (validated in practice)

5.1 Proactively clear unfinished transactions before purchase

At app startup and before purchase, scan Transaction.unfinished and call finish() on verified transactions to prevent old transactions from hijacking the new purchase flow.

Keep these key logs enabled until release:

  • unfinished transaction snapshot
  • subscription status (state, renewal info)
  • currentEntitlements iteration count
  • latest(for:) transaction/expiration details

5.3 Use a dual-path entitlement check (optional hardening)

If currentEntitlements is temporarily empty in some environments, use latest(for:) as a fallback (while still validating active status).

6. Pre-launch IAP checklist

  1. Before each test, verify the sandbox account’s subscription state (expired or not)
  2. Log unfinished count on critical paths
  3. Test three scenarios: first purchase / renew after expiration / restore purchases
  4. Check consistency between currentEntitlements and latest
  5. If “no popup” happens, first check whether an old transaction path was hit

7. Conclusion

“Tap purchase with no popup” is not always a UI issue or a timing issue.
In StoreKit 2, transaction state, unfinished queue, and subscription lifecycle all affect final behavior.

Adding these logs and cleanup steps before launch can eliminate many IAP issues that are hard to reproduce in production.