How to properly restore purchases using IAP in Swift - ios

I'm trying to display an alert to the user when purchases are restored. But when i debug and print out the number of restored purchases i get 0 transactions were restored. I don't understand why it would bring back 0. I thought i only needed to call to restoreCompletedTransactions() method. I post a notification to notify me if restoration has completed and i doesn't even reach this point. I'm using
paymentQueueRestoreCompletedTransactionsFinished(_ pQueue: SKPaymentQueue) to notify me when restoration is done. How can i properly restore the purchases.
let paymentQueue = SKPaymentQueue.default()
func restorePurchases() {
if !self.canMakePurchases {
return
}
self.paymentQueue.add(self)
self.paymentQueue.restoreCompletedTransactions()
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
print("Restore failed")
}
func paymentQueue(_ pQueue: SKPaymentQueue, updatedTransactions pTransactions: [SKPaymentTransaction]) {
for scanTransaction in pTransactions {
switch scanTransaction.transactionState {
case .purchasing:
break
case .purchased:
NotificationCenter.default.post(name: .AppDelegateUserHasPurchasedProductNotification, object: self)
pQueue.finishTransaction(scanTransaction)
default:
pQueue.finishTransaction(scanTransaction)
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ pQueue: SKPaymentQueue) {
print("Received restored transactions: \(pQueue.transactions.count)")
for scanTansaction in pQueue.transactions {
switch scanTansaction.transactionState {
case .restored:
NotificationCenter.default.post(name: .AppDelegateUserHasRestoredPurchasesNotification, object: self)
pQueue.finishTransaction(scanTansaction)
default:
break
}
}
}
The Logs
Received restored transactions: 0

Your implementation of
func paymentQueue(_ pQueue: SKPaymentQueue, updatedTransactions pTransactions: [SKPaymentTransaction]) {
is wrong. You have left out the transaction state for when a purchase is restored! You have case .purchased but you forgot case .restored. Put it in. That is where you are notified and can respond.

Related

Will Apple reject application if I call receipt validation after finishing all pending transaction inside removeTransaction delegate?

I am developing Auto-Renewable In-App Purchase. Right now I am calling the Receipt Validation function inside of updatedTransactions delegate like this :
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
self.IAPResponseCheck(iapReceiptValidationFrom: .purchaseButton)
KeychainWrapper.standard.set(false, forKey: receiptValidationAllow)
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
self.IAPResponseCheck(iapReceiptValidationFrom: .restoreButton)
KeychainWrapper.standard.set(false, forKey: receiptValidationAllow)
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
onBuyProductHandler?(.failure(error))
} else {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
}
PrintUtility.printLog(tag: String(describing: type(of: self)), text: "IAPError: \(error.localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing: break
#unknown default: break
}
}
}
First I am calling the Receipt Validation function where I am simply getting all the previous transactions list and calculating expiration dates and purchase dates to unlock my premium features from the lastest Info Receipt response array. In this function, I am checking the Purchase Status according to my logic and returning true or false. If it's true I take the user inside of my app and if it's false I take him to the purchase screen.
Then I am finishing the transaction immediately like this:
SKPaymentQueue.default().finishTransaction(transaction)
But what I have noticed is that If the user has a long transaction list (100+), It takes a long time to finish all the transactions. I print the finished transactions and remain transactions in the removedTransactions delegate like this:
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
PrintUtility.printLog(tag: String(describing: type(of: self)), text: "Removed transactions: \(transactions.count)")
PrintUtility.printLog(tag: String(describing: type(of: self)), text: "Unfinished transaction: \(queue.transactions.count)")
}
The problem is If I try to restore or purchase a new product before finishing all pending transactions it triggers updatedTransactions weirdly. It works fine If I wait till it finishes all transactions one by one. So my question is, If I call receipt validation inside of removedTransactions delegate, and finish each transaction inside updateTransactions delegate will it be considered as a possible reason for app rejection on Apple?
Finally, It will look like this:
updatedTransactions delegate:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
totalPurchaseOrRestoreFailed += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing: break
#unknown default: break
}
}
}
removedTransactions delegate:
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
print("Removed transactions: \(transactions.count)")
print("Unfinished transaction: \(queue.transactions.count)")
//This will be called after finishing all transactions
if queue.transactions.count == 0 {
if totalPurchaseOrRestoreFailed != 0 {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:break
case .restored: break
case .failed:
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
onBuyProductHandler?(.failure(error))
} else {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
}
print("IAP Error:", error.localizedDescription)
totalPurchaseOrRestoreFailed = 0
}
case .deferred, .purchasing: break
#unknown default: break
}
}
} else {
self.IAPResponseCheck(iapReceiptValidationFrom: .purchaseAndRestoreButton)
UserDefaults.standard.set(false, forKey: "receiptValidationAllow")
}
}
}
I believe the issue is exactly like in this SO question:
SKPaymentTransaction's stuck in queue after finishTransaction called
The problem isn't that Apple rejects your App because you finish each transaction inside updateTransactions, it's because Apple's framework introduces bugs if you do, and the payment simply doesn't work. It's important to realize that real users will never have this issue, because they won't have 100+ subscriptions. Because only in debugging/simulators a year-long-subscription will be considered as a 1 hour-subscription. So the solution is easy, just start the payment after the queue is empty. It's just a bug in Apple's framework, and real users won't have an issue. This is the code that I'm using, and Apple has not rejected my App since I used this:
while self.paymentQueue.transactions.count > 0 {
DLog("Still busy removing previous transactions: \(self.paymentQueue.transactions.count)")
delay(1) {
self.checkTransactions()
}
}
self.startPaymentRequest()

