Zhenyi Tan And a Dinosaur

Gotchas of Testing Subscriptions with Sandbox, Part 3

Here’s how to handle in-app purchase transactions in StoreKit:

  1. Implement SKPaymentTransactionObserver.
  2. Wait for paymentQueue(_:updatedTransactions:) to get called when transactions are updated.
  3. Loop through the transactions:
    1. If the transactionState is .purchased or .restored, unlock the in-app purchase.
    2. If the transactionState is .failed, show an error or something.
    3. Other states are less important, and you can ignore them if you want.
  4. Call finishTransaction(_:) on .purchased, .restored, and .failed transactions to remove them from the queue.

If you don’t call finishTransaction(_:), the transactions will remain in the queue and show up over and over again. But it seems like finished transactions don’t always get removed in the sandbox environment.

I noticed that the receipt validation endpoint on my server gets called every time I launch my app. But it’s only supposed to happen when I process the transactions in paymentQueue(_:updatedTransactions:). So I added a few print statements, and as I suspected, paymentQueue(_:updatedTransactions:) is getting called whenever I launch the app.

Obligatory.

Maybe there’s a bug in my code? So I simplified paymentQueue(_:updatedTransactions:) to this:

func paymentQueue(
    _ queue: SKPaymentQueue,
    updatedTransactions transactions: [SKPaymentTransaction]
) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchased, .restored:
            queue.finishTransaction(transaction)
        case .failed:
            queue.finishTransaction(transaction)
        default:
            break
        }
    }
}

Nope, the transactions still keep coming back.

Obligatory.

Next, I implemented paymentQueue(_:removedTransactions:) to confirm that the transactions are getting removed from the queue:

func paymentQueue(
    _ queue: SKPaymentQueue,
    removedTransactions transactions: [SKPaymentTransaction]
) {
    print("Removed \(transactions.count)")
    print("Remaining \(queue.transactions.count)")
}

They were removed, but come back after a relaunch for some reason.

Obligatory.

And no, I didn’t register the observer twice: I start observing in application(_:didFinishLaunchingWithOptions:) and remove the observer in applicationWillTerminate(_:).

I’m not really sure what’s going on? I just hope that this is one of the gotchas with subscriptions and sandbox and doesn’t happen in production.