axiom-storekit-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStoreKit 2 — Complete API Reference
StoreKit 2 — 完整API参考文档
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.
StoreKit 2是苹果推出的现代应用内购买框架,支持async/await API、自动收据验证以及SwiftUI集成。本参考资料涵盖所有API、iOS 18.4的增强功能,并附带全面的WWDC 2025代码示例。
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
相比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
- Async/Await: 采用现代并发模型,替代代理/闭包
- 自动验证: 内置JSON Web Signature (JWS)验证
- 交易类型: 使用强类型Swift类型,替代SKPaymentTransaction
- 测试: 支持StoreKit配置文件进行本地测试
- SwiftUI视图: 预构建的购买UI(ProductView、SubscriptionStoreView)
- 服务器API: App Store Server API和服务器通知
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:
- — Discipline skill with testing-first workflow, architecture patterns
axiom-in-app-purchases - (Future: agent for auditing existing IAP code)
iap-auditor - (Future: agent for implementing IAP from scratch)
iap-implementation
在以下场景使用本参考资料:
- 使用StoreKit 2实现应用内购买
- 了解iOS 18.4新增字段(appTransactionID、offerPeriod等)
- 查询特定API签名和参数
- 规划订阅架构
- 调试交易问题
- 实现StoreKit视图
- 集成App Store Server API
相关技能:
- — 采用测试优先工作流、架构模式的专业技能
axiom-in-app-purchases - (未来: agent,用于审计现有IAP代码)
iap-auditor - (未来: agent,用于从零开始实现IAP)
iap-implementation
Product
Product
Overview
概述
ProductProductLoading Products
加载产品
Basic Loading:
swift
import StoreKit
let productIDs = [
"com.app.coins_100",
"com.app.premium",
"com.app.pro_monthly"
]
let products = try await Product.products(for: productIDs)基础加载:
swift
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
来自WWDC 2021-10114
Handling Missing Products:
swift
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
}处理缺失的产品:
swift
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)")
// 产品未在App Store Connect或.storekit文件中配置
}Product Properties
Product属性
Basic Properties:
swift
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 // .nonConsumableProduct Type Enum:
swift
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
}基础属性:
swift
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产品类型枚举:
swift
switch product.type {
case .consumable:
// 游戏币、提示、增益
case .nonConsumable:
// 高级功能、关卡包
case .autoRenewable:
// 月度/年度订阅
case .nonRenewing:
// 季票
@unknown default:
break
}Subscription-Specific Properties
订阅专属属性
Check if Product is Subscription:
swift
if let subscriptionInfo = product.subscription {
// Product is auto-renewable subscription
let groupID = subscriptionInfo.subscriptionGroupID
let period = subscriptionInfo.subscriptionPeriod
}Subscription Period:
swift
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:
swift
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:
swift
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)")
}检查产品是否为订阅:
swift
if let subscriptionInfo = product.subscription {
// 产品是自动续订订阅
let groupID = subscriptionInfo.subscriptionGroupID
let period = subscriptionInfo.subscriptionPeriod
}订阅周期:
swift
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
}介绍性优惠:
swift
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
}
}促销优惠:
swift
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+):
swift
let product: Product
let scene: UIWindowScene
let result = try await product.purchase(confirmIn: scene)带UI上下文的购买(iOS 18.2+):
swift
let product: Product
let scene: UIWindowScene
let result = try await product.purchase(confirmIn: scene)From WWDC 2025-241:9:32
来自WWDC 2025-241:9:32
Purchase with Options:
swift
let accountToken = UUID()
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)带选项的购买:
swift
let accountToken = UUID()
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)From WWDC 2025-241:11:01
来自WWDC 2025-241:11:01
Purchase with Promotional Offer (JWS Format):
swift
let jwsSignature: String // From your server
let result = try await product.purchase(
confirmIn: scene,
options: [
.promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
]
)带促销优惠的购买(JWS格式):
swift
let jwsSignature: String // 来自你的服务器
let result = try await product.purchase(
confirmIn: scene,
options: [
.promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
]
)From WWDC 2025-241:10:55
来自WWDC 2025-241:10:55
Purchase with Custom Intro Eligibility:
swift
let jwsSignature: String // From your server
let result = try await product.purchase(
confirmIn: scene,
options: [
.introductoryOfferEligibility(signature: jwsSignature)
]
)带自定义介绍性优惠资格的购买:
swift
let jwsSignature: String // 来自你的服务器
let result = try await product.purchase(
confirmIn: scene,
options: [
.introductoryOfferEligibility(signature: jwsSignature)
]
)From WWDC 2025-241:10:42
来自WWDC 2025-241:10:42
SwiftUI Purchase (Using Environment):
swift
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)")
}
}
}
}
}SwiftUI购买(使用环境变量):
swift
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
来自WWDC 2025-241:9:50
PurchaseResult
PurchaseResult
Handling Purchase Results:
swift
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
}处理购买结果:
swift
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:
// 购买需要操作(请求购买、支付问题)
// 交易通过Transaction.updates在批准后送达
print("Purchase pending approval")
@unknown default:
break
}From WWDC 2025-241
来自WWDC 2025-241
Transaction
Transaction
Overview
概述
TransactionTransactionNew Fields (iOS 18.4)
iOS 18.4新增字段
appTransactionID:
swift
let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// Unique ID for app download (same across all purchases by same Apple Account)appTransactionID:
swift
let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// 应用下载的唯一ID(同一苹果账户的所有购买共用此ID)From WWDC 2025-241:4:13
来自WWDC 2025-241:4:13
offerPeriod:
swift
if let offerPeriod = transaction.offer?.period {
print("Offer duration: \(offerPeriod)")
// ISO 8601 duration format (e.g., "P1M" for 1 month)
}offerPeriod:
swift
if let offerPeriod = transaction.offer?.period {
print("Offer duration: \(offerPeriod)")
// ISO 8601时长格式(例如"P1M"代表1个月)
}From WWDC 2025-249:3:11
来自WWDC 2025-249:3:11
advancedCommerceInfo:
swift
if let advancedInfo = transaction.advancedCommerceInfo {
// Only present for Advanced Commerce API purchases
// nil for standard IAP
}advancedCommerceInfo:
swift
if let advancedInfo = transaction.advancedCommerceInfo {
// 仅在使用Advanced Commerce API购买时存在
// 标准IAP为nil
}From WWDC 2025-241:4:42
来自WWDC 2025-241:4:42
Essential Properties
核心属性
Basic Fields:
swift
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:
swift
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 .otherOffer Fields:
swift
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
}基础字段:
swift
let transaction: Transaction
transaction.id // 唯一交易ID
transaction.originalID // 原始交易ID(续订时保持一致)
transaction.productID // "com.app.pro_monthly"
transaction.productType // .autoRenewable
transaction.purchaseDate // 购买日期
transaction.appAccountToken // 购买时设置的UUID(如果提供)订阅字段:
swift
transaction.expirationDate // 订阅过期时间
transaction.isUpgraded // 如果用户升级到更高层级则为true
transaction.revocationDate // 退款日期(未退款则为nil)
transaction.revocationReason // .developerIssue或.other优惠字段:
swift
if let offer = transaction.offer {
offer.type // .introductory或.promotional或.code
offer.id // App Store Connect中的优惠标识符
offer.paymentMode // .freeTrial、.payAsYouGo、.payUpFront、.oneTime
}From WWDC 2025-241:8:00
来自WWDC 2025-241:8:00
Current Entitlements
当前权限
Get All Current Entitlements:
swift
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)
}
}获取所有当前权限:
swift
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
来自WWDC 2025-241
Get Entitlements for Specific Product (iOS 18.4+):
swift
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
}
}获取特定产品的权限(iOS 18.4+):
swift
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
来自WWDC 2025-241:3:31
Deprecated API (iOS 18.4):
swift
// ❌ 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) {
// ...
}已弃用API(iOS 18.4):
swift
// ❌ 在iOS 18.4中已弃用
let entitlement = await Transaction.currentEntitlement(for: productID)
// ✅ 请使用此替代方案(返回序列,支持家庭共享)
for await result in Transaction.currentEntitlements(for: productID) {
// ...
}From WWDC 2025-241:3:31
来自WWDC 2025-241:3:31
Transaction History
交易历史
Get All Transactions:
swift
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:
swift
for await result in Transaction.all(matching: productID) {
guard let transaction = try? result.payloadValue else {
continue
}
// All transactions for this product
}获取所有交易:
swift
for await result in Transaction.all {
guard let transaction = try? result.payloadValue else {
continue
}
print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}获取特定产品的交易:
swift
for await result in Transaction.all(matching: productID) {
guard let transaction = try? result.payloadValue else {
continue
}
// 该产品的所有交易
}Transaction Listener
交易监听器
Listen for Real-Time Updates (REQUIRED):
swift
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()
}监听实时更新(必填):
swift
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
来自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
交易来源:
- 应用内购买
- App Store购买(推广IAP)
- 优惠码兑换
- 订阅续订
- 家庭共享交易
- 待处理购买(请求购买)完成
- 退款通知
Verification
验证
VerificationResult:
swift
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
VerificationResult:
swift
let result: VerificationResult<Transaction>
switch result {
case .verified(let transaction):
// ✅ 交易由App Store签名
await grantEntitlement(for: transaction)
await transaction.finish()
case .unverified(let transaction, let error):
// ❌ 交易签名无效
print("Unverified: \(error)")
// 请勿授予权限
await transaction.finish() // 仍需完成以清除队列
}验证检查内容:
- 交易由App Store签名(非欺诈)
- 交易属于当前应用(Bundle ID匹配)
- 交易属于当前设备
Finishing Transactions
完成交易
Always Call finish():
swift
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
- re-emits transaction
Transaction.updates - Queue builds up over time
务必调用finish():
swift
await transaction.finish()何时完成:
- ✅ 向用户授予权限后
- ✅ 存储交易收据/ID后
- ✅ 即使是未验证的交易(清除队列)
- ✅ 即使是已退款的交易
不完成的后果:
- 应用下次启动时会重新发送交易
- 会重新发出交易
Transaction.updates - 队列会随时间堆积
AppTransaction
AppTransaction
Overview
概述
AppTransactionAppTransaction.sharedAppTransactionAppTransaction.sharedNew Fields (iOS 18.4)
iOS 18.4新增字段
appTransactionID:
swift
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)")
}appTransactionID:
swift
let appTransaction = try await AppTransaction.shared
switch appTransaction {
case .verified(let transaction):
let appTransactionID = transaction.appTransactionID
// 此苹果账户+应用的全局唯一ID
// 该值也会出现在Transaction和RenewalInfo中
case .unverified(_, let error):
print("AppTransaction verification failed: \(error)")
}From WWDC 2025-241:1:42
来自WWDC 2025-241:1:42
originalPlatform:
swift
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
}
}originalPlatform:
swift
if let appTransaction = try? await AppTransaction.shared.payloadValue {
let platform = appTransaction.originalPlatform
switch platform {
case .iOS:
print("最初下载于iPhone/iPad")
case .macOS:
print("最初下载于Mac")
case .tvOS:
print("最初下载于Apple TV")
case .visionOS:
print("最初下载于Vision Pro")
@unknown default:
break
}
}From WWDC 2025-241:2:11
来自WWDC 2025-241:2:11
Note: Apps downloaded on watchOS show
originalPlatform = .iOS注意: 在watchOS上下载的应用会显示
originalPlatform = .iOSEssential Properties
核心属性
swift
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 verificationswift
let appTransaction: AppTransaction
appTransaction.appVersion // "1.2.3"
appTransaction.originalAppVersion // "1.0.0"
appTransaction.originalPurchaseDate // 首次下载日期
appTransaction.bundleID // "com.company.app"
appTransaction.deviceVerification // 设备UUID
appTransaction.deviceVerificationNonce // 验证用随机数Use Cases
使用场景
Check App Version:
swift
if let appTransaction = try? await AppTransaction.shared.payloadValue {
if appTransaction.appVersion != currentVersion {
// Prompt user to update
}
}检查应用版本:
swift
if let appTransaction = try? await AppTransaction.shared.payloadValue {
if appTransaction.appVersion != currentVersion {
// 提示用户更新
}
}From WWDC 2025-241:0:51
来自WWDC 2025-241:0:51
Business Model Migration:
swift
// 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()
}商业模式迁移:
swift
// 从付费应用迁移到带IAP的免费应用
if appTransaction.originalPlatform == .iOS,
appTransaction.originalPurchaseDate < migrationDate {
// 用户在迁移前购买了应用 - 授予高级权限
await grantPremiumAccess()
}From WWDC 2025-241:2:32
来自WWDC 2025-241:2:32
Product.SubscriptionInfo.RenewalInfo
Product.SubscriptionInfo.RenewalInfo
Overview
概述
RenewalInfoRenewalInfoNew Fields (iOS 18.4)
iOS 18.4新增字段
appTransactionID:
swift
let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionIDappTransactionID:
swift
let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionIDFrom WWDC 2025-241:6:40
来自WWDC 2025-241:6:40
offerPeriod:
swift
if let offerPeriod = renewalInfo.offerPeriod {
print("Next renewal offer period: \(offerPeriod)")
// ISO 8601 duration (applies at next renewal)
}offerPeriod:
swift
if let offerPeriod = renewalInfo.offerPeriod {
print("Next renewal offer period: \(offerPeriod)")
// ISO 8601时长(下次续订时生效)
}From WWDC 2025-249:3:11
来自WWDC 2025-249:3:11
appAccountToken:
swift
if let token = renewalInfo.appAccountToken {
// UUID associating subscription with your server account
}appAccountToken:
swift
if let token = renewalInfo.appAccountToken {
// 将订阅与你的服务器账户关联的UUID
}From WWDC 2025-241:6:56
来自WWDC 2025-241:6:56
advancedCommerceInfo:
swift
if let advancedInfo = renewalInfo.advancedCommerceInfo {
// Only for Advanced Commerce API subscriptions
}advancedCommerceInfo:
swift
if let advancedInfo = renewalInfo.advancedCommerceInfo {
// 仅适用于Advanced Commerce API订阅
}From WWDC 2025-241:6:50
来自WWDC 2025-241:6:50
Essential Properties
核心属性
Renewal State:
swift
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:
swift
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
}续订状态:
swift
let renewalInfo: RenewalInfo
renewalInfo.willAutoRenew // 订阅是否会自动续订
renewalInfo.autoRenewPreference // 用户将续订到的产品ID
renewalInfo.expirationReason // 订阅过期原因(如果已过期)过期原因:
swift
switch renewalInfo.expirationReason {
case .autoRenewDisabled:
// 用户关闭了自动续订
case .billingError:
// 支付方式问题
case .didNotConsentToPriceIncrease:
// 用户未接受价格上涨 - 展示赢回优惠!
case .productUnavailable:
// 产品不再可用
case .unknown:
// 未知原因
@unknown default:
break
}From WWDC 2025-241:5:38
来自WWDC 2025-241:5:38
Grace Period:
swift
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
// Subscription in grace period - billing issue
// Show update payment method UI
}Price Increase Consent:
swift
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
}
}宽限期:
swift
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
// 订阅处于宽限期 - 支付问题
// 展示更新支付方式的UI
}价格上涨同意状态:
swift
if let consentStatus = renewalInfo.priceIncreaseStatus {
switch consentStatus {
case .agreed:
// 用户接受了价格上涨
case .notYetResponded:
// 用户尚未回应 - 展示同意UI
@unknown default:
break
}
}Accessing RenewalInfo
获取RenewalInfo
From SubscriptionStatus:
swift
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)")
}
}从SubscriptionStatus获取:
swift
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
Product.SubscriptionInfo.Status
Overview
概述
SubscriptionStatusSubscriptionStatusSubscription States
订阅状态
State Enum:
swift
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
}状态枚举:
swift
let status: Product.SubscriptionInfo.Status
switch status.state {
case .subscribed:
// 用户拥有活跃订阅 - 完整权限
case .expired:
// 订阅已过期 - 展示重新订阅/赢回优惠
case .inGracePeriod:
// 存在账单问题但仍保留权限 - 展示更新支付方式UI
case .inBillingRetryPeriod:
// Apple正在重试支付 - 保留权限
case .revoked:
// 家庭共享权限被移除 - 撤销权限
@unknown default:
break
}From WWDC 2025-241
来自WWDC 2025-241
Getting Subscription Status
获取订阅状态
For Subscription Group:
swift
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 }按订阅组获取:
swift
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
来自WWDC 2025-241:6:22
For Specific Transaction (iOS 18.4+):
swift
let transactionID = transaction.id
let status = try await Product.SubscriptionInfo.status(for: transactionID)按特定交易获取(iOS 18.4+):
swift
let transactionID = transaction.id
let status = try await Product.SubscriptionInfo.status(for: transactionID)From WWDC 2025-241:6:40
来自WWDC 2025-241:6:40
Listen for Status Updates:
swift
for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
// Process updated statuses
for status in statuses {
print("Status: \(status.state)")
}
}监听状态更新:
swift
for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
// 处理更新后的状态
for status in statuses {
print("Status: \(status.state)")
}
}Status Properties
状态属性
swift
let status: Product.SubscriptionInfo.Status
status.state // .subscribed, .expired, etc.
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>swift
let status: Product.SubscriptionInfo.Status
status.state // .subscribed、.expired等
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>StoreKit Views
StoreKit视图
ProductView (iOS 17+)
ProductView(iOS 17+)
Basic Usage:
swift
import StoreKit
struct ContentView: View {
let productID = "com.app.premium"
var body: some View {
ProductView(id: productID)
}
}基础用法:
swift
import StoreKit
struct ContentView: View {
let productID = "com.app.premium"
var body: some View {
ProductView(id: productID)
}
}From WWDC 2023-10013
来自WWDC 2023-10013
With Loaded Product:
swift
struct ContentView: View {
let product: Product
var body: some View {
ProductView(for: product)
}
}Custom Icon:
swift
ProductView(id: productID) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}Control Styles:
swift
ProductView(id: productID)
.productViewStyle(.regular) // Default
ProductView(id: productID)
.productViewStyle(.compact) // Smaller
ProductView(id: productID)
.productViewStyle(.large) // Prominent使用已加载的产品:
swift
struct ContentView: View {
let product: Product
var body: some View {
ProductView(for: product)
}
}自定义图标:
swift
ProductView(id: productID) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}控件样式:
swift
ProductView(id: productID)
.productViewStyle(.regular) // 默认
ProductView(id: productID)
.productViewStyle(.compact) // 小型
ProductView(id: productID)
.productViewStyle(.large) // 醒目StoreView (iOS 17+)
StoreView(iOS 17+)
Basic Store:
swift
struct ContentView: View {
let productIDs = [
"com.app.coins_100",
"com.app.coins_500",
"com.app.coins_1000"
]
var body: some View {
StoreView(ids: productIDs)
}
}基础商店视图:
swift
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
来自WWDC 2023-10013
With Loaded Products:
swift
struct ContentView: View {
let products: [Product]
var body: some View {
StoreView(products: products)
}
}使用已加载的产品:
swift
struct ContentView: View {
let products: [Product]
var body: some View {
StoreView(products: products)
}
}SubscriptionStoreView (iOS 17+)
SubscriptionStoreView(iOS 17+)
Basic Subscription Store:
swift
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")
}
}
}
}基础订阅商店视图:
swift
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
来自WWDC 2023-10013
Control Style:
swift
SubscriptionStoreView(groupID: groupID) {
// Marketing content
}
.subscriptionStoreControlStyle(.automatic) // Default
.subscriptionStoreControlStyle(.picker) // Horizontal picker
.subscriptionStoreControlStyle(.buttons) // Stacked buttons
.subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)控件样式:
swift
SubscriptionStoreView(groupID: groupID) {
// 营销内容
}
.subscriptionStoreControlStyle(.automatic) // 默认
.subscriptionStoreControlStyle(.picker) // 水平选择器
.subscriptionStoreControlStyle(.buttons) // 堆叠按钮
.subscriptionStoreControlStyle(.prominentPicker) // 大型选择器(iOS 18.4+)From WWDC 2025-241
来自WWDC 2025-241
SubscriptionOfferView (iOS 18.4+)
SubscriptionOfferView(iOS 18.4+)
Basic Offer View:
swift
struct ContentView: View {
let productID = "com.app.pro_monthly"
var body: some View {
SubscriptionOfferView(id: productID)
}
}基础优惠视图:
swift
struct ContentView: View {
let productID = "com.app.pro_monthly"
var body: some View {
SubscriptionOfferView(id: productID)
}
}From WWDC 2025-241:14:27
来自WWDC 2025-241:14:27
With Promotional Icon:
swift
SubscriptionOfferView(
id: productID,
prefersPromotionalIcon: true
)With Custom Icon:
swift
SubscriptionOfferView(id: productID) {
Image("custom-icon")
.resizable()
.frame(width: 60, height: 60)
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.gray)
}带促销图标:
swift
SubscriptionOfferView(
id: productID,
prefersPromotionalIcon: true
)带自定义图标:
swift
SubscriptionOfferView(id: productID) {
Image("custom-icon")
.resizable()
.frame(width: 60, height: 60)
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.gray)
}From WWDC 2025-241:15:14
来自WWDC 2025-241:15:14
With Detail Action:
swift
@State private var showStore = false
var body: some View {
SubscriptionOfferView(id: productID)
.subscriptionOfferViewDetailAction {
showStore = true
}
.sheet(isPresented: $showStore) {
SubscriptionStoreView(groupID: "pro_tier")
}
}带详情操作:
swift
@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
来自WWDC 2025-241:15:38
Visible Relationship:
swift
// 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
)可见关系:
swift
// 仅当客户可以升级时显示
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
来自WWDC 2025-241:17:44
With App Icon:
swift
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .all,
useAppIcon: true
)带应用图标:
swift
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .all,
useAppIcon: true
)From WWDC 2025-241:19:06
来自WWDC 2025-241:19:06
Offer Modifiers
优惠修饰符
Promotional Offer (JWS):
swift
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
}
)促销优惠(JWS):
swift
SubscriptionStoreView(groupID: groupID)
.subscriptionPromotionalOffer(
for: { subscription in
// 返回此订阅的优惠
return subscription.promotionalOffers.first
},
signature: { subscription, offer in
// 从服务器获取JWS签名
let signature = try await server.signOffer(
productID: subscription.id,
offerID: offer.id
)
return signature
}
)From WWDC 2025-241:12:17
来自WWDC 2025-241:12:17
Offer Codes (iOS 18.2+)
优惠码(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:
swift
func showOfferCodeSheet() {
guard let scene = view.window?.windowScene else { return }
StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}UIKit:
swift
func showOfferCodeSheet() {
guard let scene = view.window?.windowScene else { return }
StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}From WWDC 2025-241:7:38
来自WWDC 2025-241:7:38
SwiftUI:
swift
.offerCodeRedemption(isPresented: $showRedeemSheet)SwiftUI:
swift
.offerCodeRedemption(isPresented: $showRedeemSheet)Payment Mode
支付模式
New: .oneTime:
swift
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
}
}新增: .oneTime:
swift
let transaction: Transaction
if let offer = transaction.offer {
switch offer.paymentMode {
case .freeTrial:
// 优惠期内不收费
case .payAsYouGo:
// 每个计费周期的折扣价格
case .payUpFront:
// 整个周期的一次性折扣价格
case .oneTime:
// ✨ 新增:一次性优惠码兑换(iOS 17.2+)
@unknown default:
break
}
}From WWDC 2025-241:8:17
来自WWDC 2025-241:8:17
Legacy Access (iOS 15-17.1):
swift
if let offerMode = transaction.offerPaymentModeStringRepresentation {
// String representation for older OS versions
print(offerMode) // "oneTime"
}旧版系统兼容(iOS 15-17.1):
swift
if let offerMode = transaction.offerPaymentModeStringRepresentation {
// 旧版系统的字符串表示
print(offerMode) // "oneTime"
}From WWDC 2025-241:8:49
来自WWDC 2025-241:8:49
App Store Server Library
App Store Server Library
Overview
概述
Open-source library for signing IAP requests and decoding server API responses. Available in Swift, Java, Python, Node.js.
用于签署IAP请求和解码服务器API响应的开源库,支持Swift、Java、Python、Node.js。
Create Promotional Offer Signature
创建促销优惠签名
Swift Example:
swift
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 stringSwift示例:
swift
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 // 紧凑的JWS字符串From WWDC 2025-241:12:44, 2025-249
来自WWDC 2025-241:12:44, 2025-249
Server Endpoint Example:
swift
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
}服务器端点示例:
swift
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
来自WWDC 2025-241:12:52
App Store Server API
App Store Server API
Set App Account Token
设置应用账户令牌
Endpoint:
PATCH /inApps/v1/transactions/{originalTransactionId}Request Body:
json
{
"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
端点:
PATCH /inApps/v1/transactions/{originalTransactionId}请求体:
json
{
"appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}用途:
- 为应用外购买的交易设置appAccountToken(优惠码、App Store)
- 账户所有权变更时更新appAccountToken
- 将交易与你的服务器上的客户账户关联
From WWDC 2025-249:5:19
来自WWDC 2025-249:5:19
Get App Transaction Info
获取应用交易信息
Endpoint:
GET /inApps/v2/appTransaction/{transactionId}Response:
json
{
"signedAppTransactionInfo": "eyJhbGc..."
}Usage:
- Get app download information on server
- Check app version, platform, environment
- Available later in 2025
端点:
GET /inApps/v2/appTransaction/{transactionId}响应:
json
{
"signedAppTransactionInfo": "eyJhbGc..."
}用途:
- 在服务器上获取应用下载信息
- 检查应用版本、平台、环境
- 2025年晚些时候可用
From WWDC 2025-249:10:48
来自WWDC 2025-249:10:48
Send Consumption Information V2
发送消费信息V2
Endpoint:
PUT /inApps/v2/transactions/consumption/{transactionId}Request Body:
json
{
"customerConsented": true,
"sampleContentProvided": false,
"deliveryStatus": "DELIVERED",
"refundPreference": "GRANT_PRORATED",
"consumptionPercentage": 25000
}Fields:
- (required): User consented to send consumption data
customerConsented - (optional): Sample provided before purchase
sampleContentProvided - (required): "DELIVERED" or various UNDELIVERED statuses
deliveryStatus - (optional): "NO_REFUND", "GRANT_REFUND", "GRANT_PRORATED"
refundPreference - (optional): 0-100000 (millipercent, e.g., 25000 = 25%)
consumptionPercentage
Prorated Refund:
- New in 2025
- Supports partial consumption (consumables, non-consumables, non-renewing)
- For auto-renewable subscriptions, App Store calculates based on time remaining
端点:
PUT /inApps/v2/transactions/consumption/{transactionId}请求体:
json
{
"customerConsented": true,
"sampleContentProvided": false,
"deliveryStatus": "DELIVERED",
"refundPreference": "GRANT_PRORATED",
"consumptionPercentage": 25000
}字段说明:
- (必填): 用户同意发送消费数据
customerConsented - (可选): 购买前提供了样本
sampleContentProvided - (必填): "DELIVERED"或各种UNDELIVERED状态
deliveryStatus - (可选): "NO_REFUND"、"GRANT_REFUND"、"GRANT_PRORATED"
refundPreference - (可选): 0-100000(毫百分比,例如25000=25%)
consumptionPercentage
按比例退款:
- 2025年新增
- 支持部分消费的退款(消耗型、非消耗型、非续订订阅)
- 对于自动续订订阅,App Store会根据剩余时间计算退款金额
From WWDC 2025-249:16:09
来自WWDC 2025-249:16:09
Refund Notifications
退款通知
REFUND Notification:
json
{
"notificationType": "REFUND",
"data": {
"signedTransactionInfo": "...",
"refundPercentage": 75,
"revocationType": "REFUND_PRORATED"
}
}revocationType Values:
- : 100% refund - revoke all access
REFUND_FULL - : Partial refund - revoke proportional access
REFUND_PRORATED - : Family Sharing removed - revoke access
FAMILY_REVOKE
REFUND通知:
json
{
"notificationType": "REFUND",
"data": {
"signedTransactionInfo": "...",
"refundPercentage": 75,
"revocationType": "REFUND_PRORATED"
}
}revocationType取值:
- : 全额退款 - 撤销所有权限
REFUND_FULL - : 部分退款 - 按比例撤销权限
REFUND_PRORATED - : 家庭共享权限被移除 - 撤销权限
FAMILY_REVOKE
From WWDC 2025-249:20:17
来自WWDC 2025-249:20:17
Edge Cases
边缘情况
Family Sharing
家庭共享
Detect Family Shared Transactions:
swift
// 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:
swift
// Each family member has unique appTransactionID
// Use appTransactionID to identify individual family members检测家庭共享交易:
swift
// 家庭共享交易的appAccountToken不可用
let transaction: Transaction
if transaction.appAccountToken == nil {
// 可能是家庭共享交易(或未设置appAccountToken)
// 检查ownershipType(如果可用)
}家庭共享的订阅状态:
swift
// 每个家庭成员有唯一的appTransactionID
// 使用appTransactionID识别单个家庭成员From WWDC 2025-241:1:54
来自WWDC 2025-241:1:54
Refunds
退款
Handle Refund:
swift
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)
}
}处理退款:
swift
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
Advanced Commerce API
Check if Transaction Uses Advanced Commerce:
swift
if transaction.advancedCommerceInfo != nil {
// Transaction from Advanced Commerce API
// Large catalogs, creator experiences, subscriptions with add-ons
}More Info: Visit Advanced Commerce API documentation
检查交易是否使用Advanced Commerce:
swift
if transaction.advancedCommerceInfo != nil {
// 来自Advanced Commerce API的交易
// 适用于大型商品目录、创作者体验、带附加项的订阅
}更多信息: 访问Advanced Commerce API文档
From WWDC 2025-241:4:51
来自WWDC 2025-241:4:51
Win-Back Offers
赢回优惠
Show Win-Back for Expired Subscription:
swift
let renewalInfo: RenewalInfo
if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
// Perfect time for win-back offer!
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .current
)
.preferredSubscriptionOffer(offer: winBackOffer)
}为过期订阅展示赢回优惠:
swift
let renewalInfo: RenewalInfo
if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
// 展示赢回优惠的最佳时机!
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .current
)
.preferredSubscriptionOffer(offer: winBackOffer)
}From WWDC 2025-241:5:38
来自WWDC 2025-241:5:38
Testing
测试
StoreKit Configuration File
StoreKit配置文件
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)
创建:
- Xcode → File → New → StoreKit Configuration File
- 添加产品(消耗型、非消耗型、订阅)
- 配置价格、图片、描述
在Scheme中启用:
- Scheme → Edit Scheme → Run → Options
- StoreKit Configuration: 选择.storekit文件
测试场景:
- 成功购买
- 取消购买
- 订阅续订(加速时间)
- 订阅过期
- 升级/降级
- 优惠码兑换
- 家庭共享(在配置文件中启用)
Sandbox Testing
Sandbox测试
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
创建Sandbox账户:
- App Store Connect → Users and Access → Sandbox Testers
- 创建测试Apple ID
- 在设备的Settings → App Store → Sandbox Account中登录
清除购买历史:
- Settings → App Store → Sandbox Account → Clear Purchase History
Migration from StoreKit 1
从StoreKit 1迁移
Key Changes
主要变化
Delegates → Async/Await:
swift
// 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:
swift
// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)
// StoreKit 2
let transaction: Transaction // Automatically verified!Products → Product.products(for:):
swift
// StoreKit 1
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
// StoreKit 2
let products = try await Product.products(for: productIDs)代理 → Async/Await:
swift
// StoreKit 1
class StoreObserver: NSObject, SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// 处理交易
}
}
// StoreKit 2
for await result in Transaction.updates {
// 处理交易
}收据 → Transaction:
swift
// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)
// StoreKit 2
let transaction: Transaction // 自动验证!Products → Product.products(for:):
swift
// 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
WWDC: 2025-241, 2025-249, 2024-10061, 2024-10062, 2024-10110, 2023-10013, 2023-10140, 2022-10007, 2022-110404, 2021-10114
文档: /storekit
技能: axiom-in-app-purchases
Quick Reference
快速参考
Product Types
产品类型
- - Can purchase multiple times (coins, boosts)
.consumable - - Purchase once, own forever (premium, level packs)
.nonConsumable - - Auto-renewing subscriptions
.autoRenewable - - Fixed duration subscriptions
.nonRenewing
- - 可重复购买(游戏币、增益)
.consumable - - 一次性购买永久拥有(高级功能、关卡包)
.nonConsumable - - 自动续订订阅
.autoRenewable - - 固定时长订阅
.nonRenewing
Transaction States
交易状态
- - Purchase completed
success - - User tapped cancel
userCancelled - - Requires action (Ask to Buy)
pending
- - 购买完成
success - - 用户点击取消
userCancelled - - 需要操作(请求购买)
pending
Subscription States
Subscription States
- - Active subscription
.subscribed - - Subscription ended
.expired - - Billing issue, access maintained
.inGracePeriod - - Apple retrying payment
.inBillingRetryPeriod - - Family Sharing removed
.revoked
- - 活跃订阅
.subscribed - - 订阅已结束
.expired - - 账单问题,保留权限
.inGracePeriod - - Apple正在重试支付
.inBillingRetryPeriod - - 家庭共享权限被移除
.revoked
Essential Calls
核心调用
swift
// 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()swift
// 加载产品
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()