swiftin-app-purchasestorekitin-app-subscriptionskproduct

Skproduct skipping product because no price was available


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.


Solution

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