alert confirming in-app purchase never appears - ios

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?

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.

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.

Related

Listen to when the in-app purchase window opens on iOS

SKPaymentQueue.default().add(payment)
I'm starting an in-app purchase with. But I think the purchase window sometimes opens late. Is there a method, delegate method to listen for the situation where this screen opens?
I researched this but I could not reach a conclusion, does anyone know?
You can use the delegate below to handle it (ex. show/hide progress view) regarding to SKPaymentTransactionState:
// Handle transaction status after you call .add(payment).
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(trans)
// purchased..
case .failed:
SKPaymentQueue.default().finishTransaction(trans)
// failed ..
case .restored:
SKPaymentQueue.default().finishTransaction(trans)
// restored
default:
break
}
}
}
}
I suggest using a very well made (and easy to user) library for handling in-app purchases. It is called SwiftyStoreKit. enter link description here We use it in many projects and it has nice closures while handling purchasing. You can put your UI blocking progress indicator just before calling it's methods and remove when the closure returns with a result.

iOS: A default alert will be showed after purchasing a product in-app purchase

I am struggling with a problem that after purchasing a product successfully and a system alert ("You're all set, Your purchase was successful") will be showed as well. The problem is that I am not able to get a callback or any event to know the system alert was dismissed in order to display a custom popup
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
// the default alert will be showed after purchasing a product successfully
break
case .failed:
break
case .restored:
break
default:
break
}
}
}
Note that: the system has prompted won't get a callback
I wanna show the below photo after users tapped the "Ok" button from the default alert. I am not able to get a callback or any event to know the default alert was dismissed in order to display my custom popup
Thank you so much for helping me

updatedTransactions(transactionState == .restored) vs paymentQueueRestoreCompletedTransactionsFinished

As the title describes, what is the actual different?
If I have this:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.transactionState == .purchased {
} else if transaction.transactionState == .failed {
} else if transaction.transactionState == .restored { // <- This one
}
}
Do I still need to use this (if yes, what code to use here, and what code in transactionState == .restored?:
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
}
When you start restoring transactions, any available transactions are presented to your payment queue observer with the .restored state. At this point you should restore the transaction; effectively processing it in the same way that you processed the initial purchase.
Depending on what transactions are available to be restored, you will get from 0 to n transactions presented (There may not be any transactions available for restoration).
Once all available transactions have been presented, you will get a call to paymentQueueRestoreCompletedTransactionsFinished. You will always get 1 call to this method for each time you call restoreCompletedTransactions.
Note that no details of the restored transactions are passed to this method. The purpose of this method is to allow you to perform any final housekeeping such as updating your UI.
For example, you could display a "Restoring" message when the user taps your restore button and hide that message when you get the call to paymentQueueRestoreCompletedTransactionsFinished

How to deal with old transactions when dealing with restoring purchases?

I'm currently developing and testing my app with a sandbox user. Even though the transactions have been completed, when I try to Restore Purchases, I get as much as 32 old transactions from the queue.
Essentially I would like to alert as in Your purchase is being restored. Upon completion this dialog will close. and dismiss it when it's finished.
private func showRestoreInProgressAlert() {
let alert = UIAlertController(title: "Restoring Purchase", message: "Your purchase history is being restored. Upon completion this dialog will close.", preferredStyle: .alert)
present(alert, animated: true, completion: nil)
NotificationCenter.default.addObserver(self, selector: #selector(dismissRestoreInProgressAlert(notification:)), name: SubscriptionService.restoreSuccessfulNotification, object: nil)
}
You may recognise this method below from SKPaymentTransactionObserver. Once the notification .restoreSuccessfulNotification has been sent, the alert would be dismissed as expected. But because there are 32 transactions in the queue, the popup keeps appearing and disappearing 32 times.
func handleRestoredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("Purchase restored for product id: \(transaction.payment.productIdentifier)")
queue.finishTransaction(transaction)
SubscriptionService.shared.uploadReceipt { (success) in
DispatchQueue.main.async {
NotificationCenter.default.post(name: SubscriptionService.restoreSuccessfulNotification, object: nil)
}
}
}
func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
handlePurchasingState(for: transaction, in: queue)
case .purchased:
handlePurchasedState(for: transaction, in: queue)
case .restored:
handleRestoredState(for: transaction, in: queue)
case .failed:
handleFailedState(for: transaction, in: queue)
case .deferred:
handleDeferredState(for: transaction, in: queue)
}
}
}
I already finish the transactions both in handlePurchasedState and handleRestoredState like this:
queue.finishTransaction(transaction)
So why do I still have so many old transactions sitting in the queue, whenever I click restore purchases?
UPDATE
This could be indeed an issue with the sandbox.
I tried to do a count but that doesn't help, simply because not all these transactions seem to be restorable.
I did the "hard-reset":
for transaction: AnyObject in SKPaymentQueue.default().transactions {
guard let currentTransaction: SKPaymentTransaction = transaction as? SKPaymentTransaction else {return}
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
}
Now it seems that slowly the transactions are reducing to zero, which means Sandbox is now losing them.
Why is SKPaymentQueue.default().finishTransaction() working, but queue.finishTransaction() didn't? Should I refactor my code to use SKPaymentQueue.default().finishTransaction() instead to be on the safe side? Or is that just a bad day with the IAP sandbox?
Restoration re-delivers all purchases to your transaction observer delegate. That is the intended behaviour.
However, displaying the alert in the way that you want is quite straight-forward.
When the user starts the restore operation, display the alert.
Then, once all items are restored, you will get a call to the paymentQueueRestoreCompletedTransactionsFinished delegate method. In this method you can dismiss your alert.
All active subscription and all non-consumable items (purchased by the current user) will be returned each time you request a restore.
Think about it, if they would not be returned anymore after you finish, how would you be able to restore?
Apparently your app can have multiple active subscriptions and/or non-consumables for a user. To avoid getting multiple alerts, you should combine all StoreKit restore callback into one (or a few) user notifications.
Does this make sense in your situation?

TouchID canceling should dismiss the view controller

For my app I need to save one page of it with TouchID. So the user is kinda forced to use TouchID or if the device does not support it, a password. If the user cancels the TouchID authentication, I want to View to disappear and get back to the root view. I already had that working, but somehow it does not work anymore and I really don't know why?! I just copied until the canceled option, the rest is does not matter I guess.
func authenticateUser() {
let context = LAContext()
var error: NSError?
let reasonString = "Authentication is needed to access your App"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error){
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success, policyError) -> Void in
if success {
print("authentification successful")
}
}else{
switch policyError!.code{
case LAError.SystemCancel.rawValue:
print("Authentification was canceled by the system")
case LAError.UserCancel.rawValue:
print("Authentication was canceled by user")
self.navigationController?.dismissViewControllerAnimated(true, completion: nil)
//Yes I also tried popToRootViewController, still doesn't work
}
The documentation for the evaluatePolicy call says:
"Reply block that is executed when policy evaluation finishes. This block is evaluated on a private queue internal to the framework in an unspecified threading context."
So the problem is that you are trying to call navigation from the wrong thread. You need to make that call on the UI thread. For example:
dispatch_async(dispatch_get_main_queue()) {
// Navigation goes here
}

Resources