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)
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
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
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?
I have 4 non-consumables in my application and the purchasing of these works without any problems.
However, I have created a restorePurchases button which restores all products regardless of whether the user has purchased them or not. I've tested this on 4 different sand box test users, and the results are consistent (i.e. for a test user who has never bought the non consumables, clicking 'restore' restores all products)
My restore code looks like:
#IBAction func restorePurchases(sender: UIButton) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
print("transactions restored")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "productAbc1":
defaults.setBool(true , forKey: "productAbc1")
case "productAbc2":
defaults.setBool(true , forKey: "productAbc2")
case "productAbc3":
defaults.setBool(true , forKey: "productAbc3")
case "productAbc3":
defaults.setBool(true , forKey: "productAbc4")
default:
print("IAP not setup; enable it")
}
}
}
Do I need to be checking additional parameters in paymentQueueRestoreCompletedTransactionsFinished? Comparing my code to many other similar questions / IAP examples, it looks pretty much the same. Is this an issue with the sandbox environment or code?
queue.finishTransaction(transaction)
for each case was missing; meaning they were never clearing from the queue. So the next time I attempted to restore the purchases, all previous transactions were found (and restoring).
Adding the above line of code resolved the issue.