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
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
func handleTransaction(_ result: VerificationResult
// 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
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
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()