Skproduct skipping product because no price was available
Asked Answered
L

4

9

This is my first experience with creating purchases. The app I'm working on hasn't been released yet. I've been testing subscriptions locally using the Configuration.storekit file. Everything worked fine. I recently encountered a problem - my subscriptions are no longer displayed in the project. I got an error like this in the terminal: Image with output from the terminal

UPD:

  • I decided to check the application on the emulator and everything works there. As far as I remember everything broke after installing xcode 14 and updating to ios 16.
  • On the physical device, the problem remains.

I didn't change the code in those places. I tried to create new .storekit files, but it still doesn't work. I tried to load the .storekit file with the synchronization. In it the price is pulled up and displayed correctly, as on the site, but in the terminal again writes the same error.

Here is the file that works with purchases:

import StoreKit

typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result<Bool, Error>

typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void


class Purchases: NSObject {
    static let `default` = Purchases()
    private let productIdentifiers = Set<String>(
        arrayLiteral: "test.1month", "test.6month", "test.12month"
    )

    private var products: [String: SKProduct]?
    private var productRequest: SKProductsRequest?
    private var productsRequestCallbacks = [RequestProductsCompletion]()
    fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?
    
    
    func initialize(completion: @escaping RequestProductsCompletion) {
        requestProducts(completion: completion)
    }
    

    private func requestProducts(completion: @escaping RequestProductsCompletion) {
        guard productsRequestCallbacks.isEmpty else {
            productsRequestCallbacks.append(completion)
            return
        }

        productsRequestCallbacks.append(completion)

        let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productRequest.delegate = self
        productRequest.start()

        self.productRequest = productRequest
    }
    

    func purchaseProduct(productId: String, completion: @escaping (PurchaseProductResult) -> Void) {
        
        guard productPurchaseCallback == nil else {
            completion(.failure(PurchasesError.purchaseInProgress))
            return
        }
        
        guard let product = products?[productId] else {
            completion(.failure(PurchasesError.productNotFound))
            return
        }

        productPurchaseCallback = completion

        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }

    
    public func restorePurchases(completion: @escaping (PurchaseProductResult) -> Void) {
        guard productPurchaseCallback == nil else {
            completion(.failure(PurchasesError.purchaseInProgress))
            return
        }
        productPurchaseCallback = completion
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
}


extension Purchases: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        guard !response.products.isEmpty else {
            print("Found 0 products")

            productsRequestCallbacks.forEach { $0(.success(response.products)) }
            productsRequestCallbacks.removeAll()
            return
        }

        var products = [String: SKProduct]()
        for skProduct in response.products {
            print("Found product: \(skProduct.productIdentifier)")
            products[skProduct.productIdentifier] = skProduct
        }

        self.products = products

        productsRequestCallbacks.forEach { $0(.success(response.products)) }
        productsRequestCallbacks.removeAll()
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Failed to load products with error:\n \(error)")

        productsRequestCallbacks.forEach { $0(.failure(error)) }
        productsRequestCallbacks.removeAll()
    }
}


extension Purchases: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased, .restored:
                if finishTransaction(transaction) {
                    SKPaymentQueue.default().finishTransaction(transaction)
                    productPurchaseCallback?(.success(true))
                    UserDefaults.setValue(true, forKey: "isPurchasedSubscription")
                } else {
                    productPurchaseCallback?(.failure(PurchasesError.unknown))
                }
        
            case .failed:
                productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
                SKPaymentQueue.default().finishTransaction(transaction)
                
            default:
                break
                
            }
        }

        productPurchaseCallback = nil
        
    }
}


extension Purchases {
    func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
        let productId = transaction.payment.productIdentifier
        print("Product \(productId) successfully purchased")
        return true
    }
}

There is also a file that is responsible for displaying available subscription options:


//
//  PremiumRatesTVC.swift
//  CalcYou
//
//  Created by Admin on 29.08.2022.
//

import StoreKit
import UIKit

class PremiumRatesTVC: UITableViewController {
    var oneMonthPrice    = ""
    var sixMonthPrice    = ""
    var twelveMonthPrice = ""
    
    @IBOutlet weak var oneMonthPriceLabel:     UILabel!
    @IBOutlet weak var oneMothDailyPriceLabel: UILabel!
    
