IAP work in simulator but don't appear on a real device

I have implemented IAP in my app which seem to work fine in the simulator, however they don’t appear in my preview canvas or when i run it on a real device. I request products by calling this when the screen appears

.onAppear(perform: {IAPManager.shared.getProductsV5()})

then use a foreach to display the requested products

ForEach((0 ..< self.products.items.count), id: \.self) { column in
                    Button(action: {
                        let _ = IAPManager.shared.purchaseV5(product: self.products.items[column]); self.soundEffectsM.playSound(sound: "ShellGrab", type: "wav")
                    }) {
                        Text(self.products.items[column].localizedDescription) }

And if it helps here is my purchase manager file, i’m sorry if its too much code but i want to give all the info i can

final class ProductsDB: ObservableObject, Identifiable {
    
    static let shared = ProductsDB()
    var items: [SKProduct] = [] {
        willSet {
            DispatchQueue.main.async {
                self.objectWillChange.send()
            }
        }
    }
}

class IAPManager: NSObject, ObservableObject {
    
    // extra rounds
    @Published var rounds = Extra()
    var purchaseExtraRounds = false
    let purchasePublisher = PassthroughSubject<(String, Bool), Never>()
    static let shared = IAPManager()
    var totalRestoredPurchases: Int = 0
    
    private override init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }
    
    func buyV5(product: SKProduct) {
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }
    
    func canMakePayments() -> Bool {
        return SKPaymentQueue.canMakePayments()
    }
    
    func startObserving() {
        SKPaymentQueue.default().add(self)
    }
    
    func stopObserving() {
        SKPaymentQueue.default().remove(self)
    }
    
    func getProductsV5() {
        let productIDs = Set(returnProductIDs())
        let request = SKProductsRequest(productIdentifiers: Set(productIDs))
        request.delegate = self
        request.start()
    }
    
    func returnProductIDs() -> [String] {
        return ["ProductOne", "ProductTwo", "ProductThree"]
    }
    
    func formatPrice(for product: SKProduct) -> String? {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = product.priceLocale
        return formatter.string(from: product.price)
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        if totalRestoredPurchases != 0 {
            purchasePublisher.send(("IAP: Purchases successfuly restored!", true))
        } else {
            purchasePublisher.send(("IAP: No purchases to restore!", true))
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        if let error = error as? SKError {
            if error.code != .paymentCancelled {
                purchasePublisher.send(("IAP Restore Error: " + error.localizedDescription, false))
            } else {
                purchasePublisher.send(("IAP Error: " + error.localizedDescription, false))
            }
        }
    }
    
    func restorePurchasesV5() {
        totalRestoredPurchases = 0
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
    
    func purchaseV5(product: SKProduct) -> Bool {
        if !IAPManager.shared.canMakePayments() {
            return false
        } else {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
        }
        return true
    }
}

extension IAPManager: SKProductsRequestDelegate, SKRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let badProducts = response.invalidProductIdentifiers
        let goodProducts = response.products
        
        if !goodProducts.isEmpty {
            ProductsDB.shared.items = response.products
            print("Good", ProductsDB.shared.items)
        }
        print("BadProducts", badProducts)
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("didFailWithError", error)
        purchasePublisher.send(("Purchase request failed", true))
    }
    
    func requestDidFinish(_ request: SKRequest) {
        print("Request did finish")
    }
}

extension IAPManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach { (transaction) in
            switch transaction.transactionState {
            
            case .purchased:
                purchasePublisher.send(("Purchase successful", true))
                handlePurchase(transaction.payment.productIdentifier)
                SKPaymentQueue.default().finishTransaction(transaction)
                print("Purchase successful")
                
            case .restored:
                totalRestoredPurchases += 1
                purchasePublisher.send(("Purchases restored", true))
                SKPaymentQueue.default().finishTransaction(transaction)
                print("Restore purchases successful")
                
            case .failed:
                if let error = transaction.error as? SKError {
                    purchasePublisher.send(("Payment error \(error.code) ", false))
                    print("Purchase failed \(error.code)")
                }
                SKPaymentQueue.default().finishTransaction(transaction)
                
            case .deferred:
                print("Purchase permission required")
                purchasePublisher.send(("Payment deferred", false))
                SKPaymentQueue.default().finishTransaction(transaction)
                
            case .purchasing:
                print("Purchase in progress")
                purchasePublisher.send(("Payment in progress", false))
                
            default:
                break
            }
        }
    }
    private func handlePurchase(_ id: String) {
}

I tried to come up with another way of displaying the IAP by creating buttons to each directly call a specific index in the returnProductIDs function but i get a crash with the error index out of range.
Thanks for any help.

Can you show a screenshot of the error?

The top image that only displays my logo is what i see running it on a real device. The bottom image is from the simulator and what i want it to display.

oh theres no error or crash?

maybe it has something to do with permissions or some region setting that prevents you from seeing the IAP. on a real device

Apologies there is a crash, when i created buttons to call each item in the index as a work around because the foreach method wasn’t displaying the products on a real device.
This error-
Thread 1: Fatal error: Index out of range
On this line-

Text(self.products.items[0].localizedDescription)

I think it has something to do with items being a SKProduct type and the array is empty but not sure.
it is listing my products as bad products

extension IAPManager: SKProductsRequestDelegate, SKRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let badProducts = response.invalidProductIdentifiers
        let goodProducts = response.products
        
        if !goodProducts.isEmpty {
            ProductsDB.shared.items = response.products
            print("Good", ProductsDB.shared.items)
        }
        print("BadProducts", badProducts)
    }

As per the norm it was me being an idiot. I had the wrong bundle ID in the AppStore. Working fine now.

1 Like