Introduction

I was wondering how to add in-app purchases to my app. I chose non-consumable in-app purchase because you can pay one time for this item. Here are a few steps on how I did it.

First Step

Set up In-App Purchases for your app in App Store Connect account or add a .storekit configuration file and start from there. If you’ve already set up In-App Purchases in your account, you can sync the StoreKit config with that data.

Caveats

Be aware that if you choose to set up the StoreKit configuration file first, you will not find that file in the Xcode 15.3.0 iOS template. Instead, switch to macOS and search for it there.

Second Step

fetchProducts by identifiers to retrieve data and by using an SKProductsRequestDelegate to receive and display products.

func fetchProducts() {
    let productIDs: Set<String> = ["com.remove.ads.nonconsumable"]

    let request = SKProductsRequest(productIdentifiers: productIDs)
    request.delegate = self
    request.start()
}
// MARK: - SKProductsRequestDelegate
extension ViewModel: SKProductsRequestDelegate {

    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        DispatchQueue.main.async {
            self.products = response.products.map { product in
                Product(id: product.productIdentifier, title: product.localizedTitle, price: product.price.doubleValue)
            }

            for product in response.products {
                self.productsMap[product.productIdentifier] = product
            }
        }
    }

}

Third Step

Add a purchaseProduct method and connect the view model with the UI.

func purchaseProduct(product: Product) {
    guard SKPaymentQueue.canMakePayments() else {
        errorMessage = "In-app purchases are disabled on this device."
        return
    }

    guard let skProduct = productsMap[product.id] else {
        errorMessage = "Product information not available."
        return
    }

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

UI

var body: some View {
    ZStack(alignment: .top) {
        VStack(spacing: 10) {
            Text("With StoreKit 2")
                .padding()
            ForEach(viewModel.products) { product in
                Button {                    
                    viewModel.purchaseProduct(product: product)                        
                } label: {
                    Text(product.title)
                }
            }
        }
        .padding()
    }
    .onAppear(perform: {
        viewModel.fetchProducts()
    })
}

ViewModel

final class ViewModel: NSObject, ObservableObject {

    @Published var products: [Product] = []
    private var productsMap: [String: SKProduct] = [:]

    @Published var errorMessage: String?

    func fetchProducts() {
        let productIDs: Set<String> = ["com.remove.ads.nonconsumable"]

        let request = SKProductsRequest(productIdentifiers: productIDs)
        request.delegate = self
        request.start()
    }

    func purchaseProduct(product: Product) {
        guard SKPaymentQueue.canMakePayments() else {
            errorMessage = "In-app purchases are disabled on this device."
            return
        }

        guard let skProduct = productsMap[product.id] else {
            errorMessage = "Product information not available."
            return
        }

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

}

// MARK: - SKProductsRequestDelegate
extension ViewModel: SKProductsRequestDelegate {

    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        DispatchQueue.main.async {
            self.products = response.products.map { product in
                Product(id: product.productIdentifier, title: product.localizedTitle, price: product.price.doubleValue)
            }

            for product in response.products {
                self.productsMap[product.productIdentifier] = product
            }
        }
    }

}

Helpers

struct Product: Identifiable {
    let id: String
    let title: String
    let price: Double
}

Thank you for reading! 😊