4
votes

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)
    }
}
1
That error is from trying to purchase the same product while one is still being processed. Do you disable the "Buy Now" button as soon as it's tapped?enc_life
Yes it's disabled, I'l double check though. The problem reported by users is that once this error begin to appear they can't buy any other products at all. Even after deleting app reinstalling doesn't help. I had this problem with one user which I remember and it didn't work out at all. In the end I pushed another release which resolved this issue. Does this give any more clue on what I'm doing wrong?Davis
Can you share code with how you're finishing the transactions? My understanding is that this will happen if you try to re-purchase the same product before you finish the transaction.enc_life
I've added code for complete transaction, is this the one you wanted? I've doubled checked my code and I'm disabling entire table selection in the table (I'm displaying products in table) tableView.allowsSelection = false and enabling it only after purchase is success or user cancels the purchase. I don't think it will be possible to click on Buy Now again while one is already in progress.Davis
What's triggering the complete function? It could be a problem where SKPaymentQueue.default().finishTransaction(transaction) isn't called in some instances.enc_life

1 Answers

1
votes

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

Solution

Follow Apple's best practices, and add the transaction observer at app launch 👍

In your case

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