#StackBounty: #ios #in-app-purchase #storekit #skpaymenttransaction SKPaymentTransaction's stuck in queue after finishTransaction c…

Bounty: 100

We’ve got an app that’s been rejected by apple a few times for being unable to complete an auto renewing IAP purchase, and being unable to restore if attempted. We’ve finally narrow down the errors by adding some extra logging, and noticed Payment added for transaction already in the SKPaymentQueue: ... in the logs.

While trying to reproduce we booted up a phone we hadn’t used for a while, and noticed that it had 27 transactions for the same purchase in the queue, all in the SKPaymentTransactionState.purchased state. Our SKPaymentTransactionObserver is notified of these transactions, and we do call finishTransaction on them. There are so many transactions, I’m assuming, because we have a monthly subscription which auto renews every 5 minutes in the sandbox, and we had been making many additional purchases of this same IAP with the same App Store account on another phone- so this phone is now being notified of all the updates.

The strange thing is that even though we call finishTransaction on these transactions, they seem to remain unfinished, which is why we see the "payment added for transaction already in queue" message in the console.

So to debug this I implemented the paymentQueue(_:removedTransactions:) of SKPaymentTransactionObserver, and noticed that even though we’re calling finishTransaction on so many transactions, we’d only see one be removed – and seemingly if we stayed in the app a long time just doing nothing a couple more would go through over the span of 10+ minutes.

In my frustration I went ahead and did something like this

    public func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
        let remainingTransactions = queue.transactions
        let hasRemainingTransactions = !remainingTransactions.isEmpty
        
        if hasRemainingTransactions {
            paymentQueue(queue, updatedTransactions: remainingTransactions)
        }
    }

Which of course is super gross but wouldn’t you know for each of the n transactions they were removed one by one- only ever one at a time.

So my first thought is, well, this probably isn’t super likely in real world scenarios where you’re purchasing over and over again, renewing so fast etc, so maybe the SDK doesn’t expect so many transactions completed at once? I tried something a little less gross and changed our paymentQueue(_:updatedTransactions:) from something like this

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

            case .restored:
                SKPaymentQueue.default().finishTransaction(transaction)
                
            case .failed:
                SKPaymentQueue.default().finishTransaction(transaction)
                
            default:
                break
            }
        }
        
    }

To something like this

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

The same code just every transaction processed not during the same runloop. What happened here was paymentQueue(_:removedTransactions:) was called with 1 removed transaction twice in a row, and then with the remaining 5 or so I had during this test run in a batch. So this "fixes" / works around the issue – but why?

So what’s happening here? Is this a sandbox quirk with the time it takes to finish transactions? Are we not expected to finish them all in the same run of the run loop? Am I just totally missing some core concept?

Environment wise the application is being built in Xcode 11.3.1, issue is most reproducible on iOS 13.6.1 and other iOS 13 versions, seems to happen but way less on iOS 12.0, have not seen it happen on iOS 14 betas. iOS SDK target is 11.0. We have only one IAP, an auto renewing monthly subscription.

While this work around seems to fix the issue, we’ve been in a rejection loop with Apple and would love to have a more solid understanding or reasoning for what’s happening before just throwing more builds at them hoping something sticks.


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.