Reputation: 536027
I have the usual store kit queue observer code:
func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for t in transactions {
switch t.transactionState {
case .purchasing, .deferred: break // do nothing
case .purchased, .restored:
let p = t.payment
if p.productIdentifier == myProductID {
// ... set UserDefaults to signify purchase ...
// ... put up an alert thanking the user ...
queue.finishTransaction(t)
}
case .failed:
queue.finishTransaction(t)
}
}
}
The problem is what to do where I have the comment "put up an alert thanking the user". It seems simple enough: I'm creating a UIAlertController and calling present
to show it. But it sometimes doesn't appear!
The trouble seems to have something to do with the fact that the runtime puts up its own alert ("You're all set"). I don't get any notice of this so I don't know this is happening. How can I cause my UIAlertController to be presented for certain?
Upvotes: 1
Views: 329
Reputation: 1720
Just FYI. I implemented a similar behavior in my app but I didn't observer the issue Matt described in the question. My app showed an alert to describe the failure and suggested action when a transaction failed. Below is my code:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
...
case .failed:
let (reason, suggestion) = parsePaymentError(error: transaction.error)
SKPaymentQueue.default().finishTransaction(transaction)
if let purchaseFailureHandler = self.purchaseFailureHandler {
DispatchQueue.main.async {
purchaseFailureHandler(reason, suggestion)
}
}
}
}
}
I tested the code quite a few times with network connection error and user cancellation error. It worked perfectly for me.
Upvotes: 0
Reputation: 536027
You’ve put your finger on a serious issue of timing and information with regard to in-app purchases and StoreKit.
What’s going wrong here is that you (the store observer) receive paymentQueue(_:updatedTransactions:)
and at that moment two things happen simultaneously, resulting in a race condition:
The runtime puts up its “You’re all set” alert.
You try to put your UIAlertController (and kick off various other activities).
As you rightly say, you don’t get any event to tell you when the user has dismissed the runtime’s “You’re all set” alert. So how can you do something after that alert is over?
Moreover, if you try to put up your alert at the same time that the system is putting up its “You’re all set” alert, you will fail silently — your UIAlertController alert will never appear.
The solution is to recognize that while the system’s “you’re all set” alert is up, your app is deactivated. We can detect this fact and register to be notified when your app is activated again. And that is the moment when the user has dismissed the “You’re all set” alert!
Thus it is now safe for you to put up your UIAlertController alert.
Like this (uses my delay
utility, see https://stackoverflow.com/a/24318861/341994; vc
is the view controller we’re going to present the alert on top of):
let alert = UIAlertController( // ...
// ... configure your alert here ...
delay(0.1) { // important! otherwise there's a race and we can get the wrong answer
if UIApplication.shared.applicationState == .active {
vc.present(alert, animated:true)
} else { // if we were deactivated, present only after we are reactivated
var ob : NSObjectProtocol? = nil
ob = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil, queue: nil) { n in
NotificationCenter.default.removeObserver(ob as Any)
delay(0.1) { // can omit this delay, but looks nicer
vc.present(alert, animated:true)
}
}
}
}
I’ve tested this approach repeatedly (though with difficulty, because testing the store kit stuff works so badly), and it seems thoroughly reliable.
Upvotes: 4