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...")
}
}
Related
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()
I created a monthly subscription (auto-renewing) and installed a trial of 14 days for it in the App Store Connect. How to activate it now? I want that immediately after registering the user in the application a window appears with a proposal to purchase a monthly subscription or take advantage of the 14-day free version. How to use it in code?
It doesn't matter whether or not your subscription has introductory period. You need to implement:
1. products fetching using SKProductsRequest:
func loadProducts(){
let request = SKProductsRequest.init(productIdentifiers: productIds)
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
}
initiate payment process of that product using SKPaymentQueue:
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
Handle each switch-case in updated transactions method:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
// handle here
break
case .failed:
SKPaymentQueue.default().finishTransaction(transaction)
// handle here
break
case .restored:
SKPaymentQueue.default().finishTransaction(transaction)
// handle here
break
case .deferred, .purchasing:
break
default:
break
}
}
More details you can read in my article: https://blog.apphud.com/swift-tutorial-subscriptions/
You can also use the SwiftyStoreKit library.
How can I check when the user tap the accept button in the last Alert showed by Apple after the user make a subscription purchase.
I have the following code
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
complete(transaction: transaction)
case .failed:
fail(transaction: transaction)
case .restored:
restore(transaction: transaction)
case .deferred:
break
case .purchasing:
break
default:
break
}
}
}
But the complete method is called before user tap on the Alert.
How can I check the Alert callback ?
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?
I want to implement annual subscription into my app, and I do it using StoreKit. The problem is, when I tap subscriptionButton. An app throwing an error: 'NSInvalidArgumentException', reason: 'Cannot finish a purchasing transaction'. I was stuck in this place and can't resolve my problem. So, my paymentQueue code looks like that:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print(transactions)
for transaction in transactions {
print(transaction.error ?? "")
switch transaction.transactionState {
case .deferred:
print("deffered")
case let .failed(err):
print("failed: \(err)")
case .purchased:
let productID = p.productIdentifier
selectProduct(productID: productID)
case .purchasing:
print("purhasing")
print("produkt name: \(p.localizedTitle)") // after executing this line of code app crashes
case .restored:
let productID = p.productIdentifier
selectProduct(productID: productID)
}
queue.finishTransaction(transaction)
}
}
Do you have any suggestion, how can I fix it?
Working solution of this problem in my case is:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.error ?? "")
switch transaction.transactionState {
case .deferred:
print("deffered")
queue.finishTransaction(transaction)
case let .failed(err):
print("failed: \(err)")
queue.finishTransaction(transaction)
case .purchased:
let productID = p.productIdentifier
selectProduct(productID: productID)
queue.finishTransaction(transaction)
case .purchasing:
print("purhasing")
case .restored:
let productID = p.productIdentifier
selectProduct(productID: productID)
queue.finishTransaction(transaction)
}
}
}