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?
Related
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.
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
I have added auto renewable subscription to my iOS app. I have used a sandbox user to test the app and it worked fine. After that I logged out of the previous sandbox account and logged in with another sandbox account. Now my app sends receipts with two original transaction ids to validate from the server. It seems like my previous sandbox user data has not completely wiped off. Does anyone else experiencing the same issue? Any thoughts on this?
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(transaction as! SKPaymentTransaction)
if let completion = self.purchaseProductcompletion {
completion(PurchaseHandlerStatus.purchased, self.productToPurchase, trans)
}
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
let errorCode = (trans.error as? SKError)?.code
if (errorCode == .paymentCancelled) {
if let completion = self.purchaseProductcompletion {
completion(PurchaseHandlerStatus.purchaseCancelled, self.productToPurchase, trans)
}
} else {
if let completion = self.purchaseProductcompletion {
completion(PurchaseHandlerStatus.purchaseFailed, self.productToPurchase, trans)
}
}
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
totalRestoredPurchases += 1
default:
break
}
}
}
}
To avoid this annoying issue, you should finish all pending transactions from the old sandbox account.
When you debug in-app purchases and/or change Apple ID too often, some transactions may stay in the queue (for example, if you broke execution until transaction is finished). And these transactions will try to finish at every next app launch. In this case you may see a system alert prompting to enter your Apple ID password and this may also lead to sandbox receipt will not immediately update/ may not match logged in sandbox account.
To fix this issue, make sure you finish all pending transactions when launching the app.
SKPaymentQueue.default().finishTransaction(transaction)
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.
In-app purchase working in the sandbox environment but in Appstore version amount debited from user account but the content is not unlocked.
I don't think there is an issue in coding. otherwise, it will not work in the sandbox environment. I think it may possible that transaction receipt is nil.
When I click to purchase again that it shows "you have already subscribed". But still, it's not unlocking app content. Even I clicked to restore the purchase but its also not working. I surprised why everything is working in the sandbox environment.
Subscription type: auto-renewable.
Content unlocking: Audio, video and pdf tutorials .
I have checked backend log. API never executed and the only issue I fill is that I did not get app store receipt even if the user purchased successfully. everything working perfectly in sandbox environment.
Code:
public 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, queue: queue)
case .failed:
handleFailedState(for: transaction, in: queue)
case .deferred:
handleDeferredState(for: transaction, in: queue)
}
}
}
//On transaction state changed to purchased:
func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("User purchased product id: \(transaction.payment.productIdentifier)")
print("User purchased product id: \(transaction)")
queue.finishTransaction(transaction)
self.completeTransaction(transaction:transaction)
}
//In completeTransaction Method:
func completeTransaction(transaction:SKPaymentTransaction)
{
if let receiptURL = Bundle.main.appStoreReceiptURL,FileManager.default.fileExists(atPath: receiptURL.path)
{
let receipt:Data = try! Data(contentsOf: receiptURL)
let jsonObjectString = receipt.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))
var strIdentifier:String = transaction.transactionIdentifier!
if let identifier = transaction.original?.transactionIdentifier
{
strIdentifier = identifier
}
//API call to save receipt to validate later and unlock the content
//In case API calling failed then I stored data and called API again on app home page.
}
}
Please finish the transaction after you verify the receipt queue.finishTransaction(transaction)
apple also recommends that "Download all Apple-hosted content before finishing the transaction. After a transaction is complete, its download objects can no longer be used."
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html