0 Replies
      Latest reply on Feb 7, 2019 6:09 AM by Burve
      Burve Level 1 Level 1 (0 points)

        I am trying to restore Non-Consumable IAP with SKPaymentQueue.default().restoreCompletedTransactions() and nothing work according to plan. In sandbox I get "There's no information available for In-App Purchases. Try again later. 21105" and in Prod just nothing happens. From what I can see that error in Sandbox is thrown with actual SKPaymentQueue.default().restoreCompletedTransactions() line and nothing is processed after that. I am sure everything is right, since I can make my purchase by hand and it works, I can also repurchase anything again with error, that I already have it and then item get activated, but Restore button is not working (restorePurchases()). I do have 38 IAP in this app, but I am not sure if that is related in any way, and they all are Non-Consumable. It is iMessage Extension that sells stickers. What is also strange, is that Apple did kick my app back because I used my Own Art without My own permission (that was halarious), but everything states, that with Non-Consumable must have Restore and Apple check for that, and in this case everything passed while it is not working.

         

        My whiole IAP handlicg class look like this:

        
        import Foundation
        import StoreKit
        import os.log
        
        
        class IAPService: NSObject
        {
            private override init() {} // make sure there is no extra copies
            
            static let shared = IAPService() // makes this singleton
            fileprivate var products = [SKProduct]()
            let paymentQueue = SKPaymentQueue.default()
            let defaults = UserDefaults.standard
            var haveData = false
            var reference = StickewrsCollectionViewController()
            fileprivate var request = SKProductsRequest()
            let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "IAPService")
        
            let receiptFetcher = ReceiptFetcher()
        
            
            func getProducts()
            {
                let allStickersIAP = “IAP reference”
                var iapList = Dictionary<string, string="">()
                if let path = Bundle.main.path(forResource: "StickersData", ofType: ".plist")
                {
                    let dict = NSDictionary(contentsOfFile: path) as! Dictionary<string, anyobject="">
                    let globalData = dict["StickerData"] as! Array
                    iapList = (globalData[1]) as! Dictionary<string, string="">
                }
                var products: Set = [allStickersIAP]
                for iap in iapList
                {
                    products.insert(iap.value)
                }
                request = SKProductsRequest(productIdentifiers: products)
                request.delegate = self
                request.start()
                paymentQueue.add(self)
                SKPaymentQueue.default().add(self)
                os_log("Getting products")
                
        
            }
            
            func purchase(product: String)
            {
                if (SKPaymentQueue.canMakePayments())
                {
                    guard let productToPurchase = products.filter({$0.productIdentifier == product}).first
                    else {return}
        
                    let payment = SKPayment(product: productToPurchase)
                    paymentQueue.add(payment)
                }
            }
            
            func restorePurchases()
            {
                print("Restoring purchases")
                os_log("Restoring purchases")
        
                SKPaymentQueue.default().restoreCompletedTransactions()
        
            }
            
            func iapCheck() -> Bool
            {
                return SKPaymentQueue.canMakePayments() && haveData
            }
            
            func getProductData(iap: String) -> SKProduct
            {
                return products.filter({$0.productIdentifier == iap}).first ?? SKProduct()
            }
            
            func priceStringForProduct(item: SKProduct) -> String? {
                let price = item.price
                if price == NSDecimalNumber(decimal: 0.00) {
                    return NSLocalizedString("free", comment: "") 
                } else {
                    let numberFormatter = NumberFormatter()
                    let locale = item.priceLocale
                    numberFormatter.numberStyle = .currency
                    numberFormatter.locale = locale
                    return numberFormatter.string(from: price)
                }
            }
            
            func setReference(ref: StickewrsCollectionViewController)
            {
                reference = ref
            }
            
            public func passPopUp(_ text: String)
            {
                reference.ShowPopUp(text)
            }
           
            // Called when the application is about to terminate.
            func applicationWillTerminate(_ application: UIApplication) {
                // Remove the observer.
                SKPaymentQueue.default().remove(self)
            }
            
        }
        
        extension IAPService: SKProductsRequestDelegate, SKPaymentTransactionObserver
        {
            func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
                products = response.products
        
                haveData = true
            }
        
            func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
                for transaction in transactions {
                   switch  transaction.transactionState
                    {
                        case .purchasing: break
                        case .purchased:
                            do {
                                defaults.set(true, forKey: transaction.payment.productIdentifier)
                                defaults.synchronize() // save changes in PlayerPrefs
                                print(transaction.payment.productIdentifier)
                                queue.finishTransaction(transaction)
                                }
                    case .restored:
                        do {
                            print("Do actual restoring")
                            os_log("Now restoring %@",  transaction.original!.payment.productIdentifier)
                            reference.ShowPopUp("Restoring purchase \(transaction.original!.payment.productIdentifier)")
                            defaults.set(true, forKey: transaction.original!.payment.productIdentifier)
                            defaults.synchronize() // save changes in PlayerPrefs
                            print(transaction.payment.productIdentifier)
                            queue.finishTransaction(transaction)
                        }
                        default: queue.finishTransaction(transaction)
                    }
                }
                reference.redrawnAfterPurchase()
            }
            
            func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
                for transaction in queue.transactions {
                    let t: SKPaymentTransaction = transaction
                    let prodID = t.payment.productIdentifier as String
                    
                    defaults.set(true, forKey: prodID)
                    defaults.synchronize() // save changews in PlayerPrefs
                    print(prodID)
                    queue.finishTransaction(transaction)
                }
                reference.redrawnAfterPurchase()
                reference.ShowPopUp(NSLocalizedString("restoreCompleted", comment: ""))
            }
           
        }
        
        extension SKPaymentTransactionState
        {
            func status() -> String
            {
                switch self {
                case .deferred: return "deferred"
                case .failed: return "failed"
                case .purchased: return "purchased"
                case .purchasing: return "purchasing"
                case .restored: return "restored"
        
                }
            }
        }

         

        Whart I did wrong and how I can make RestorePurchases actually do something ?

        • Re: restoreCompletedTransactions() not working
          Claude31 Level 8 Level 8 (6,865 points)

          You'd probably better move this post to the IAP section of the forum.

           

          I read in doc :

           

          When you create a new product to be sold in your store, you choose whether that product can be restored or not. See the In-App Purchase Programming Guide for more information.

           

          Have you checked you chose "can be restored" ?

          • Re: restoreCompletedTransactions() not working
            PBK Level 7 Level 7 (3,295 points)

            I don't do Swift so I can't comment on your code - and besides, TMI. 

             

            three things:

             

            1) I don't understand the Swift structure of switch/case/default/break - It is unusual how the case.restored code block is indented.  Does it work/not work the same way if you switch the order of the blocks of case.purchased with case.restored?

             

            2) A key question is whether StoreKit ever calls updatedTransactions with a state restored.  In your case - are you or are you not executing the line: 

                  print("Do actual restoring").

            If you are - your code is screwed up from that point forward.

             

            3) The only other thing I can think of is - have you executed

                  SKPaymentQueue.default().add(self)

            before calling restoreCompeltedTransactions? 

              • Re: restoreCompletedTransactions() not working
                Burve Level 1 Level 1 (0 points)

                I never get that far in sandbox, error I get is already in SKPaymentQueue.default().restoreCompletedTransactions().

                 

                As for SKPaymentQueue.default().add(self) I call it in getProducts() that is called from viewDidLoad() in main view, and that works fine, since I am getting all my IAP data and can actually buy anythging I want.

                 

                As for case, I am not sure either, but from what I have seen, even if I just do regular purchase I don't get any prints or anything else from restore part, so it might be good.

                  • Re: restoreCompletedTransactions() not working
                    PBK Level 7 Level 7 (3,295 points)

                    So to debug....

                     

                    First - do you call getProducts() when all you want to do is restore? If not, then add SKPaymentQueue.default().add(self) to restorePurchases()

                     

                    Second - add a print command to print out the value of transaction.transactionState in "for transaction in transactions {"

                     

                    Third -  switch the code block for case.purchased:do{...}  with the code block for case.restored:do{...}  and see if you can still make a purchase.   That will prove that the code block for case.restored is correctly positioned.  Something is wrong with the indentation - that must mean something about the Swift text editor or your structure of case.purchased:do{...} structure.

                      • Re: restoreCompletedTransactions() not working
                        Burve Level 1 Level 1 (0 points)

                        first - yes, getProduct() is always called on app start inside viewDidLoad() in the main view controller, so it is impossible to miss it. as an extra step in productsRequest() I have the haveData variable that is true only after I got data about all my IAP and then I display buy all button, and I do all tests after I see Buy All button

                         

                        second - I never get that far in when I try to restore, I got an error message with code 21105 right after I press a button and I never get to paymentQueue(). From what I understand restore just kicks me out while saying - there is nothing to restore, go away.

                         

                        third - technically only difference between .purchase and .restored is in this line
                        defaults.set(true, forKey: transaction.original!.payment.productIdentifier)
                        or rather in restored it is transaction.original!.payment.productIdentifier while purchased it is transaction.payment.productIdentifier and rest is just print, log and popup, that never happens since it is never called.

                         

                        Generally, I start to wonder logic in restore function. When I want to purchase something I need to pass specific IAP identifier to buy that (and that makes purchase functions quite generic, while restore function is a psychic - it finds old purchases on its own. Any chance I am missing something there and that why nothing works ?)

                         

                        EDIT. This whole thing happens in the iMessage app and it seems it is a common Apple issue, at least I found people with same problem https://forums.developer.apple.com/thread/111646

                         

                        What is interesting is that I tested my IAP script in single view app and new iMessage app, and in a single view app all worked fine while in iMessage I got the same error again. The only question is - if it is official Apple bug, how come I see other apps that have restore button with animation; however, I have not tried to purchase anything there and restore afterwards