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
}