Reputation: 105
On my live app users keep getting this error for consumable products. This is very random error and happens rarely.
This In-App Purchase has already been bought. It will be restored for free.
In my app I've prevented users tapping on Buy Now button unless app purchase process is completed.
I've already read solution provided on following questions
Sandbox trying to restore consumable IAP
My IAP isn't working. Bugs at func Paymentqueue
I've SKPaymentQueue.default().add() at two places in my code as shown below. I'm also calling SKPaymentQueue.default().finishTransaction(transaction) for each transactionState.
Can anyone let me know what else I need to check to fix this issue?
open class IAPHelper: NSObject {
// Callback
var purchaseStatusBlock: ((IAPHandlerAlertType, String, NSData) -> Void)?
var purchaseFailed: ((SKPaymentTransaction) -> Void)?
private let productIdentifiers: Set<ProductIdentifier>
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
super.init()
SKPaymentQueue.default().add(self) // #1
}
}
And second one is
extension IAPHelper {
public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct, vc: UIViewController) {
let viewController = vc as! PurchaseViewController
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment) // #2
}
}
Transaction
extension IAPHelper: 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) {
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = NSData(contentsOf: receiptURL!)
if (receipt == nil) {
// No local receipt -- handle the error
let alert = UIAlertController(title: "Purchase Error", message: "No local receipt", preferredStyle: UIAlertController.Style.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertAction.Style.default) { (action) in
}
alert.addAction(okAction)
return
}
// Callback
purchaseStatusBlock?(.purchased, transaction.payment.productIdentifier, receipt!)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
}
// Callback
purchaseFailed?(transaction)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
// purchasedProductIdentifiers.insert(identifier)
// UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}
Upvotes: 4
Views: 2176
Reputation: 2643
We had a similar issue bugging us for a long time...
When users initiated a purchase and then lost Internet connection or killed the app before the transaction was fully processed, they would be charged but never receive the IAP content even upon restoring
Follow Apple's best practices, and add the transaction observer at app launch 👍
You should remove:
SKPaymentQueue.default().add(self) // #1
from your IAPHelper.init
method.
And instead add the observer in the AppDelegate
:
class AppDelegate: UIResponder, UIApplicationDelegate {
let iapHelper = IAPHelper()
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
SKPaymentQueue.default().add(iapHelper)
}
Then in the ViewController where you need it, you can access the iapHelper
using:
let iapHelper = (UIApplication.shared.delegate as! AppDelegate).iapHelper
Upvotes: 1