Search code examples
iosswiftswiftuiin-app-purchasestorekit

Restore purchase is restoring the purchase when user hasn't purchased at all


I am using Storekit to implement In-App Purchase in my app. It's live now. I have implemented a non-consumable purchase to remove ads. Now the issue is if even a new user tap on restore purchase, it's successfully restoring the purchase which it shouldn't. I am unable to debug it due to lack of testing device but I have asked multiple people to download the app and restore the purchase and they were successfully transitioned to Premium.

Below is the code I am using:

enum IAPHandlerAlertType {
    case initialize
    case setProductIds
    case disabled
    case restored
    case purchased
    case failed
    case error
    case restoreFailed
    
    var message: String{
        switch self {
        case .error: return "An error occured"
        case .initialize: return ""
        case .setProductIds: return "Product ids not set, call setProductIds method!"
        case .disabled: return "Purchases are disabled in your device!"
        case .restored: return "You've successfully restored your purchase!"
        case .purchased: return "You've successfully bought this purchase!"
        case .failed: return "Failed to buy this purchase!"
        case .restoreFailed: return "Failed to restore this purchase!"
        }
    }
}


class IAPManager: NSObject {
    
    //MARK:- Shared Object
    //MARK:-
    static let shared = IAPManager()
    private override init() { }
    
    //MARK:- Properties
    //MARK:- Private
    fileprivate var productIds = ["com.identifier.appName.removeAds"]
    fileprivate var productID = ""
    fileprivate var productsRequest = SKProductsRequest()
    fileprivate var fetchProductComplition: (([SKProduct])->Void)?
    
    fileprivate var productToPurchase: SKProduct?
    var purchaseProductComplition: ((IAPHandlerAlertType, Error?)->Void)?
    
    //MARK:- Public
    var isLogEnabled: Bool = true
    
    //MARK:- Methods
    //MARK:- Public
    
    //Set Product Ids
    func setProductIds(ids: [String]) {
        self.productIds = ids
    }

    //MAKE PURCHASE OF A PRODUCT
    func canMakePurchases() -> Bool {  return SKPaymentQueue.canMakePayments()  }
    
    func purchase(product: SKProduct, completion: @escaping ((IAPHandlerAlertType, Error?) -> Void)) {
        
        self.purchaseProductComplition = completion
        self.productToPurchase = product

        if self.canMakePurchases() {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(self)
            SKPaymentQueue.default().add(payment)
            
            log("PRODUCT TO PURCHASE: \(product.productIdentifier)")
            productID = product.productIdentifier
        }
        else {
            completion(IAPHandlerAlertType.disabled, nil)
        }
    }
    
    // RESTORE PURCHASE
    func restorePurchase(){
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
    
    
    // FETCH AVAILABLE IAP PRODUCTS
    func fetchAvailableProducts(completion: @escaping (([SKProduct])->Void)){
        
        self.fetchProductComplition = completion
        // Put here your IAP Products ID's
        if self.productIds.isEmpty {
            log(IAPHandlerAlertType.setProductIds.message)
            fatalError(IAPHandlerAlertType.setProductIds.message)
        }
        else {
            productsRequest = SKProductsRequest(productIdentifiers: Set(self.productIds))
            productsRequest.delegate = self
            productsRequest.start()
        }
    }
    
    //MARK:- Private
    fileprivate func log <T> (_ object: T) {
        if isLogEnabled {
            NSLog("\(object)")
        }
    }
}

//MARK:- Product Request Delegate and Payment Transaction Methods

extension IAPManager: SKProductsRequestDelegate, SKPaymentTransactionObserver {
    
    func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) {
        
        if let completion = self.fetchProductComplition {
            completion(response.products)
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        if let completion = self.fetchProductComplition {
            completion([])
        }
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        if let completion = self.purchaseProductComplition {
            completion(IAPHandlerAlertType.restored, nil)
        }
    }
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        if let completion = self.purchaseProductComplition {
            completion(IAPHandlerAlertType.restoreFailed, error)
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction:AnyObject in transactions {
            if let trans = transaction as? SKPaymentTransaction {
                switch trans.transactionState {
                case .purchased:
                    log("Product purchase done")
                    SKPaymentQueue.default().finishTransaction(trans)
                    if let completion = self.purchaseProductComplition {
                        completion(IAPHandlerAlertType.purchased, nil)
                    }
                    break
                    
                case .failed:
                    log("Product purchase failed")
                    SKPaymentQueue.default().finishTransaction(trans)
                    if let completion = self.purchaseProductComplition {
                        completion(IAPHandlerAlertType.failed, trans.error)
                    }
                    break
                case .restored:
                    log("Product restored")
                    SKPaymentQueue.default().finishTransaction(trans)
                    if let completion = self.purchaseProductComplition {
                        completion(IAPHandlerAlertType.restored, nil)
                    }
                    break
                    
                default: break
                }
            }
        }
    }
}

ViewModel

func restoreAction() {
            Spinner.start()
            IAPManager.shared.fetchAvailableProducts { products in
                if products.count > 0 {
                    IAPManager.shared.restorePurchase()
                    IAPManager.shared.purchaseProductComplition = { [self] result, error in
                        self.handlePurchaseRestoreResult(result: result, error: error)
                        Spinner.stop()
                    }
                } else {
                    Spinner.stop()
                }
            }
        }
    
    
     func handlePurchaseRestoreResult(result: IAPHandlerAlertType, error: Error?) {
            if error != nil {
                showAlert = .init(id: .error)
                return
            }
            switch result {
            case .disabled:
                showAlert = .init(id: .disabled)
            case .purchased:
                Defaults.isPremiumPurchased = true
                Defaults.totalCoins += 1000
                isPremiumPurchased = 1
                break
            case .restored:
                Defaults.isPremiumPurchased = true
                isPremiumPurchased = 1
                break
            case .failed:
                showAlert = .init(id: .failed)
            default:
                break
            }
        }

Am I doing something wrong here?


Solution

  • In your paymentQueueRestoreCompletedTransactionsFinished delegate method you are calling your completionHandler and passing IAPHandlerAlertType.restored.

    In your handlePurchaseRestoreResult this status results in you setting isPremiumPurchased = 1

    However, you have misunderstood the purpose of the paymentQueueRestoreCompletedTransactionsFinished method - It simply indicates that the restoration process is complete. You would typically use this to update your UI; removing an activity indicator for example.

    This delegate method is called regardless of whether there were any purchases to restore, so you should not set isPremiumPurchased = 1 simply because this method was called.

    You should only set isPremiumPurchased = 1 in response to a transaction being presented to your payment queue.

    The original StoreKit API is also being deprecated in iOS 18. You may want to consider moving to StoreKit2 API. This is much simpler to use, doesn't require purchase restoration flows and doesn't event need your to persist your own 'purchased' state - You can simply check to see if the product has been purchased.