| name | axiom-storekit-ref |
| description | Reference — Complete StoreKit 2 API guide covering Product, Transaction, AppTransaction, RenewalInfo, SubscriptionStatus, StoreKit Views, purchase options, server APIs, and all iOS 18.4 enhancements with WWDC 2025 code examples |
| license | MIT |
| compatibility | iOS 15+ (iOS 18.4+ for latest features) |
| metadata | {"version":"1.0.0","last-updated":"2025-12-08"} |
StoreKit 2 — Complete API Reference
Overview
StoreKit 2 is Apple's modern in-app purchase framework with async/await APIs, automatic receipt validation, and SwiftUI integration. This reference covers every API, iOS 18.4 enhancements, and comprehensive WWDC 2025 code examples.
Product Types Supported
Consumable:
- Products that can be purchased multiple times
- Examples: coins, hints, temporary boosts
- Do NOT restore on new devices
Non-Consumable:
- Products purchased once, owned forever
- Examples: premium features, level packs, remove ads
- MUST restore on new devices
Auto-Renewable Subscription:
- Subscriptions that renew automatically
- Organized into subscription groups
- MUST restore on new devices
- Support: free trials, intro offers, promotional offers, win-back offers
Non-Renewing Subscription:
- Fixed duration subscriptions (no auto-renewal)
- Examples: seasonal passes
- MUST restore on new devices
Key Improvements Over StoreKit 1
- Async/Await: Modern concurrency instead of delegates/closures
- Automatic Verification: JSON Web Signature (JWS) verification built-in
- Transaction Types: Strong Swift types instead of SKPaymentTransaction
- Testing: StoreKit configuration files for local testing
- SwiftUI Views: Pre-built purchase UIs (ProductView, SubscriptionStoreView)
- Server APIs: App Store Server API and Server Notifications
When to Use This Reference
Use this reference when:
- Implementing in-app purchases with StoreKit 2
- Understanding new iOS 18.4 fields (appTransactionID, offerPeriod, etc.)
- Looking up specific API signatures and parameters
- Planning subscription architecture
- Debugging transaction issues
- Implementing StoreKit Views
- Integrating with App Store Server APIs
Related Skills:
axiom-in-app-purchases — Discipline skill with testing-first workflow, architecture patterns
- (Future:
iap-auditor agent for auditing existing IAP code)
- (Future:
iap-implementation agent for implementing IAP from scratch)
Product
Overview
Product represents an in-app purchase item configured in App Store Connect or StoreKit configuration file.
Loading Products
Basic Loading:
import StoreKit
let productIDs = [
"com.app.coins_100",
"com.app.premium",
"com.app.pro_monthly"
]
let products = try await Product.products(for: productIDs)
From WWDC 2021-10114
Handling Missing Products:
let products = try await Product.products(for: productIDs)
let loadedIDs = Set(products.map { $0.id })
let missingIDs = Set(productIDs).subtracting(loadedIDs)
if !missingIDs.isEmpty {
print("Missing products: \(missingIDs)")
}
Product Properties
Basic Properties:
let product: Product
product.id
product.displayName
product.description
product.displayPrice
product.price
product.type
Product Type Enum:
switch product.type {
case .consumable:
case .nonConsumable:
case .autoRenewable:
case .nonRenewing:
@unknown default:
break
}
Subscription-Specific Properties
Check if Product is Subscription:
if let subscriptionInfo = product.subscription {
let groupID = subscriptionInfo.subscriptionGroupID
let period = subscriptionInfo.subscriptionPeriod
}
Subscription Period:
let period = product.subscription?.subscriptionPeriod
switch period?.unit {
case .day:
print("\(period?.value ?? 0) days")
case .week:
print("\(period?.value ?? 0) weeks")
case .month:
print("\(period?.value ?? 0) months")
case .year:
print("\(period?.value ?? 0) years")
default:
break
}
Introductory Offer:
if let introOffer = product.subscription?.introductoryOffer {
print("Free trial: \(introOffer.period.value) \(introOffer.period.unit)")
print("Price: \(introOffer.displayPrice)")
switch introOffer.paymentMode {
case .freeTrial:
print("Free trial - no charge")
case .payAsYouGo:
print("Discounted price per period")
case .payUpFront:
print("One-time discounted price")
@unknown default:
break
}
}
Promotional Offers:
let offers = product.subscription?.promotionalOffers ?? []
for offer in offers {
print("Offer ID: \(offer.id)")
print("Price: \(offer.displayPrice)")
print("Period: \(offer.period.value) \(offer.period.unit)")
}
Purchase Methods
Purchase with UI Context (iOS 18.2+):
let product: Product
let scene: UIWindowScene
let result = try await product.purchase(confirmIn: scene)
From WWDC 2025-241:9:32
Purchase with Options:
let accountToken = UUID()
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)
From WWDC 2025-241:11:01
Purchase with Promotional Offer (JWS Format):
let jwsSignature: String
let result = try await product.purchase(
confirmIn: scene,
options: [
.promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
]
)
From WWDC 2025-241:10:55
Purchase with Custom Intro Eligibility:
let jwsSignature: String
let result = try await product.purchase(
confirmIn: scene,
options: [
.introductoryOfferEligibility(signature: jwsSignature)
]
)
From WWDC 2025-241:10:42
SwiftUI Purchase (Using Environment):
struct ProductView: View {
let product: Product
@Environment(\.purchase) private var purchase
var body: some View {
Button("Buy \(product.displayPrice)") {
Task {
do {
let result = try await purchase(product)
} catch {
print("Purchase failed: \(error)")
}
}
}
}
}
From WWDC 2025-241:9:50
PurchaseResult
Handling Purchase Results:
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verificationResult):
guard let transaction = try? verificationResult.payloadValue else {
print("Transaction verification failed")
return
}
await grantEntitlement(for: transaction)
await transaction.finish()
case .userCancelled:
print("User cancelled purchase")
case .pending:
print("Purchase pending approval")
@unknown default:
break
}
From WWDC 2025-241
Transaction
Overview
Transaction represents a successful in-app purchase. Contains purchase metadata, product ID, purchase date, and for subscriptions, expiration date.
New Fields (iOS 18.4)
appTransactionID:
let transaction: Transaction
let appTransactionID = transaction.appTransactionID
From WWDC 2025-241:4:13
offerPeriod:
if let offerPeriod = transaction.offer?.period {
print("Offer duration: \(offerPeriod)")
}
From WWDC 2025-249:3:11
advancedCommerceInfo:
if let advancedInfo = transaction.advancedCommerceInfo {
}
From WWDC 2025-241:4:42
Essential Properties
Basic Fields:
let transaction: Transaction
transaction.id
transaction.originalID
transaction.productID
transaction.productType
transaction.purchaseDate
transaction.appAccountToken
Subscription Fields:
transaction.expirationDate
transaction.isUpgraded
transaction.revocationDate
transaction.revocationReason
Offer Fields:
if let offer = transaction.offer {
offer.type
offer.id
offer.paymentMode
}
From WWDC 2025-241:8:00
Current Entitlements
Get All Current Entitlements:
var purchasedProductIDs: Set<String> = []
for await result in Transaction.currentEntitlements {
guard let transaction = try? result.payloadValue else {
continue
}
if transaction.revocationDate == nil {
purchasedProductIDs.insert(transaction.productID)
}
}
From WWDC 2025-241
Get Entitlements for Specific Product (iOS 18.4+):
let productID = "com.app.premium"
for await result in Transaction.currentEntitlements(for: productID) {
if let transaction = try? result.payloadValue,
transaction.revocationDate == nil {
return true
}
}
From WWDC 2025-241:3:31
Deprecated API (iOS 18.4):
let entitlement = await Transaction.currentEntitlement(for: productID)
for await result in Transaction.currentEntitlements(for: productID) {
}
From WWDC 2025-241:3:31
Transaction History
Get All Transactions:
for await result in Transaction.all {
guard let transaction = try? result.payloadValue else {
continue
}
print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}
Get Transactions for Product:
for await result in Transaction.all(matching: productID) {
guard let transaction = try? result.payloadValue else {
continue
}
}
Transaction Listener
Listen for Real-Time Updates (REQUIRED):
func listenForTransactions() -> Task<Void, Never> {
Task.detached {
for await verificationResult in Transaction.updates {
await handleTransaction(verificationResult)
}
}
}
func handleTransaction(_ result: VerificationResult<Transaction>) async {
guard let transaction = try? result.payloadValue else {
return
}
if transaction.revocationDate != nil {
await revokeEntitlement(for: transaction.productID)
} else {
await grantEntitlement(for: transaction)
}
await transaction.finish()
}
From WWDC 2021-10114
Transaction Sources:
- In-app purchases
- Purchases from App Store (promoted IAP)
- Offer code redemptions
- Subscription renewals
- Family Sharing transactions
- Pending purchases (Ask to Buy) that complete
- Refund notifications
Verification
VerificationResult:
let result: VerificationResult<Transaction>
switch result {
case .verified(let transaction):
await grantEntitlement(for: transaction)
await transaction.finish()
case .unverified(let transaction, let error):
print("Unverified: \(error)")
await transaction.finish()
}
What Verification Checks:
- Transaction signed by App Store (not fraudulent)
- Transaction belongs to this app (bundle ID match)
- Transaction belongs to this device
Finishing Transactions
Always Call finish():
await transaction.finish()
When to finish:
- ✅ After granting entitlement to user
- ✅ After storing transaction receipt/ID
- ✅ Even for unverified transactions (to clear queue)
- ✅ Even for refunded transactions
What happens if you don't finish:
- Transaction redelivered on next app launch
Transaction.updates re-emits transaction
- Queue builds up over time
AppTransaction
Overview
AppTransaction represents the original app download. Available via AppTransaction.shared.
New Fields (iOS 18.4)
appTransactionID:
let appTransaction = try await AppTransaction.shared
switch appTransaction {
case .verified(let transaction):
let appTransactionID = transaction.appTransactionID
case .unverified(_, let error):
print("AppTransaction verification failed: \(error)")
}
From WWDC 2025-241:1:42
originalPlatform:
if let appTransaction = try? await AppTransaction.shared.payloadValue {
let platform = appTransaction.originalPlatform
switch platform {
case .iOS:
print("Originally downloaded on iPhone/iPad")
case .macOS:
print("Originally downloaded on Mac")
case .tvOS:
print("Originally downloaded on Apple TV")
case .visionOS:
print("Originally downloaded on Vision Pro")
@unknown default:
break
}
}
From WWDC 2025-241:2:11
Note: Apps downloaded on watchOS show originalPlatform = .iOS
Essential Properties
let appTransaction: AppTransaction
appTransaction.appVersion
appTransaction.originalAppVersion
appTransaction.originalPurchaseDate
appTransaction.bundleID
appTransaction.deviceVerification
appTransaction.deviceVerificationNonce
Use Cases
Check App Version:
if let appTransaction = try? await AppTransaction.shared.payloadValue {
if appTransaction.appVersion != currentVersion {
}
}
From WWDC 2025-241:0:51
Business Model Migration:
if appTransaction.originalPlatform == .iOS,
appTransaction.originalPurchaseDate < migrationDate {
await grantPremiumAccess()
}
From WWDC 2025-241:2:32
Product.SubscriptionInfo.RenewalInfo
Overview
RenewalInfo provides information about auto-renewable subscription renewal state, including whether it will renew, expiration reason, and upcoming offers.
New Fields (iOS 18.4)
appTransactionID:
let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID
From WWDC 2025-241:6:40
offerPeriod:
if let offerPeriod = renewalInfo.offerPeriod {
print("Next renewal offer period: \(offerPeriod)")
}
From WWDC 2025-249:3:11
appAccountToken:
if let token = renewalInfo.appAccountToken {
}
From WWDC 2025-241:6:56
advancedCommerceInfo:
if let advancedInfo = renewalInfo.advancedCommerceInfo {
}
From WWDC 2025-241:6:50
Essential Properties
Renewal State:
let renewalInfo: RenewalInfo
renewalInfo.willAutoRenew
renewalInfo.autoRenewPreference
renewalInfo.expirationReason
Expiration Reasons:
switch renewalInfo.expirationReason {
case .autoRenewDisabled:
case .billingError:
case .didNotConsentToPriceIncrease:
case .productUnavailable:
case .unknown:
@unknown default:
break
}
From WWDC 2025-241:5:38
Grace Period:
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
}
Price Increase Consent:
if let consentStatus = renewalInfo.priceIncreaseStatus {
switch consentStatus {
case .agreed:
case .notYetResponded:
@unknown default:
break
}
}
Accessing RenewalInfo
From SubscriptionStatus:
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
for status in statuses {
switch status.renewalInfo {
case .verified(let renewalInfo):
print("Will renew: \(renewalInfo.willAutoRenew)")
case .unverified(_, let error):
print("Renewal info verification failed: \(error)")
}
}
Product.SubscriptionInfo.Status
Overview
SubscriptionStatus represents the current state of an auto-renewable subscription, including whether it's active, expired, in grace period, or in billing retry.
Subscription States
State Enum:
let status: Product.SubscriptionInfo.Status
switch status.state {
case .subscribed:
case .expired:
case .inGracePeriod:
case .inBillingRetryPeriod:
case .revoked:
@unknown default:
break
}
From WWDC 2025-241
Getting Subscription Status
For Subscription Group:
let groupID = "pro_tier"
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
let activeStatus = statuses
.filter { $0.state == .subscribed }
.max { $0.transaction.productID < $1.transaction.productID }
From WWDC 2025-241:6:22
For Specific Transaction (iOS 18.4+):
let transactionID = transaction.id
let status = try await Product.SubscriptionInfo.status(for: transactionID)
From WWDC 2025-241:6:40
Listen for Status Updates:
for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
for status in statuses {
print("Status: \(status.state)")
}
}
Status Properties
let status: Product.SubscriptionInfo.Status
status.state
status.transaction
status.renewalInfo
StoreKit Views
ProductView (iOS 17+)
Basic Usage:
import StoreKit
struct ContentView: View {
let productID = "com.app.premium"
var body: some View {
ProductView(id: productID)
}
}
From WWDC 2023-10013
With Loaded Product:
struct ContentView: View {
let product: Product
var body: some View {
ProductView(for: product)
}
}
Custom Icon:
ProductView(id: productID) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
Control Styles:
ProductView(id: productID)
.productViewStyle(.regular)
ProductView(id: productID)
.productViewStyle(.compact)
ProductView(id: productID)
.productViewStyle(.large)
StoreView (iOS 17+)
Basic Store:
struct ContentView: View {
let productIDs = [
"com.app.coins_100",
"com.app.coins_500",
"com.app.coins_1000"
]
var body: some View {
StoreView(ids: productIDs)
}
}
From WWDC 2023-10013
With Loaded Products:
struct ContentView: View {
let products: [Product]
var body: some View {
StoreView(products: products)
}
}
SubscriptionStoreView (iOS 17+)
Basic Subscription Store:
struct SubscriptionView: View {
let groupID = "pro_tier"
var body: some View {
SubscriptionStoreView(groupID: groupID) {
VStack {
Image("app-icon")
Text("Go Pro")
.font(.largeTitle.bold())
Text("Unlock all features")
}
}
}
}
From WWDC 2023-10013
Control Style:
SubscriptionStoreView(groupID: groupID) {
}
.subscriptionStoreControlStyle(.automatic)
.subscriptionStoreControlStyle(.picker)
.subscriptionStoreControlStyle(.buttons)
.subscriptionStoreControlStyle(.prominentPicker)
From WWDC 2025-241
SubscriptionOfferView (iOS 18.4+)
Basic Offer View:
struct ContentView: View {
let productID = "com.app.pro_monthly"
var body: some View {
SubscriptionOfferView(id: productID)
}
}
From WWDC 2025-241:14:27
With Loaded Product:
let product: Product
SubscriptionOfferView(product: product)
With Promotional Icon:
SubscriptionOfferView(
id: productID,
prefersPromotionalIcon: true
)
SubscriptionOfferView(id: productID)
.prefersPromotionalIcon(true)
With Custom Icon:
SubscriptionOfferView(id: productID) {
Image("custom-icon")
.resizable()
.frame(width: 60, height: 60)
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.gray)
}
From WWDC 2025-241:15:14
With Detail Action:
@State private var showStore = false
var body: some View {
SubscriptionOfferView(id: productID)
.subscriptionOfferViewDetailAction {
showStore = true
}
.sheet(isPresented: $showStore) {
SubscriptionStoreView(groupID: "pro_tier")
}
}
From WWDC 2025-241:15:38
Visible Relationship:
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .upgrade
)
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .downgrade
)
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .crossgrade
)
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .current
)
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .all
)
From WWDC 2025-241:17:44
With App Icon:
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .all,
useAppIcon: true
)
From WWDC 2025-241:19:06
Offer Modifiers
Promotional Offer (JWS):
SubscriptionStoreView(groupID: groupID)
.subscriptionPromotionalOffer(
for: { subscription in
return subscription.promotionalOffers.first
},
signature: { subscription, offer in
let signature = try await server.signOffer(
productID: subscription.id,
offerID: offer.id
)
return signature
}
)
From WWDC 2025-241:12:17
subscriptionStatusTask Modifier (iOS 18.4+)
Track subscription status at the app level with a SwiftUI modifier. Eliminates manual polling by reacting to status changes automatically.
Basic Usage:
@main
struct MyApp: App {
@State private var customerStatus: CustomerStatus = .unknown
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.customerSubscriptionStatus, customerStatus)
.subscriptionStatusTask(for: "your.group.id") { statuses in
if statuses.contains(where: { $0.state == .subscribed }) {
customerStatus = .subscribed
} else if statuses.contains(where: { $0.state == .expired }) {
customerStatus = .expired
} else {
customerStatus = .notSubscribed
}
}
}
}
}
Key behavior:
- Fires on app launch with current statuses
- Fires again when subscription status changes (renewal, expiration, upgrade)
- Translate StoreKit statuses to your app's model — keep your domain model simple
- Attach at the top of your view hierarchy (App or root WindowGroup)
Offer Codes (iOS 18.2+)
Overview
Offer codes now support all product types (previously subscription-only):
- Consumables
- Non-consumables
- Non-renewing subscriptions
- Auto-renewable subscriptions
Redeem in App
UIKit:
func showOfferCodeSheet() {
guard let scene = view.window?.windowScene else { return }
StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}
From WWDC 2025-241:7:38
SwiftUI:
.offerCodeRedemption(isPresented: $showRedeemSheet)
Payment Mode
New: .oneTime:
let transaction: Transaction
if let offer = transaction.offer {
switch offer.paymentMode {
case .freeTrial:
case .payAsYouGo:
case .payUpFront:
case .oneTime:
@unknown default:
break
}
}
From WWDC 2025-241:8:17
Legacy Access (iOS 15-17.1):
if let offerMode = transaction.offerPaymentModeStringRepresentation {
print(offerMode)
}
From WWDC 2025-241:8:49
App Store Server Library
Overview
Open-source library for signing IAP requests and decoding server API responses. Available in Swift, Java, Python, Node.js.
Create Promotional Offer Signature
Swift Example:
import AppStoreServerLibrary
let signingKey = "YOUR_PRIVATE_KEY"
let keyID = "YOUR_KEY_ID"
let issuerID = "YOUR_ISSUER_ID"
let bundleID = "com.app.bundle"
let creator = PromotionalOfferV2SignatureCreator(
privateKey: signingKey,
keyID: keyID,
issuerID: issuerID,
bundleID: bundleID
)
let productID = "com.app.pro_monthly"
let offerID = "promo_winback"
let transactionID = transaction.id
let signature = try creator.createSignature(
productIdentifier: productID,
subscriptionOfferIdentifier: offerID,
applicationUsername: nil,
nonce: UUID(),
timestamp: Date().timeIntervalSince1970,
transactionIdentifier: transactionID
)
return signature
From WWDC 2025-241:12:44, 2025-249
Server Endpoint Example:
app.get("promo-offer") { req async throws -> String in
let productID = try req.query.get(String.self, at: "productID")
let offerID = try req.query.get(String.self, at: "offerID")
let signature = try creator.createSignature(
productIdentifier: productID,
subscriptionOfferIdentifier: offerID,
transactionIdentifier: nil
)
return signature
}
From WWDC 2025-241:12:52
App Store Server API
Set App Account Token
Endpoint:
PATCH /inApps/v1/transactions/{originalTransactionId}
Request Body:
{
"appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}
Usage:
- Set appAccountToken for purchases made outside your app (offer codes, App Store)
- Update appAccountToken when account ownership changes
- Associates transaction with customer account on your server
From WWDC 2025-249:5:19
Get App Transaction Info
Endpoint:
GET /inApps/v2/appTransaction/{transactionId}
Response:
{
"signedAppTransactionInfo": "eyJhbGc..."
}
Usage:
- Get app download information on server
- Check app version, platform, environment
- Available later in 2025
From WWDC 2025-249:10:48
Send Consumption Information V2
Endpoint:
PUT /inApps/v2/transactions/consumption/{transactionId}
Request Body:
{
"customerConsented": true,
"sampleContentProvided": false,
"deliveryStatus": "DELIVERED",
"refundPreference": "GRANT_PRORATED",
"consumptionPercentage": 25000
}
Fields:
customerConsented (required): User consented to send consumption data
sampleContentProvided (optional): Sample provided before purchase
deliveryStatus (required): "DELIVERED" or various UNDELIVERED statuses
refundPreference (optional): "NO_REFUND", "GRANT_REFUND", "GRANT_PRORATED"
consumptionPercentage (optional): 0-100000 (millipercent, e.g., 25000 = 25%)
Prorated Refund:
- New in 2025
- Supports partial consumption (consumables, non-consumables, non-renewing)
- For auto-renewable subscriptions, App Store calculates based on time remaining
From WWDC 2025-249:16:09
Refund Notifications
REFUND Notification:
{
"notificationType": "REFUND",
"data": {
"signedTransactionInfo": "...",
"refundPercentage": 75,
"revocationType": "REFUND_PRORATED"
}
}
revocationType Values:
REFUND_FULL: 100% refund - revoke all access
REFUND_PRORATED: Partial refund - revoke proportional access
FAMILY_REVOKE: Family Sharing removed - revoke access
From WWDC 2025-249:20:17
Edge Cases
Family Sharing
Detect Family Shared Transactions:
let transaction: Transaction
if transaction.appAccountToken == nil {
}
Subscription Status for Family Sharing:
From WWDC 2025-241:1:54
Refunds
Handle Refund:
func handleTransaction(_ transaction: Transaction) async {
if let revocationDate = transaction.revocationDate {
print("Refunded on \(revocationDate)")
switch transaction.revocationReason {
case .developerIssue:
case .other:
@unknown default:
break
}
await revokeEntitlement(for: transaction.productID)
}
}
Advanced Commerce API
The Advanced Commerce API enables support for:
- In-app purchases for large content catalogs
- Creator experiences (tipping, patronage)
- Subscriptions with optional add-ons
Check if Transaction Uses Advanced Commerce:
if transaction.advancedCommerceInfo != nil {
}
Accessible through the advancedCommerceInfo field on both Transaction and RenewalInfo. Returns nil for standard IAP transactions.
From WWDC 2025-241:4:51
Win-Back Offers
Show Win-Back for Expired Subscription:
let renewalInfo: RenewalInfo
if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .current
)
.preferredSubscriptionOffer(offer: winBackOffer)
}
From WWDC 2025-241:5:38
Testing
StoreKit Configuration File
Create:
- Xcode → File → New → StoreKit Configuration File
- Add products (consumables, non-consumables, subscriptions)
- Configure prices, images, descriptions
Enable in Scheme:
- Scheme → Edit Scheme → Run → Options
- StoreKit Configuration: Select .storekit file
Test Scenarios:
- Successful purchases
- Cancelled purchases
- Subscription renewals (accelerated time)
- Subscription expirations
- Upgrades/downgrades
- Offer code redemptions
- Family Sharing (enable in config file)
Transaction Manager
Use the Transaction Manager window in Xcode to inspect and manipulate transactions during testing:
- Create transactions manually (test specific purchase flows)
- Modify transaction properties (expiration, renewal state)
- Test subscription offer scenarios
- Inspect transaction details and verification status
Open: Debug → StoreKit → Manage Transactions (while running with StoreKit configuration)
Sandbox Testing
Create Sandbox Account:
- App Store Connect → Users and Access → Sandbox Testers
- Create test Apple ID
- Sign in on device Settings → App Store → Sandbox Account
Clear Purchase History:
- Settings → App Store → Sandbox Account → Clear Purchase History
Migration from StoreKit 1
Key Changes
Delegates → Async/Await:
class StoreObserver: NSObject, SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
}
}
for await result in Transaction.updates {
}
Receipt → Transaction:
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)
let transaction: Transaction
Products → Product.products(for:):
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
let products = try await Product.products(for: productIDs)
Resources
WWDC: 2025-241, 2025-249, 2024-10061, 2024-10062, 2024-10110, 2023-10013, 2023-10140, 2022-10007, 2022-110404, 2021-10114
Docs: /storekit
Skills: axiom-in-app-purchases
Quick Reference
Product Types
.consumable - Can purchase multiple times (coins, boosts)
.nonConsumable - Purchase once, own forever (premium, level packs)
.autoRenewable - Auto-renewing subscriptions
.nonRenewing - Fixed duration subscriptions
Transaction States
success - Purchase completed
userCancelled - User tapped cancel
pending - Requires action (Ask to Buy)
Subscription States
.subscribed - Active subscription
.expired - Subscription ended
.inGracePeriod - Billing issue, access maintained
.inBillingRetryPeriod - Apple retrying payment
.revoked - Family Sharing removed
Essential Calls
try await Product.products(for: productIDs)
try await product.purchase(confirmIn: scene)
Transaction.currentEntitlements(for: productID)
Transaction.updates
Product.SubscriptionInfo.status(for: groupID)
try await AppStore.sync()
await transaction.finish()