Swift: IAP updatedTransactions not called on .Purchased - ios

I have a problem with my code. The function updatedTransactions is only called once while the transaction is .Purchasing and is not called after the transaction has ben completed.
func buyProduct(product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction)
switch (transaction.transactionState) {
case .Purchased, .Restored:
print("Completed")
complete(transaction)
break
case .Failed:
fail(transaction)
break
default:
break
}
}
}

Sorry I might answer a bit late, but it might be related to this question. Are you testing your IAP from Xcode when it doesn't reach the .purchased state?
If yes, then you need to set your device's App Store ID to one you have created on iTunes Connect (User menu, Sandbox testers).
If no, then maybe it could be because your SKPaymentQueue already contains too many transactions waiting to be finished. You can check it with SKPaymentQueue.default().transactions.
In my case I had 28 transactions waiting to be finished because I had a bad switch case statement during the purchase phase.
If this is the case, you can add those lines in your viewDidLoad to finish each one of them (don't forget to remove them after you find why they didn't finish in your code):
for transaction: AnyObject in SKPaymentQueue.default().transactions {
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
}

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()

Confusion with notifying the UI view that the purchase was completed

I am following the tutorial for IAP from the following site:
https://betterprogramming.pub/set-up-your-swiftui-app-to-support-in-app-purchases-ef2e0a11d10c
Here is the portion of the code that handles the transaction and the states:
extension IAPManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
purchasePublisher.send(("Purchased ",true))
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
purchasePublisher.send(("Restored ",true))
case .failed:
if let error = transaction.error as? SKError {
purchasePublisher.send(("Payment Error \(error.code) ",false))
print("Payment Failed \(error.code)")
}
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("Ask Mom ...")
purchasePublisher.send(("Payment Diferred ",false))
case .purchasing:
print("working on it...")
purchasePublisher.send(("Payment in Process ",false))
default:
break
}
}
}
}
In short, it checks each case on the queue for each buy request you placed with the server and sends the status back to the SwiftUI interface through the purchasePublisher PassThruSubject that looks like this:
let purchasePublisher = PassthroughSubject<(String, Bool), Never>()
That is the part I am confused about!? How do I access the purchasePublisher so that I can check on the SwiftUI view (SwiftUI interface) that the purchase was in fact completed successfully so that then I can take action accordingly?
purchasePublisher is an instance of PassthroughSubject, which is a publisher that broadcasts information to subscribers. You must go through this instance to access those subscriptions and you can do that with the sink method:
purchasePublisher.sink(receiveValue: { (value) in
print(value)
})
And there are, as always, caveats. You may find something like this useful for what you're doing: .send() and .sink() do not seem to work anymore for PassthroughSubject in Xcode 11 Beta 5

How to detect when the alert pop of In-App Purchase closed and completed purchase

I implemented In-App Purchase but can't detect when the user closed the alert and completed purchase. I want to check them and if they purchased an item, I want to show the log "purchased" on console.
Now case .purchased : in func paymentQueue was always through even the user doesn't purchase anything....
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing :
break
case .purchased :
// always through here even user doesn't purchase items
completeTransaction(transaction: transaction)
print("purchased")
case .failed :
failedTransaction(transaction: transaction)
case .restored :
restoreTransaction(transaction: transaction)
case .deferred :
deferredTransaction(transaction: transaction)
}
}
}

IAPs are being restoring when they have not yet been purchased yet

When the user calls restorePurchases(), the non-consumable com.premium is restored even thought they do not own it.
Here are the functions that are responsible for the restoring purchases and purchasing IAPs. This is only an issue with non-consumable IAPs.
There is no issue with purchasing. And if the user attempts to purchase an IAP that they already have, it is simply restored. Thank you for looking in to this.
func restorePurchases() {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("transactions restored")
for transaction in queue.transactions {
let t: SKPaymentTransaction = transaction
let prodID = t.payment.productIdentifier as String
print("starting restoring")
switch prodID {
case "com.premium":
print("restoring ads")
removeAds()
case "com.cash":
print("we dont restore coins")
case "com.level":
print("we dont restore levels")
default:
print("can't restore")
}
}
Here is my payment queue also.
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("add paymnet")
for transaction:AnyObject in transactions {
let trans = transaction as! SKPaymentTransaction
print(trans.error)
switch trans.transactionState {
case .purchased:
print("buying, ok unlock iap here")
print(p.productIdentifier)
let prodID = p.productIdentifier as String
switch prodID {
case "com.premium":
print("buying ads")
removeAds()
case "com.cash":
print("buying coins")
addCoins()
case "com.level":
print("buying levels")
addLevels()
default:
print("can't buy")
}
queue.finishTransaction(trans)
break;
case .failed:
print("buy error")
queue.finishTransaction(trans)
break;
default:
print("default")
break;
}
}
}
You should not update any purchase status in paymentQueueRestoreCompletedTransactionsFinished. This function just lets you know that the restoration process has completed. You could use this to update your UI or display an alert or something.
The restoration process delivers the transactions to be restored to the updatedTransactions function where you handle the .restored state in the same way that you handle the .purchased state.
Essentially "restore" just replays the purchase transaction process for non-consumable and auto-renewing subscription purchase types.

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