If you’re developing iOS apps with in-app purchases, chances are you’ve seen this dreaded dialog and wondered why SKPaymentQueue isn’t calling your SKPaymentTransactionObserver.

In-App Purchase Dialog: This In-App Purchase has already been bought

“This In-App Purchase has already been bought. It will be restored for free.”

This scenario can easily occur when an error occurs before calling SKPaymentQueue.default().finishTransaction(transaction).

Apple has designed the payment queue in such a way as to enable apps to fulfill a purchase before completing - with finishTransaction being the indication that fulfillment is complete. In the case that a failure occurs during fulfillment, the app can recover when SKPaymentQueue calls the app’s observer again.

This is all good and fine - except many developers have encountered a situation where this process breaks down. Their SKPaymentTransactionObserver is never called again, and the user is presented with the dreaded “already bought” dialog and no apparent way to fix the problem.

Like many problems in software development, it turns out that the problem is quite simple and completely non-obvious. SKPaymentQueue does indeed call a transaction observer - just not the one in your app. A library (in my case Firebase analytics) is adding its own SKPaymentTransactionObserver before your app adds its own observer. As a result, SKPaymentQueue is calling that observer first, and by the time your observer is added, as far as the payment queue is concerned it has already delivered the notification and there’s nothing left to do.

To fix the problem, ensure that your app registers its own observer before any third party libraries get a chance to do the same:

    class AppDelegate: UIApplicationDelegate {

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {


            // ORDER DEPENDENCY: before analytics

            // ORDER DEPENDENCY: do this after store service setup!

            return true

That’s all!