restoreCompletedTransactions() not working in iMessage Extension app

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 ?

Replies

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" ?

I assumed when I created Non-Consumable IAP in itunesconnect it was automatically marked as product that can be restored and there is no extra steps needed. From what I can see in itunesconnect inside actual IAP data, only "Turn On Content Hosting" is optional, so what did I miss ?

I would never assume that something is done automatically !


Cannot answer this. If PBK cannot answer, you'd better burn a DTS ticket for it. 😉

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?

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.

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.

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

Answering a PBK question:


the

do {

}

structure is curious: there is no try and no catch, do is thus useless.


And effectively identation is not done.

I have exactly the same issue WHAT WE HAVE TO DO?

Took me a while to suss out, but the reason my StoreKit was not updatingTransactions and restoring the purchase was because of a broken Configuration setting in my app's Scheme. When I set that to None, it worked! In Xcode I went into Edit>Scheme (image1) clicked on the Run>Options tab and selected None for StoreKit Configuration (image2). I also went on my physical device, and logged out of my personal Apple purchase account (Settings >Your Name/Pic at the Top > Media & Purchases > Sign Out) (image3). And finally, this step might not be critical, but I logged into my test sandbox account on my device at the bottom of the Settings>App Store menu(image4 and image5). That is an account that I setup in developer.apple.com under test users.