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)
// Check what loaded let loadedIDs = Set(products.map { $0.id }) let missingIDs = Set(productIDs).subtracting(loadedIDs)
if !missingIDs.isEmpty { print("Missing products: (missingIDs)") // Products not configured in App Store Connect or .storekit file }
Product Properties
Basic Properties:
let product: Product
product.id // "com.app.premium" product.displayName // "Premium Upgrade" product.description // "Unlock all features" product.displayPrice // "$4.99" product.price // Decimal(4.99) product.type // .nonConsumable
Product Type Enum:
switch product.type { case .consumable: // Coins, hints, boosts case .nonConsumable: // Premium features, level packs case .autoRenewable: // Monthly/annual subscriptions case .nonRenewing: // Seasonal passes @unknown default: break }
Subscription-Specific Properties
Check if Product is Subscription:
if let subscriptionInfo = product.subscription { // Product is auto-renewable 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 // From your server
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 // From your server
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)
// Handle result
} 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): // Purchase succeeded - verify transaction guard let transaction = try? verificationResult.payloadValue else { print("Transaction verification failed") return }
// Grant entitlement
await grantEntitlement(for: transaction)
await transaction.finish()
case .userCancelled: // User tapped "Cancel" in payment sheet print("User cancelled purchase")
case .pending: // Purchase requires action (Ask to Buy, payment issue) // Transaction will arrive via Transaction.updates when approved 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 // Unique ID for app download (same across all purchases by same Apple Account)
From WWDC 2025-241:4:13
offerPeriod:
if let offerPeriod = transaction.offer?.period { print("Offer duration: (offerPeriod)") // ISO 8601 duration format (e.g., "P1M" for 1 month) }
From WWDC 2025-249:3:11
advancedCommerceInfo:
if let advancedInfo = transaction.advancedCommerceInfo { // Only present for Advanced Commerce API purchases // nil for standard IAP }
From WWDC 2025-241:4:42
Essential Properties
Basic Fields:
let transaction: Transaction
transaction.id // Unique transaction ID transaction.originalID // Original transaction ID (consistent across renewals) transaction.productID // "com.app.pro_monthly" transaction.productType // .autoRenewable transaction.purchaseDate // Date of purchase transaction.appAccountToken // UUID set at purchase time (if provided)
Subscription Fields:
transaction.expirationDate // When subscription expires transaction.isUpgraded // true if user upgraded to higher tier transaction.revocationDate // Date of refund (nil if not refunded) transaction.revocationReason // .developerIssue or .other
Offer Fields:
if let offer = transaction.offer { offer.type // .introductory or .promotional or .code offer.id // Offer identifier from App Store Connect offer.paymentMode // .freeTrial, .payAsYouGo, .payUpFront, .oneTime }
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 }
// Only include non-refunded transactions
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 { // User owns this product return true } }
From WWDC 2025-241:3:31
Deprecated API (iOS 18.4):
// ❌ Deprecated in iOS 18.4 let entitlement = await Transaction.currentEntitlement(for: productID)
// ✅ Use this instead (returns sequence, handles Family Sharing) 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 }
// All transactions for this product
}
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 }
// Grant or revoke entitlement
if transaction.revocationDate != nil {
await revokeEntitlement(for: transaction.productID)
} else {
await grantEntitlement(for: transaction)
}
// CRITICAL: Always finish 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): // ✅ Transaction signed by App Store await grantEntitlement(for: transaction) await transaction.finish()
case .unverified(let transaction, let error): // ❌ Transaction signature invalid print("Unverified: (error)") // DO NOT grant entitlement await transaction.finish() // Still finish to clear queue }
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 // Globally unique ID for this Apple Account + app // Same value appears in Transaction and RenewalInfo
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 // "1.2.3" appTransaction.originalAppVersion // "1.0.0" appTransaction.originalPurchaseDate // First download date appTransaction.bundleID // "com.company.app" appTransaction.deviceVerification // UUID for device appTransaction.deviceVerificationNonce // Nonce for verification
Use Cases
Check App Version:
if let appTransaction = try? await AppTransaction.shared.payloadValue { if appTransaction.appVersion != currentVersion { // Prompt user to update } }
From WWDC 2025-241:0:51
Business Model Migration:
// Moving from paid app to free app with IAP if appTransaction.originalPlatform == .iOS, appTransaction.originalPurchaseDate < migrationDate { // User paid for app before migration - grant premium 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)") // ISO 8601 duration (applies at next renewal) }
From WWDC 2025-249:3:11
appAccountToken:
if let token = renewalInfo.appAccountToken { // UUID associating subscription with your server account }
From WWDC 2025-241:6:56
advancedCommerceInfo:
if let advancedInfo = renewalInfo.advancedCommerceInfo { // Only for Advanced Commerce API subscriptions }
From WWDC 2025-241:6:50
Essential Properties
Renewal State:
let renewalInfo: RenewalInfo
renewalInfo.willAutoRenew // true if subscription will renew renewalInfo.autoRenewPreference // Product ID customer will renew to renewalInfo.expirationReason // Why subscription expired (if expired)
Expiration Reasons:
switch renewalInfo.expirationReason { case .autoRenewDisabled: // User turned off auto-renewal case .billingError: // Payment method issue case .didNotConsentToPriceIncrease: // User didn't accept price increase - show win-back offer! case .productUnavailable: // Product no longer available case .unknown: // Unknown reason @unknown default: break }
From WWDC 2025-241:5:38
Grace Period:
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate { // Subscription in grace period - billing issue // Show update payment method UI }
Price Increase Consent:
if let consentStatus = renewalInfo.priceIncreaseStatus { switch consentStatus { case .agreed: // User accepted price increase case .notYetResponded: // User hasn't responded - show consent UI @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: // User has active subscription - full access
case .expired: // Subscription expired - show resubscribe/win-back offer
case .inGracePeriod: // Billing issue but access maintained - show update payment UI
case .inBillingRetryPeriod: // Apple retrying payment - maintain access
case .revoked: // Family Sharing access removed - revoke access
@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)
// Find highest service level 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) { // Process updated statuses for status in statuses { print("Status: (status.state)") } }
Status Properties
let status: Product.SubscriptionInfo.Status
status.state // .subscribed, .expired, etc. status.transaction // VerificationResult<Transaction> status.renewalInfo // VerificationResult<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) // Default
ProductView(id: productID) .productViewStyle(.compact) // Smaller
ProductView(id: productID) .productViewStyle(.large) // Prominent
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) {
// Marketing content above subscription options
VStack {
Image("app-icon")
Text("Go Pro")
.font(.largeTitle.bold())
Text("Unlock all features")
}
}
}
}
From WWDC 2023-10013
Control Style:
SubscriptionStoreView(groupID: groupID) { // Marketing content } .subscriptionStoreControlStyle(.automatic) // Default .subscriptionStoreControlStyle(.picker) // Horizontal picker .subscriptionStoreControlStyle(.buttons) // Stacked buttons .subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)
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 Promotional Icon:
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:
// Only show if customer can upgrade SubscriptionOfferView( groupID: "pro_tier", visibleRelationship: .upgrade )
// Only show if customer can downgrade SubscriptionOfferView( groupID: "pro_tier", visibleRelationship: .downgrade )
// Show crossgrade options (same tier, different billing period) SubscriptionOfferView( groupID: "pro_tier", visibleRelationship: .crossgrade )
// Show current subscription (only if offer available) SubscriptionOfferView( groupID: "pro_tier", visibleRelationship: .current )
// Show any plan in group 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 offer for this subscription return subscription.promotionalOffers.first }, signature: { subscription, offer in // Get JWS signature from server let signature = try await server.signOffer( productID: subscription.id, offerID: offer.id ) return signature } )
From WWDC 2025-241:12:17
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: // No charge during offer period case .payAsYouGo: // Discounted price per billing period case .payUpFront: // One-time discounted price for entire duration case .oneTime: // ✨ New: One-time offer code redemption (iOS 17.2+) @unknown default: break } }
From WWDC 2025-241:8:17
Legacy Access (iOS 15-17.1):
if let offerMode = transaction.offerPaymentModeStringRepresentation { // String representation for older OS versions print(offerMode) // "oneTime" }
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
// Configure signing 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 )
// Create signature let productID = "com.app.pro_monthly" let offerID = "promo_winback" let transactionID = transaction.id // Optional but recommended
let signature = try creator.createSignature( productIdentifier: productID, subscriptionOfferIdentifier: offerID, applicationUsername: nil, nonce: UUID(), timestamp: Date().timeIntervalSince1970, transactionIdentifier: transactionID )
// Send signature to app return signature // Compact JWS string
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:
// appAccountToken is NOT available for family shared transactions let transaction: Transaction
if transaction.appAccountToken == nil { // Might be family shared (or appAccountToken not set) // Check ownershipType (if available) }
Subscription Status for Family Sharing:
// Each family member has unique appTransactionID // Use appTransactionID to identify individual family members
From WWDC 2025-241:1:54
Refunds
Handle Refund:
func handleTransaction(_ transaction: Transaction) async { if let revocationDate = transaction.revocationDate { // Transaction was refunded print("Refunded on (revocationDate)")
switch transaction.revocationReason {
case .developerIssue:
// Refund due to app issue
case .other:
// Other refund reason
@unknown default:
break
}
// Revoke entitlement
await revokeEntitlement(for: transaction.productID)
}
}
Advanced Commerce API
Check if Transaction Uses Advanced Commerce:
if transaction.advancedCommerceInfo != nil { // Transaction from Advanced Commerce API // Large catalogs, creator experiences, subscriptions with add-ons }
More Info: Visit Advanced Commerce API documentation
From WWDC 2025-241:4:51
Win-Back Offers
Show Win-Back for Expired Subscription:
let renewalInfo: RenewalInfo
if renewalInfo.expirationReason == .didNotConsentToPriceIncrease { // Perfect time for win-back offer! 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)
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:
// StoreKit 1 class StoreObserver: NSObject, SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { // Handle transactions } }
// StoreKit 2 for await result in Transaction.updates { // Handle transactions }
Receipt → Transaction:
// StoreKit 1 let receiptURL = Bundle.main.appStoreReceiptURL let receipt = try Data(contentsOf: receiptURL!)
// StoreKit 2 let transaction: Transaction // Automatically verified!
Products → Product.products(for:):
// StoreKit 1 let request = SKProductsRequest(productIdentifiers: Set(productIDs)) request.delegate = self request.start()
// StoreKit 2 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
// Load products try await Product.products(for: productIDs)
// Purchase try await product.purchase(confirmIn: scene)
// Current entitlements Transaction.currentEntitlements(for: productID)
// Transaction listener Transaction.updates
// Subscription status Product.SubscriptionInfo.status(for: groupID)
// Restore purchases try await AppStore.sync()
// Finish transaction (REQUIRED) await transaction.finish()