Davis
Davis

Reputation: 105

Consumable products - This In-App Purchase has already been bought. It will be restored for free

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

Answers (1)

Josselin
Josselin

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

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

Upvotes: 1

Related Questions