    @IBOutlet weak var sixMonthPriceLabel:      UILabel!
    @IBOutlet weak var sixMonthDailyPriceLabel: UILabel!
    
    @IBOutlet weak var twelveMonthPriceLabel:      UILabel!
    @IBOutlet weak var twelveMonthDailyPriceLabel: UILabel!
    
    @IBOutlet weak var tableViewCellOneMonth:    UITableViewCell!
    @IBOutlet weak var tableViewCellSixMonth:    UITableViewCell!
    @IBOutlet weak var tableViewCellTwelveMonth: UITableViewCell!
    
    
    @IBAction func cancelButton(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
    
    // MARK: ViewDidLoad()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        hideSubscriptions()
        navigationItem.title = "Premium PRO version"
        
        Purchases.default.initialize { [weak self] result in
            guard let self = self else { return }

            switch result {
            case let .success(products):
                guard products.count > 0 else {
                    let message = "Failed to get a list of subscriptions. Please try again later."
                    self.showMessage("Oops", withMessage: message)
                    return
                    
                }
                self.showSubscriptions()
                
                DispatchQueue.main.async {
                    self.updateInterface(products: products)
                    
                }
                
            default:
                break
                
            }
        }
    }
    
    
    // MARK: Functions()
    private func updateInterface(products: [SKProduct]) {
        updateOneMonth(with: products[0])
        updateSixMonth(with: products[1])
        updateTwelveMonth(with: products[2])
    }
    
    
    private func hideSubscriptions() {
        DispatchQueue.main.async {
            self.tableViewCellOneMonth.isHidden = true
            self.tableViewCellSixMonth.isHidden = true
            self.tableViewCellTwelveMonth.isHidden = true
            
        }
    }
    
    
    private func showSubscriptions() {
        DispatchQueue.main.async {
            self.tableViewCellOneMonth.isHidden = false
            self.tableViewCellSixMonth.isHidden = false
            self.tableViewCellTwelveMonth.isHidden = false
            
        }
    }
    
    
    func showMessage(_ title: String, withMessage message: String) {
        DispatchQueue.main.async {
            let alert = UIAlertController(title: title,
                                          message: message,
                                          preferredStyle: UIAlertController.Style.alert)
            let dismiss = UIAlertAction(title: "Ok",
                                        style: UIAlertAction.Style.default,
                                        handler: nil)
            
            alert.addAction(dismiss)
            self.present(alert, animated: true, completion: nil)
        }
    }

    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        
        if indexPath.section == 0 && indexPath.row == 0 {
            guard let premiumBuyVC = storyboard.instantiateViewController(identifier: "PremiumBuyVC") as? PremiumBuyVC else { return }
            
            premiumBuyVC.price = oneMonthPrice
            premiumBuyVC.productId = "1month"
            premiumBuyVC.period = "per month"
            show(premiumBuyVC, sender: nil)
        }
        
        if indexPath.section == 1 && indexPath.row == 0 {
            guard let premiumBuyVC = storyboard.instantiateViewController(identifier: "PremiumBuyVC") as? PremiumBuyVC else { return }
            
            premiumBuyVC.price = sixMonthPrice
            premiumBuyVC.productId = "6month"
            premiumBuyVC.period = "per 6 month"
            show(premiumBuyVC, sender: nil)
        }
        
        if indexPath.section == 2 && indexPath.row == 0 {
            guard let premiumBuyVC = storyboard.instantiateViewController(identifier: "PremiumBuyVC") as? PremiumBuyVC else { return }
            
            premiumBuyVC.price = twelveMonthPrice
            premiumBuyVC.productId = "12month"
            premiumBuyVC.period = "per 12 month"
            show(premiumBuyVC, sender: nil)
        }
    }
}


extension SKProduct {
    public var localizedPrice: String? {
        let numberFormatter = NumberFormatter()
        numberFormatter.locale = self.priceLocale
        numberFormatter.numberStyle = .currency
        return numberFormatter.string(from: self.price)
    }
}


