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:
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:
Also the image from the edit scheme:
also the file testConfiguration.storekit in the left menu with a question mark.
I hope I described the problem I encountered in detail and correctly. Many thanks to everyone who took the time.