matt
matt

Reputation: 536027

alert confirming in-app purchase never appears

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

Answers (2)

rayx
rayx

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

matt
matt

Reputation: 536027

The Problem

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

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

Related Questions