StoreKit 2 Fundamentals
Introduction to StoreKit 2
StoreKit 2 is Apple's modern framework for in-app purchases, introduced in iOS 15. It provides a simpler, more reliable API compared to the original StoreKit framework.
Product Types
Apple supports four product types:
| Type | Description | Example |
|---|---|---|
| Consumable | Used up and can be repurchased | Coins, gems, lives |
| Non-Consumable | One-time purchase, permanent | Pro features, ad removal |
| Auto-Renewable Subscription | Recurring subscription | Monthly premium |
| Non-Renewing Subscription | Fixed duration, manual renewal | Season pass |
Setting Up Products in App Store Connect
- Navigate to your app in App Store Connect
- Go to In-App Purchases section
- Click the plus button to create a new product
- Choose the product type
- Set a unique Product ID (e.g., com.yourapp.coins100)
- Set pricing and metadata
Basic StoreKit 2 Implementation
import StoreKit
@Observable
class StoreManager {
var products: [Product] = []
var purchasedProductIDs: Set<String> = []
let productIDs: Set<String> = [
"com.yourapp.coins100",
"com.yourapp.pro",
"com.yourapp.removeads"
]
func loadProducts() async {
do {
products = try await Product.products(for: productIDs)
.sorted(by: { $0.price < $1.price })
} catch {
print("Failed to load products: \(error)")
}
}
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()
return transaction
case .userCancelled:
return nil
case .pending:
return nil
@unknown default:
return nil
}
}
}
Verifying Transactions
StoreKit 2 uses Swift result types to verify transactions:
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw StoreError.failedVerification(error)
case .verified(let value):
return value
}
}
Listening for Transaction Updates
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
await self.updatePurchasedProducts()
await transaction.finish()
} catch {
print("Transaction failed verification: \(error)")
}
}
}
}
Purchase Flow Implementation
Complete Purchase Flow
A robust purchase flow handles success, failure, and edge cases:
@Observable
class PurchaseManager {
var isLoading = false
var purchaseError: String?
var showPurchaseAlert = false
func purchaseProduct(_ product: Product) async {
isLoading = true
defer { isLoading = false }
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await handleSuccessfulPurchase(transaction)
await transaction.finish()
case .userCancelled:
break
case .pending:
purchaseError = "Purchase is pending. Check back later."
showPurchaseAlert = true
@unknown default:
break
}
} catch {
purchaseError = error.localizedDescription
showPurchaseAlert = true
}
}
private func handleSuccessfulPurchase(_ transaction: Transaction) async {
switch transaction.productType {
case .consumable:
await addConsumableBalance(transaction)
case .nonConsumable:
await unlockFeature(transaction.productID)
case .autoRenewable:
await activateSubscription(transaction)
default:
break
}
}
}
Consumable Purchases
Consumable products are used up and can be repurchased:
func addConsumableBalance(_ transaction: Transaction) async {
let productID = transaction.productID
let amounts: [String: Int] = [
"com.yourapp.coins100": 100,
"com.yourapp.coins500": 500,
"com.yourapp.coins1000": 1000
]
if let amount = amounts[productID] {
await gameState.addCoins(amount)
}
}
Restoring Purchases
Users can restore previous purchases on new devices:
func restorePurchases() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
await handleSuccessfulPurchase(transaction)
await transaction.finish()
}
}
}
Handling Pending Transactions
Some purchases require parental approval:
func checkPendingTransactions() async {
for await result in Transaction.pendingEntitlements {
if case .verified(let transaction) = result {
notificationManager.showPendingPurchase(transaction.productID)
}
}
}
Receipt Validation and Security
Why Validate Receipts?
Receipt validation ensures that purchases are legitimate and prevents fraud. Apple recommends server-side validation for security.
Server-Side Validation
For production apps, validate receipts on your server:
struct ReceiptValidator {
let serverURL: URL
func validate(_ receipt: String) async throws -> ReceiptStatus {
var request = URLRequest(url: serverURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = [
"receipt": receipt,
"password": "your-shared-secret"
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(ReceiptResponse.self, from: data)
return response.status == 0 ? .valid : .invalid
}
}
enum ReceiptStatus {
case valid
case invalid
case expired
}
Shared Secret
The shared secret is used for auto-renewable subscription validation:
- Go to App Store Connect > App > App Information
- Find the Shared Secret section
- Generate or view the shared secret
- Use it in server-side validation requests
Common Validation Errors
| Error Code | Meaning |
|---|---|
| 21002 | The data in the receipt is malformed |
| 21003 | The receipt could not be authenticated |
| 21004 | The shared secret is not valid |
| 21005 | The receipt server is not available |
| 21006 | The receipt is valid but has expired |
Best Practices
- Always validate receipts server-side in production
- Use the shared secret for subscription validation
- Handle network failures gracefully
- Cache validation results locally
- Implement retry logic for transient errors
Quiz
1. Which StoreKit 2 API is used to fetch available products?
2. What happens to a consumable product after purchase?
3. How should you listen for transaction updates in StoreKit 2?
4. What is the recommended approach for receipt validation in production?
Flashcards
Question
What are the four types of in-app purchase products?
Click to reveal answer
Answer
Consumable (used up), Non-Consumable (permanent), Auto-Renewable Subscription (recurring), Non-Renewing Subscription (fixed duration).
Question
What is the difference between Product.products(for:) and Transaction.updates?
Click to reveal answer
Answer
Product.products(for:) fetches available products. Transaction.updates listens for purchase transaction updates.
Question
How do you restore purchases in StoreKit 2?
Click to reveal answer
Answer
Iterate through Transaction.currentEntitlements to find and activate all previously purchased products.
Question
What is a pending transaction in StoreKit 2?
Click to reveal answer
Answer
A purchase that requires approval (e.g., parental controls) and has not yet been completed.
Revision Notes
Key Takeaways
- 1. StoreKit 2 provides a simpler async/await API for in-app purchases
- 2. Always listen for Transaction.updates to handle server-initiated purchases
- 3. Validate receipts server-side in production apps
- 4. Call transaction.finish() after processing every transaction
Interview Tips
- • Explain the different in-app purchase product types
- • Describe the complete purchase flow from product fetch to transaction finish
- • Discuss receipt validation strategies and why server-side is preferred
- • Know how to handle pending transactions and purchase restoration
Cheat Sheet
StoreKit 2 Quick Reference
- Product.products(for:) fetches products
- product.purchase() initiates purchase
- Transaction.updates listens for updates
- Transaction.currentEntitlements restores purchases
- Always call transaction.finish()
- Server-side receipt validation for production