SKPaymentTransaction's stuck in queue after finishTransaction called
Asked Answered
C

1

9

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.

Coffey answered 20/8, 2020 at 14:54 Comment(8)
Apple approved a build with my dispatch async work around- still not sure why / how this fixes anything, we’ve reached out to developer technical support and I’ll follow up here if i learn anything. I mean I guess this answers my question but doesn’t answer why and if this is expected or a sandbox issue?Coffey
Well they rejected us on the next submission for the same issue so still not resolved.Coffey
This smells like something weird is happening somewhere else. Can you post more code for context? Like your full implementation of the SKPaymentTransactionObserver?Culver
I have the same issue. Have you managed to resolve it yet @AndrewCarter ?Dettmer
I am facing the exact same problem. Help :(Burleigh
@Burleigh Have you tried my answer below?Braeunig
@BobdeGraaf Yes, I have followed it. In removedTransactions first I remove all transactions one by one and then call the receipt validation from here. In updateTransaction delegate I just call the finished transaction. I am still confused about this approach. Although It works. Please check this question for details. Thanks! #71458958Burleigh
@AndrewCarter Have you tried my solution? If it works for you, can you accept it as the answer for all future SO users who are looking for a solution?Braeunig
B
5

I believe I have a solution for this issue. I had exactly the same problem, Apple kept rejecting my App and I was already on 32 transactions in my Sandbox environment ;)

Somewhere else I saw that you can implement this method: 'removedTransactions'. In this method I found out that it is actually removing all the 32 transactions, but one at a time, and that it was super slow, like 1 sec. per transaction. And if I didn't wait with a new purchase before it was finished, then it always got stuck and never correctly started my new purchase! Here is the method and my logs:

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

So once I figured that out, I knew I just had to wait until the queue was empty before launching a new purchase. So this is the code I used for that:

DLog("Initiating purchase...")
while self.paymentQueue.transactions.count > 0 {
    DLog("Still busy removing previous transactions: \(self.paymentQueue.transactions.count)")
    sleep(2)
}
DLog("Payment queue is empty, let's buy!")
self.payment = SKPayment(product: product)
self.paymentQueue.add(self.payment!)
Braeunig answered 10/4, 2021 at 9:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.