iOS IAP paymentQueue:updatedTransactions - ios

The SKPaymentTransactionObserver.paymentQueue:updatedTransactions returns a array of transactions. When I make a payment, how do I know which transaction is the payment I made? Does it always return one transaction when I make a payment?
Meanwhile, this observer function is also called when restoring transactions. So, what is the best practice to handle the updatedTransactions?
BTW, my subscription product is a auto-renew subscription.

iterate through the loop of the transactions and check for each transaction.
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
completeTransaction(transaction: transaction)
break
case .failed:
failedTransaction(transaction: transaction)
break
case .restored:
restoreTransaction(transaction: transaction)
break
case .deferred:
// TODO show user that is waiting for approval
break
case .purchasing:
break
}
}
}
private func completeTransaction(transaction: SKPaymentTransaction) {
print("completeTransaction...")
deliverPurchaseForIdentifier(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restoreTransaction(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restoreTransaction... \(productIdentifier)")
deliverPurchaseForIdentifier(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func failedTransaction(transaction: SKPaymentTransaction) {
if let error = transaction.error as NSError? {
if error.domain == SKErrorDomain {
// handle all possible errors
switch (error.code) {
case SKError.unknown.rawValue:
print("Unknown error")
BaseViewController.currentViewController?.Alert(title: MFErrors.purchaseFaild.messgae.title, msg: MFErrors.purchaseFaild.messgae.body)
case SKError.clientInvalid.rawValue:
print("client is not allowed to issue the request")
BaseViewController.currentViewController?.Alert(title: MFErrors.accountNotAllowedToMakePurchase.messgae.title, msg: MFErrors.accountNotAllowedToMakePurchase.messgae.body)
case SKError.paymentCancelled.rawValue:
print("user cancelled the request")
case SKError.paymentInvalid.rawValue:
print("purchase identifier was invalid")
case SKError.paymentNotAllowed.rawValue:
print("this device is not allowed to make the payment")
BaseViewController.currentViewController?.Alert(title: MFErrors.purchaseFaild.messgae.title, msg: MFErrors.purchaseFaild.messgae.body)
default:
break;
}
}
ProgressViewManager.shared.hide()
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseForIdentifier(identifier: String?) {
guard let identifier = identifier else { return }
//Check if this transactions is a subscription
//SubscriptionsProductIdentifiers is an array of subscriptions product ids you sent to the app store to get SKProducts
//If subscription
if SubscriptionsProductIdentifiers.contains(identifier) {
}else{
//If non-consumable, consumables etc...
}
}
here's complete Store Manager in my previous answer:
How to handle shouldAddStorePayment for In-App Purchases in iOS 11?

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

why non-renewing subscription, shows "This In-App purchase has already been bought. It will be restored for free.", when purchasing item again?

I am implementing the non renewable purchase in my app. I am still using in sandbox mode. After I subscribe for the product, when I again try to subscribe the same product, it gives me an alert saying ‘This In-App purchase has already been bought. It will be restored for free.’. I don’t know how I should enable user to subscribe again.
How can I handle multiple user on same device? If one user has paid for the subscription and another user log in into same device to my application he/she should not get the alert as above.
Code :
import StoreKit
class className: SKProductsRequestDelegate
{
var productIDs: Array<String?> = []
var productsArray: Array<SKProduct?> = []
override func viewDidLoad(){
// product IDs for in-app purchase products
productIDs.append(“monthly_subscription_id”) // Monthly
productIDs.append(“yearly_subscription_id”) // Year
requestProductInfo()
SKPaymentQueue.default().add(self)
}
func requestProductInfo() {
if SKPaymentQueue.canMakePayments() {
let productIdentifiers = NSSet(array: productIDs)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers as Set<NSObject> as! Set<String>)
productRequest.delegate = self
productRequest.start()
}
else {
print("Cannot perform In App Purchases.")
}
}
// MARK: SKProductsRequestDelegate method implementation
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if response.products.count != 0 {
for product in response.products {
productsArray.append(product )
}
}
else {
print("There are no products.")
}
if response.invalidProductIdentifiers.count != 0 {
print(response.invalidProductIdentifiers.description)
}
}
// MARK: Buy Subscription button action
#IBAction func btn_purchase_Action(_ sender: Any){
let payment = SKPayment(product: self.productsArray[productIndex]!)
SKPaymentQueue.default().add(payment)
self.transactionInProgress = true
}
}
// MARK: - SKPaymentTransactionObserver
extension className: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}
private func complete(transaction: SKPaymentTransaction){
print("complete...")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction){
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction){
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
}
I could see popup saying in-app purchase is successful, but "updatedTransaction" function is not called when i successfully finish in-app purchase process.
First time in-app purchase is completed but when i try to purchase the same product again it shows the alert that product is already purchased and could restore for free.
From your code it looks like your transaction observer is a view controller.
If the view controller is dismissed before the payment transaction has been processed then you won't get a chance to complete the transaction.
Your payment queue observer should be an object that is instantiated as soon as your app launches and remains in memory for the lifetime of your app.
Creating the payment queue observer in didFinishLaunching and holding a reference to it in your app delegate is one approach that you can use.

Swift: Restore In-App purchase, no fail case called

I can buy and restore non-consummable products without any trouble.
But if I try to restore from an account that never bought the product, the method productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) is never called whereas it is called on a successful restore.
Here is my code in the class that conforms to the SKPaymentTransactionObserver protocol:
public func restorePurchases() {
print("restoring...")
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
// finishtTransaction() is called within the sub functions
switch (transaction.transactionState) {
case .purchased:
print("pruchased !")
complete(transaction: transaction)
break
case .failed:
print("failed !")
fail(transaction: transaction)
break
case .restored:
print("restored !")
restore(transaction: transaction)
break
case .deferred:
print("deferred !")
break
case .purchasing:
print("purchasing !")
break
default:
print("default...")
break
}
}
}
Please note that I can buy and restore products, I just never receive an answer when there is nothing to restore.
In fact, you just need to implement the following method:
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if (queue.transactions.count == 0)
{
print("Nothing to restore...")
}
}

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.

