StoreKit enables in-app purchases in iOS apps. Learn to implement consumables, non-consumables, and subscriptions.
StoreKit Setup
Configure Products in App Store Connect
- 1Go to App Store Connect
- 2Select your app
- 3Navigate to "Features" > "In-App Purchases"
- 4Create products:
- Consumable: Items that can be purchased multiple times (coins, gems) - Non-Consumable: One-time purchase (remove ads, premium features) - Auto-Renewable Subscription: Recurring payments (premium access) - Non-Renewing Subscription: Time-limited access
StoreKit 2 Implementation
Product Model
swift
import StoreKit
enum ProductType {
case consumable
case nonConsumable
case subscription
}
struct StoreProduct: Identifiable {
let id: String
let type: ProductType
let product: Product
}
class StoreManager: ObservableObject {
@Published private(set) var products: [StoreProduct] = []
@Published private(set) var purchasedProductIDs: Set<String> = []
private let productIds: [String] = [
"com.yourapp.premium",
"com.yourapp.consumable100",
"com.yourapp.subscription.monthly"
]
init() {
Task {
await loadProducts()
await updatePurchasedProducts()
}
}
func loadProducts() async {
do {
let storeProducts = try await Product.products(for: productIds)
self.products = storeProducts.map { product in
let type: ProductType
switch product.type {
case .consumable:
type = .consumable
case .nonConsumable:
type = .nonConsumable
case .autoRenewable:
type = .subscription
default:
type = .nonConsumable
}
return StoreProduct(id: product.id, type: type, product: product)
}
} catch {
print("Failed to load products: \(error)")
}
}
}Purchasing Products
Purchase Flow
swift
extension StoreManager {
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await updatePurchasedProducts()
await transaction.finish()
return transaction
case .userCancelled:
print("User cancelled purchase")
return nil
case .pending:
print("Purchase pending")
return nil
@unknown default:
return nil
}
}
private func checkVerified<T>(_ verification: Verification<T>) throws -> T {
switch verification {
case .verified(let safe):
return safe
case .unverified:
throw StoreError.failedVerification
}
}
}
enum StoreError: Error {
case failedVerification
case productNotFound
}Subscription Management
Check Subscription Status
swift
extension StoreManager {
func checkSubscriptionStatus() async -> Bool {
for product in products where product.type == .subscription {
if await isPurchased(product.id) {
return true
}
}
return false
}
func isPurchased(_ productId: String) async -> Bool {
purchasedProductIDs.contains(productId)
}
func updatePurchasedProducts() async {
var purchasedIDs = Set<String>()
for await result in Transaction.updates {
do {
let transaction = try checkVerified(result)
purchasedIDs.insert(transaction.productID)
if transaction.revocationDate == nil {
// Transaction is valid
} else {
// Transaction was refunded
purchasedIDs.remove(transaction.productID)
}
} catch {
print("Transaction verification failed")
}
}
self.purchasedProductIDs = purchasedIDs
}
}Restore Purchases
swift
extension StoreManager {
func restorePurchases() async {
try? await AppStore.sync()
await updatePurchasedProducts()
}
}UI Implementation
Store View
swift
struct StoreView: View {
@StateObject private var storeManager = StoreManager()
@State private var isLoading = false
var body: some View {
NavigationView {
List {
ForEach(storeManager.products) { product in
ProductRow(product: product) {
Task {
await purchaseProduct(product)
}
}
}
}
.navigationTitle("Store")
.task {
await storeManager.loadProducts()
}
}
}
private func purchaseProduct(_ storeProduct: StoreProduct) async {
isLoading = true
defer { isLoading = false }
do {
if let transaction = try await storeManager.purchase(storeProduct.product) {
print("Purchase successful: \(transaction.productID)")
}
} catch {
print("Purchase failed: \(error)")
}
}
}Product Row
swift
struct ProductRow: View {
let product: StoreProduct
let action: () -> Void
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(product.product.displayName)
.font(.headline)
Text(product.product.description)
.font(.caption)
.foregroundColor(.secondary)
if product.type == .subscription {
Text("Subscription")
.font(.caption2)
.padding(4)
.background(Color.blue.opacity(0.2))
.cornerRadius(4)
}
}
Spacer()
Button(action: action) {
VStack(spacing: 4) {
Text(product.product.displayPrice)
.font(.headline)
if product.type == .subscription {
Text(product.product.subscription?.subscriptionPeriod.rawValue ?? "")
.font(.caption2)
}
}
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.blue)
.cornerRadius(10)
}
}
.padding(.vertical, 8)
}
}Receipt Validation
Server-Side Validation
swift
extension StoreManager {
func validateReceipt() async throws {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
throw StoreError.receiptNotFound
}
let receiptData = try Data(contentsOf: appStoreReceiptURL)
let base64Receipt = receiptData.base64EncodedString()
// Send to your server for validation
try await validateReceiptWithServer(base64Receipt)
}
private func validateReceiptWithServer(_ receipt: String) async throws {
// Implement server-side validation
}
}
enum StoreError: Error {
case receiptNotFound
case failedVerification
case productNotFound
}Best Practices
- 1Testing: Test thoroughly with StoreKit testing in Xcode
- 2Pricing: Use localized pricing with price formatting
- 3Validation: Always validate receipts server-side
- 4Restore: Provide restore purchases option
- 5UI: Make purchase flow intuitive
- 6Error Handling: Handle all error cases gracefully
- 7Subscriptions: Provide clear subscription terms
- 8Analytics: Track purchase events and conversions
Testing Purchases
StoreKit Configuration File
xml
<!-- Create a StoreKit configuration file in Xcode -->
<StoreKitConfiguration>
<Products>
<Product>
<ProductID>com.yourapp.premium</ProductID>
<Type>NonConsumable</Type>
<Price>9.99</Price>
</Product>
<Product>
<ProductID>com.yourapp.consumable100</ProductID>
<Type>Consumable</Type>
<Price>0.99</Price>
</Product>
</Products>
</StoreKitConfiguration>StoreKit provides a complete monetization solution for iOS apps!