No receipt data for subscribers of auto-renewing subscription in UK?

I'm getting sporadic reports of users who purchase my auto-renewing subscription and there is no receipt data on their device. After the purchase is made, I parse the receipt and save the expire date into UserDefaults. Then whenever you open the app I check the expire date against today and if its later, you can access my content.
I've created a gist with my relevant code that parses the receipt and saves the expireDate that is kicked off in my appDelegate's handle purchase:
extension AppDelegate: SKPaymentTransactionObserver {
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)
}
}
}
func handlePurchasingState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("User is attempting to purchase product id: \(transaction.payment.productIdentifier)")
}
func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("User purchased product id: \(transaction.payment.productIdentifier)")
queue.finishTransaction(transaction)
SubscriptionService.shared.uploadReceipt { (success) in
DispatchQueue.main.async {
//self.updateWatchContext()
NotificationCenter.default.post(name: SubscriptionService.purchaseSuccessfulNotification, object: nil)
}
}
}
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 handleFailedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
queue.finishTransaction(transaction) //The RW tutorial did NOT finish the transaction, but iOS Slack said I should
print("Purchase failed for product id: \(transaction.payment.productIdentifier) on \(String(describing: transaction.transactionDate)) and this transaction is currently listed as \(transaction.transactionState) because of this error \(String(describing: transaction.error))")
}
func handleDeferredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("Purchase deferred for product id: \(transaction.payment.productIdentifier)")
}
}
A few users have told me that they cannot unlock the content even though they subscribed (these users also said they had purchased the annual subscription, I also have a monthly option)...I had one user install a beta version with a Firebase integration so that I could push the receipt and expire date to the console to see what it says and it appears that there is no expire date or receipt on his device. Any thoughts on what the issue is and why this seems to only be the case in the UK? My active user base is roughly 600 and I haven't heard of this issue outside of the UK.

IAP Autorenenewable subscription purchase returns status as failed in observer but says successful in the pop up

extension IAPService:SKPaymentTransactionObserver{
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions{
print(transaction.transactionState.status(),transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing :break
case .purchased : NotificationCenter.default.post(name: Notification.Name(rawValue: "purchased"), object: self)
receiptValidation()
default :queue.finishTransaction(transaction)
}
}
}
}
When I try purchasing a subscription it works fine sometimes but then after a period it says failed in status log. But still the pop up from apple says purchase successful you are all set. Is there some kind of limit we can use a sandbox account buy and test the app?