Swift, spritekit: In app purchase code runs, NOTHING happens?

Ok, so I'm working in Swift and I just need help. I have followed 4 different tutorials on how to implement in app purchases in sprite kit with Swift, copied code verbatim, and nothing is working for me.
Here are the steps I have taken:
Gone in Itunes Connect and created an in app purchase under my app's record. My in app purchase's product Id is "GameOverSaveSavior"
In Xcode, I've turned my app's In app purchase capability to ON, made sure my team is set to my account, and made sure my Bundle Identifier under info is set to my app's bundle identifier in ITunes Connect
Before writing any code, I have import StoreKit in my GameScene.swift file
As for code, this is what I have done:
(1) In Gamescene.swift, at the end of my didMoveToView func, I have:
// Set IAPS
if(SKPaymentQueue.canMakePayments()) {
println("IAP is enabled, loading")
var productID:NSSet = NSSet(objects: "GameOverSaveSavior")
var request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as Set<NSObject>)
request.delegate = self
request.start()
} else {
println("please enable IAPS")
}
This outputs "IAP is enabled, loading" when the app is run.
(2) In GameScene.swift, within the class but outside of didMoveToView, I have all the functions and variables others have used for in app purchases:
var list = [SKProduct]()
var p = SKProduct()
func purchaseMade() {
println("they bought it!")
}
func buyProduct() {
println("buy" + p.productIdentifier)
var pay = SKPayment(product: p)
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().addPayment(pay as SKPayment)
}
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
println("product request")
var myProduct = response.products
for product in myProduct {
println("product added")
println(product.productIdentifier)
println(product.localizedTitle)
println(product.localizedDescription)
println(product.price)
list.append(product as! SKProduct)
}
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
println("transactions restored")
var purchasedItemIDS = []
for transaction in queue.transactions {
var t: SKPaymentTransaction = transaction as! SKPaymentTransaction
let prodID = t.payment.productIdentifier as String
switch prodID {
case "GameOverSaveSavior":
purchaseMade()
//Right here is where you should put the function that you want to execute when your in app purchase is complete
default:
println("IAP not setup")
}
}
var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored. You may have to restart the app before banner ads are removed.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
println("add paymnet")
for transaction:AnyObject in transactions {
var trans = transaction as! SKPaymentTransaction
println(trans.error)
switch trans.transactionState {
case .Purchased, .Restored:
println("buy, ok unlock iap here")
println(p.productIdentifier)
let prodID = p.productIdentifier as String
switch prodID {
case "GameOverSaveSavior":
//Here you should put the function you want to execute when the purchase is complete
var alert = UIAlertView(title: "Thank You", message: "You may have to restart the app before the banner ads are removed.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
default:
println("IAP not setup")
}
queue.finishTransaction(trans)
break;
case .Failed:
println("buy error")
queue.finishTransaction(trans)
break;
default:
println("default")
break;
}
}
}
func finishTransaction(trans:SKPaymentTransaction)
{
println("finish trans")
}
func paymentQueue(queue: SKPaymentQueue!, removedTransactions transactions: [AnyObject]!)
{
println("remove trans");
}
This outputs "product request" to the console when the app is run.
(3) In GameScene.swift, in my touchesBegan func, I have the following for when the right button is touched:
//In app purchase
if touchedNode == saveMeBtn {
println("button touched!")
for product in list {
var prodID = product.productIdentifier
if(prodID == "GameOverSaveSavior") {
p = product
buyProduct() //This is one of the functions we added earlier
break;
}
}
This outputs "button touched!" when the button is touched.
I do not know what I am doing wrong. The code compiles with no errors, yet nothing occurs when my button is touched. There is no alert asking the user to sign in or to purchase anything- nothing.
I have mainly followed this question:
in app purchase in SKScene
Is there anything else I should have done prior to writing the code? Am I missing something in my code?
I think I can help you:
I had your issue in the past. And another similar one which I fixed here: My IAP isn't working. Bugs at func Paymentqueue
Here is the solution I had found:
Delete
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
everywhere you have it and put it once (ONLY ONCE) in a place where it will be executed each time your app boots up ( I put it in viewDidLoad()).
This will check for all unfinished transactions and terminate them once the app has loaded, thus removing any possible errors before your users triggers an IAP.
(If this answer helped you, don't forget to upvote ;))
P.S.: Also, this wasn't my issue, but make sure to finishTransaction() for each PurchaseState, like here:
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print("Add Payment")
for transaction:AnyObject in transactions{
let trans = transaction as! SKPaymentTransaction
print(trans.error)
switch trans.transactionState{
case .Purchased:
print("IAP unlocked")
print(p.productIdentifier)
let prodID = p.productIdentifier as String
switch prodID{
case "IAP id":
print("Keep on")
keepOn()
default:
print("IAP not setup")
}
queue.finishTransaction(trans)
break
case .Failed:
print ("Buy error")
queue.finishTransaction(trans)
break
default:
print("default: Error")
break
}
}
}
Never forget this:
queue.finishTransaction(trans)
And make separate case for .Restored.

Resources