Search code examples
swiftin-app-purchasestorekit

Not getting expected delegate calls when trying to restore in-app purchases with StoreKit


I've read over a number of questions (with no consistent answers) on restoring IAPs in StoreKit API. The part that seems to be incredibly ambiguous - in a test/sandbox environment - is what happens when you restore.

When using this method:

@IBAction func restorePurchasesButtonPressed(_ sender: Any) {        
   SKPaymentQueue.default().restoreCompletedTransactions(withApplicationUsername: productID)
}

I get no feedback other than a call to the

paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue)

function. Is there not a way to determine if the restore purchases was successful? I thought that the call would pass through this function:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        if transaction.transactionState == .purchased {
            //item already purchased
            print("Transaction successful...")
        } else if transaction.transactionState == .failed {
            print("Transaction failed...")
        } else if transaction.transactionState == .restored {
            print("Restored purchases...")
        }
    }
}

But it does not get called. If I try to make a purchase that has already been made against a product, the updatedTransactions func gets called, but only with a .purchased value.

I have read so many things about sandbox being weird and unpredictable. I have set up numerous verified sandbox accounts and none of them work as expected.

Purchase functionality seems to work fine. If I purchase what has already been purchased, it gets restored with a notification. But, I can not call the restoreCompletedTransactions and figure out how to get any value - success or fail. What am I missing here? Is there a definitive answer or a workaround?


Solution

  • You can use the following modular swift file, UnlockManager.swift in your app to implement in-app-purchases, although I can only guarantee it will work for a single non-consumable product, for example an unlock purchase... otherwise, you'll need to do some modification.

    Anyway, here's the meat and potatoes:

    UnlockManager.swift:

    //  2019 Boober Bunz. No rights reserved.
    
    import StoreKit
    
    protocol UnlockManagerDelegate: class {
        func showUnlockPurchaseHasBeenRestoredAlert()
    }
    
    class UnlockManager : NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
        
        // Only used in print debugging
        // let transactionStateStrings = ["purchasing","purchased","failed","restored","deferred"]
        
        weak var delegate: UnlockManagerDelegate?
        
        private let UNLOCK_IAP_PRODUCT_ID = "your_product_ID_goes_here" // <------------------------------------ ***
        
        private var requestObject: SKProductsRequest?
        private var skProductObject: SKProduct?
        
        private var onlineAndReadyToPurchase = false
        
        override init() {
            super.init() //important that super.init() comes first
            attemptStoreKitRequest()
            SKPaymentQueue.default().add(self)
        }
        
        deinit {
            SKPaymentQueue.default().remove(self)
        }
        
        // ------------------------------------------------------------------------------------ STOREKIT CALLBACKS
        
        // SKProductsRequestDelegate response
        public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
            
            if response.products.count > 0 {
                for product in response.products {
                    if product.productIdentifier == UNLOCK_IAP_PRODUCT_ID {
                        skProductObject = product
                        onlineAndReadyToPurchase = true
                        print("IAP - StoreKit server responded with correct product.  We are ready to purchase.")
                        return // success
                    }
                }
            } else { // fail
                print("IAP MODULE - on initial request, StoreKit server responded, but \(UNLOCK_IAP_PRODUCT_ID) not found.")
                print("IAP MODULE - Check for product ID mismatch.")
                print("IAP MODULE - We are not ready to purchase.\n")
            }
        }
        
        // SKProductsRequestDelegate response (fail)
        public func request(_ request: SKRequest, didFailWithError error: Error) {
            print("IAP MODULE - on initial request, StoreKit server responded with explicit error: \(error.localizedDescription)")
        }
        
        
        // SKPaymentTransactionObserver calls this
        public func paymentQueue(_ queue: SKPaymentQueue,
                                 updatedTransactions transactions: [SKPaymentTransaction]) {
            
            print("IAP MODULE - SKPaymentTransactionObserver called paymentQueue()...")
            
            //    print("Transaction Queue:")
            
            for transaction in transactions {
                //    print("\n PRODUCT ID: \(transaction.payment.productIdentifier)")
                //    print(" TRANS ID: \(transaction.transactionIdentifier)")
                //    print(" TRANS STATE: \(transactionStateStrings[transaction.transactionState.rawValue])")
                //    print(" TRANS DATE: \(transaction.transactionDate)")
                
                //    print("\nActions taken as a result of trans.state...")
                
                switch transaction.transactionState {
                case .purchased:
                    
                    if (transaction.payment.productIdentifier == UNLOCK_IAP_PRODUCT_ID) {
                        print("IAP MODULE - successful purchase of \(UNLOCK_IAP_PRODUCT_ID), so unlocking")
                        UserDefaults.standard.set(true, forKey: UNLOCK_IAP_PRODUCT_ID)
                        SKPaymentQueue.default().finishTransaction(transaction)
                    }
                    break
                case .restored:
                    guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
                    if (productIdentifier == UNLOCK_IAP_PRODUCT_ID) {
                        if !(appIsUnlocked()){
                            delegate?.showUnlockPurchaseHasBeenRestoredAlert()
                        }
                        print("IAP MODULE - previous purchase of \(UNLOCK_IAP_PRODUCT_ID), so restoring/unlocking")
                        UserDefaults.standard.set(true, forKey: UNLOCK_IAP_PRODUCT_ID)
                        SKPaymentQueue.default().finishTransaction(transaction)
                    }
                    break
                case .failed:
                    if let transactionError = transaction.error as NSError?,
                        let localizedDescription = transaction.error?.localizedDescription,
                        transactionError.code != SKError.paymentCancelled.rawValue {
                        print("IAP MODULE - ... error in transaction \(transaction.transactionIdentifier ?? "no ID?") for product: \((transaction.payment.productIdentifier)): \(localizedDescription)")
                    }
                    SKPaymentQueue.default().finishTransaction(transaction)
                    break
                case .deferred:
                    break
                case .purchasing:
                    break
                default:
                    break
                }
            }
        }
        
        // ------------------------------------------------------------------------------------ PUBLIC ONLY METHODS
        public func purchaseApp() -> Bool {
            if !onlineAndReadyToPurchase {
                print("IAP MODULE - Purchase attempted but we are not ready!")
                return false
            }
            print("IAP MODULE - Buying \(skProductObject!.productIdentifier)...")
            let payment = SKPayment(product: skProductObject!)
            SKPaymentQueue.default().add(payment)
            return true
        }
        
        public func restorePurchases() -> Bool {
            if !onlineAndReadyToPurchase {
                print("IAP MODULE - User attempted restore, but we are presumbly not online!")
                return false
            }
            SKPaymentQueue.default().restoreCompletedTransactions()
            return true
        }
        
        public func appIsUnlocked() -> Bool {
            return UserDefaults.standard.bool(forKey: UNLOCK_IAP_PRODUCT_ID)
        }
        // ------------------------------------------------------------------------------------ PUBLIC AND INTERNAL METHODS
        
        // Presumably called on app start-up
        // AND (for good measure) when user is presented with purchase dialog
        public func attemptStoreKitRequest() {
            if !onlineAndReadyToPurchase {
                requestObject = SKProductsRequest(productIdentifiers: [UNLOCK_IAP_PRODUCT_ID])
                print("IAP MODULE - sending request to StoreKit server for product ID: \(UNLOCK_IAP_PRODUCT_ID)...")
                print("IAP MODULE - waiting for response...")
                requestObject?.delegate = self
                requestObject?.start()
            }
        }
        
        
    }