What should happen when non-purchased user press the restore purchase button in iOS?

My sandbox test account can purchase non-consumable item and restore it. Everything works. However, if the account have not purchased the item before, pressing the restore button does nothing. I see nothing in the debug panel. I'm expecting iOS to detect if a certain user has purchased the item or not, if not then display a message asking them to buy it. Does it work like that or the current behavior is totally acceptable?
Here is the restore purchase code (Swift) connecting to a button inside the main storyboard:
#IBAction func restoreButtonPressed(sender: UIButton) {
statusLabel.text = "Status: Restoring Purchase"
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
Other implemented methods include:
Works for normal purchase
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {}
Works for normal restore
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {}
Never see messages coming from this method before
func paymentQueue(queue: SKPaymentQueue!, restoreCompletedTransactionsFailedWithError error: NSError!) {}
Thanks!
You can check if the queue has any returned transactions, and if not it means that there are no purchases to restore:
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
if queue.transactions.count == 0 {
let alert = UIAlertView()
alert.title = "Oops"
alert.message = "There are no purchases to restore, please buy one"
alert.addButtonWithTitle("Buy")
alert.addButtonWithTitle("Cancel")
alert.show()
}
}
you can try this :
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing: print("Payment is being processed")
case .purchased: print("USER PURCHASED YOUR PRODUCT")
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
if transaction.original?.transactionIdentifier != nil { // this identifier uniquely identifies a completed tansaction.
// queue.restoreCompletedTransactions()
print("THE BUYER BOUGHT THIS PRODUCT BEFORE")
} else {
/*
display UIAlertController showing an error
there is no receipts, user never purchased this product.
*/
}
case .failed:
self.dismiss(animated: true, completion: nil)
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("pending")
#unknown default:
break
}
}
}

Restore InApp purchase using swift, iOS

I am implementing restore in app purchase. I have a button whose action is
#IBAction func restorePurchases(send : AnyObject){
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
// if (success) {
// I want to do something here
// }
}
My question is.
Is this the right way to restore?
How can we verify success action for restoring purchases?
don't forget to check if you can make a payment:
if (SKPaymentQueue.canMakePayments()) {
SKPaymentQueue.default().restoreCompletedTransactions()
}
for check if the restore was good you have to follow the protocol:
SKPaymentTransactionObserver
and then implement the method:
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!)
and subscribe to the event by doing:
SKPaymentQueue.default().add(self)
Finally here is an exemple of how I check the result:
func paymentQueue(_ queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
print("Received Payment Transaction Response from Apple");
for transaction in transactions {
switch transaction.transactionState {
case .purchased, .restored:
print("Purchased purchase/restored")
SKPaymentQueue.default().finishTransaction(transaction as SKPaymentTransaction)
break
case .failed:
print("Purchased Failed")
SKPaymentQueue.default().finishTransaction(transaction as SKPaymentTransaction)
break
default:
print("default")
break
}
}
}
For some reason, even after trying SKPaymentQueue.default().restoreCompletedTransactions() the following function was never called for me during restoration.
func paymentQueue(queue: SKPaymentQueue!,
updatedTransactions transactions: [AnyObject]!)
This line of code SKPaymentQueue.default().add(self) was added to make my class the observer and still no updates.
It seems that this function gets called with the restored scenario if you try to make a payment all over again and the StoreKit API automatically decides if this should be purchased or restored and shows the message accordingly.
Even thought from a user's point of view, they are not charged again, Apple rejected our app because the restore scenario was not added.
So it seems if you call SKPaymentQueue.default().restoreCompletedTransactions() in your code, handle these additional StoreKit delegates to manage your restoration:
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
handleSuccess()
}
func paymentQueue(_ queue: SKPaymentQueue,
restoreCompletedTransactionsFailedWithError error: Error) {
handleError()
}

Resources