// MARK: Обновление информации
// в cell для 1, 6, 12 месяцев
extension PremiumRatesTVC {
    func updateOneMonth(with product: SKProduct) {
        let withCurrency = "\(product.priceLocale.currencyCode ?? " ")"
        let daily = dailyPrice(from: Double(truncating: product.price), withMonth: 1.0)
        
        oneMonthPriceLabel.text = "\(product.price) \(withCurrency)"
        oneMothDailyPriceLabel.text = "\(daily) \(withCurrency)"
        oneMonthPrice = "\(product.price) \(withCurrency)"
    }
    
    func updateSixMonth(with product: SKProduct) {
        let withCurrency = "\(product.priceLocale.currencyCode ?? " ")"
        let daily = dailyPrice(from: Double(truncating: product.price), withMonth: 6.0)
        
        sixMonthPriceLabel.text = "\(product.price) \(withCurrency)"
        sixMonthDailyPriceLabel.text = "\(daily) \(withCurrency)"
        sixMonthPrice = "\(product.price) \(withCurrency)"
    }

    func updateTwelveMonth(with product: SKProduct) {
        let withCurrency = "\(product.priceLocale.currencyCode ?? " ")"
        let daily = dailyPrice(from: Double(truncating: product.price), withMonth: 12.0)
        
        twelveMonthPriceLabel.text = "\(product.price) \(withCurrency)"
        twelveMonthDailyPriceLabel.text = "\(daily) \(withCurrency)"
        twelveMonthPrice = "\(product.price) \(withCurrency)"
    }
    
    func dailyPrice(from value: Double, withMonth: Double) -> String {
        let days = withMonth * 30
        let result = value / days
        
        return String(format: "%.2f", result)
    }
}


This image shows the testConfiguration.storekit file:

testConfiguration.storekit file image

Also the image from the edit scheme:

edit scheme image

also the file testConfiguration.storekit in the left menu with a question mark. file with question image

I hope I described the problem I encountered in detail and correctly. Many thanks to everyone who took the time.

Loudmouthed answered 22/9, 2022 at 7:42 Comment(4)
Having the same issue, did you be able to solve this?Lollygag
@kanobius, not yet. There is a chance that I will find a solution when I release the app in the AppStore.Loudmouthed
I have the same issueDagney
I've ended up switching to StoreKit2Lollygag
L
1

My boss didn't have the Paid Apps field filled in. Be sure to look to make sure it is active. Check this answer

Loudmouthed answered 9/1, 2023 at 13:15 Comment(0)
I
3

I had this problem too. Try with a device on iOS 15.X.

Built with Xcode 14.0.1 iPhone 13 iOS 16.0: Skipping product because no price was available

Built with Xcode 14.0.1 iPhone 11 iOS 15.5: everything works.

Insolvency answered 4/10, 2022 at 12:25 Comment(3)
Tell me, are the products themselves displayed normally in testflight on a physical device? Could the problem be that my products are missing metadata?Loudmouthed
@Loudmouthed yes you should normally see products on testflight build.Insolvency
@Loudmouthed yea, everything is normally displayed and properly registered, since it already works on iOS 15. Although with the new iOS 16.1 and the new Xcode I assume it should also work.Roentgenotherapy
R
1

I had the same problem and the same answers as @Vjardel, that this occurs on iOS 16 when started with Xcode. In my case I tested it with an iPad mini 5th generation on iOS 16 Beta 10.

Although, I discovered that this issues does not happen on the same device, if you try it with a TestFlight build. Therefore, you can test it with TestFlight, plus I assume that if the app is in the App Store the issue won't happen, as well.

Roentgenotherapy answered 6/10, 2022 at 7:16 Comment(0)
L
1

My boss didn't have the Paid Apps field filled in. Be sure to look to make sure it is active. Check this answer

Loudmouthed answered 9/1, 2023 at 13:15 Comment(0)
P
0

I experienced the same issue as some others in the comment section. Thankfully, I still had a device running iOS 15.5 laying around which made the error disappear. However, my device with iOS 16 was still testable with the Testflight version.

Protestantism answered 3/4, 2023 at 22:23 Comment(1)
This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. To get notified when this question gets new answers, you can follow this question. Once you have enough reputation, you can also add a bounty to draw more attention to this question. - From ReviewLonee

© 2022 - 2024 — McMap. All rights reserved.