关于 axiom-storekit-ref
axiom-storekit-ref 是面向 Apple 平台开发者的 StoreKit 2 API 参考技能,涵盖购买、交易与订阅相关接口。内容包括 iOS 18.4 新字段及示例,适用于实现和排查应用内购买。
主要功能
- 01StoreKit 2 API 查询
- 02商品与订阅类型说明
- 03购买流程与结果处理
- 04交易验证及权益查询
- 05iOS 18.4 新字段参考
适用场景
- 01实现应用内购买
- 02规划订阅架构
- 03排查交易处理问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-storekit-ref安装后在 Claude Code 中输入/axiom-storekit-ref确认已加载。
详细介绍
StoreKit 2 — 完整 API 参考
概览
StoreKit 2 是 Apple 的现代应用内购买框架,提供 async/await API、自动收据验证和 SwiftUI 集成。本参考涵盖所有 API、iOS 18.4 增强功能,以及 WWDC 2025 的全面代码示例。
支持的产品类型
消耗型:
- 可多次购买的产品
- 示例:金币、提示、临时增益
- 不会在新设备上恢复
非消耗型:
- 购买一次后永久拥有的产品
- 示例:高级功能、关卡包、移除广告
- 必须在新设备上恢复
自动续期订阅:
- 自动续期的订阅
- 按订阅群组组织
- 必须在新设备上恢复
- 支持:免费试用、入门优惠、促销优惠、挽回优惠
非续期订阅:
- 固定期限的订阅(不自动续期)
- 示例:季节通行证
- 必须在新设备上恢复
相较于 StoreKit 1 的主要改进
- Async/Await:使用现代并发,而非代理/闭包
- 自动验证:内置 JSON Web Signature(JWS)验证
- 事务类型:使用强类型 Swift 类型,而非 SKPaymentTransaction
- 测试:用于本地测试的 StoreKit 配置文件
- SwiftUI 视图:预构建的购买界面(ProductView、SubscriptionStoreView)
- 服务器 API:App Store Server API 和服务器通知
何时使用本参考
在以下情况下使用本参考:
- 使用 StoreKit 2 实现应用内购买
- 了解 iOS 18.4 新增字段(appTransactionID、offerPeriod 等)
- 查阅特定 API 签名和参数
- 规划订阅架构
- 调试事务问题
- 实现 StoreKit 视图
- 与 App Store Server API 集成
相关技能:
axiom-in-app-purchases— 包含测试优先工作流和架构模式的规范技能- (未来:用于审查现有 IAP 代码的
iap-auditorAgent) - (未来:用于从头实现 IAP 的
iap-implementationAgent)
Product
概览
Product 表示在 App Store Connect 或 StoreKit 配置文件中配置的应用内购买项目。
加载产品
基本加载:
import StoreKit
let productIDs = [
"com.app.coins_100",
"com.app.premium",
"com.app.pro_monthly"
]
let products = try await Product.products(for: productIDs)
来自 WWDC 2021-10114
处理缺失的产品:
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
}
产品属性
基本属性:
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
产品类型枚举:
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
}
订阅专属属性
检查产品是否为订阅:
if let subscriptionInfo = product.subscription {
// Product is auto-renewable subscription
let groupID = subscriptionInfo.subscriptionGroupID
let period = subscriptionInfo.subscriptionPeriod
}
订阅期限:
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
}
入门优惠:
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
}
}
促销优惠:
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)")
}
购买方法
使用 UI 上下文购买(iOS 18.2+):
let product: Product
let scene: UIWindowScene
let result = try await product.purchase(confirmIn: scene)
来自 WWDC 2025-241:9:32
使用选项购买:
let accountToken = UUID()
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)
来自 WWDC 2025-241:11:01
使用促销优惠购买(JWS 格式):
let jwsSignature: String // From your server
let result = try await product.purchase(
confirmIn: scene,
options: [
.promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
]
)
来自 WWDC 2025-241:10:55
使用自定义入门优惠资格购买:
let jwsSignature: String // From your server
let result = try await product.purchase(
confirmIn: scene,
options: [
.introductoryOfferEligibility(signature: jwsSignature)
]
)
来自 WWDC 2025-241:10:42
SwiftUI 购买(使用 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)")
}
}
}
}
}
来自 WWDC 2025-241:9:50
PurchaseResult
处理购买结果:
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
}
来自 WWDC 2025-241
Transaction
概览
Transaction 表示成功的应用内购买。包含购买元数据、产品 ID、购买日期,以及订阅的到期日期。
新增字段(iOS 18.4)
appTransactionID:
let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// Unique ID for app download (same across all purchases by same Apple Account)
来自 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)
}
来自 WWDC 2025-249:3:11
advancedCommerceInfo:
if let advancedInfo = transaction.advancedCommerceInfo {
// Only present for Advanced Commerce API purchases
// nil for standard IAP
}
来自 WWDC 2025-241:4:42
重要属性
基本字段:
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)
订阅字段:
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
优惠字段:
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
}
来自 WWDC 2025-241:8:00
当前权益
获取所有当前权益:
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)
}
}
来自 WWDC 2025-241
获取特定产品的权益(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
}
}
来自 WWDC 2025-241:3:31
已弃用的 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) {
// ...
}
来自 WWDC 2025-241:3:31
交易历史
获取所有交易:
for await result in Transaction.all {
guard let transaction = try? result.payloadValue else {
continue
}
print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}
获取产品的交易:
for await result in Transaction.all(matching: productID) {
guard let transaction = try? result.payloadValue else {
continue
}
// All transactions for this product
}
交易监听器
监听实时更新(必需):
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()
}
来自 WWDC 2021-10114
交易来源:
- App 内购买
- 从 App Store 购买(推广的 IAP)
- 优惠码兑换
- 订阅续订
- 家人共享交易
- 完成的待处理购买(购买前询问)
- 退款通知
验证
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
}
验证内容:
- 交易由 App Store 签名(并非欺诈)
- 交易属于此 App(Bundle ID 匹配)
- 交易属于此设备
完成交易
始终调用 finish():
await transaction.finish()
何时完成:
- ✅ 向用户授予权益后
- ✅ 存储交易收据/ID 后
- ✅ 即使交易未经验证也要完成(以清空队列)
- ✅ 即使交易已退款也要完成
如果不完成会发生什么:
- 下次启动 App 时重新传递交易
Transaction.updates会再次发出交易- 队列会随时间不断累积
AppTransaction
概述
AppTransaction 表示 App 的首次下载。可通过 AppTransaction.shared 获取。
新增字段(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)")
}
来自 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
}
}
来自 WWDC 2025-241:2:11
注意:在 watchOS 上下载的 App 会显示 originalPlatform = .iOS
基本属性
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
使用场景
检查 App 版本:
if let appTransaction = try? await AppTransaction.shared.payloadValue {
if appTransaction.appVersion != currentVersion {
// Prompt user to update
}
}
来自 WWDC 2025-241:0:51
业务模式迁移:
// 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()
}
来自 WWDC 2025-241:2:32
Product.SubscriptionInfo.RenewalInfo
概述
RenewalInfo 提供自动续订订阅状态的信息,包括是否会续订、过期原因和即将推出的优惠。
新增字段(iOS 18.4)
appTransactionID:
let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID
来自 WWDC 2025-241:6:40
offerPeriod:
if let offerPeriod = renewalInfo.offerPeriod {
print("Next renewal offer period: \(offerPeriod)")
// ISO 8601 duration (applies at next renewal)
}
来自 WWDC 2025-249:3:11
appAccountToken:
if let token = renewalInfo.appAccountToken {
// UUID associating subscription with your server account
}
来自 WWDC 2025-241:6:56
advancedCommerceInfo:
if let advancedInfo = renewalInfo.advancedCommerceInfo {
// Only for Advanced Commerce API subscriptions
}
来自 WWDC 2025-241:6:50
基本属性
续订状态:
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)
过期原因:
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
}
来自 WWDC 2025-241:5:38
宽限期:
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
// Subscription in grace period - billing issue
// Show update payment method UI
}
价格上涨同意:
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
}
}
访问 RenewalInfo
从 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
概述
SubscriptionStatus 表示自动续订订阅的当前状态,包括订阅是否有效、已过期、处于宽限期或处于账单重试状态。
订阅状态
状态枚举:
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
}
来自 WWDC 2025-241
获取订阅状态
获取订阅群组的状态:
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 }
来自 WWDC 2025-241:6:22
获取特定交易的状态(iOS 18.4+):
let transactionID = transaction.id
let status = try await Product.SubscriptionInfo.status(for: transactionID)
来自 WWDC 2025-241:6:40
监听状态更新:
for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
// Process updated statuses
for status in statuses {
print("Status: \(status.state)")
}
}
状态属性
let status: Product.SubscriptionInfo.Status
status.state // .subscribed, .expired, etc.
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>
StoreKit 视图
ProductView(iOS 17+)
基本用法:
import StoreKit
struct ContentView: View {
let productID = "com.app.premium"
var body: some View {
ProductView(id: productID)
}
}
来自 WWDC 2023-10013
使用已加载的产品:
struct ContentView: View {
let product: Product
var body: some View {
ProductView(for: product)
}
}
自定义图标:
ProductView(id: productID) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
控件样式:
ProductView(id: productID)
.productViewStyle(.regular) // Default
ProductView(id: productID)
.productViewStyle(.compact) // Smaller
ProductView(id: productID)
.productViewStyle(.large) // Prominent
StoreView(iOS 17+)
基本商店:
struct ContentView: View {
let productIDs = [
"com.app.coins_100",
"com.app.coins_500",
"com.app.coins_1000"
]
var body: some View {
StoreView(ids: productIDs)
}
}
来自 WWDC 2023-10013
使用已加载的产品:
struct ContentView: View {
let products: [Product]
var body: some View {
StoreView(products: products)
}
}
SubscriptionStoreView(iOS 17+)
基本订阅商店:
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")
}
}
}
}
来自 WWDC 2023-10013
控件样式:
SubscriptionStoreView(groupID: groupID) {
// Marketing content
}
.subscriptionStoreControlStyle(.automatic) // Default
.subscriptionStoreControlStyle(.picker) // Horizontal picker
.subscriptionStoreControlStyle(.buttons) // Stacked buttons
.subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)
来自 WWDC 2025-241
SubscriptionOfferView(iOS 18.4+)
基本优惠视图:
struct ContentView: View {
let productID = "com.app.pro_monthly"
var body: some View {
SubscriptionOfferView(id: productID)
}
}
来自 WWDC 2025-241:14:27
使用已加载的产品:
let product: Product // Already loaded via Product.products(for:)
SubscriptionOfferView(product: product)
使用促销图标:
SubscriptionOfferView(
id: productID,
prefersPromotionalIcon: true
)
// Also available as modifier
SubscriptionOfferView(id: productID)
.prefersPromotionalIcon(true)
使用自定义图标:
SubscriptionOfferView(id: productID) {
Image("custom-icon")
.resizable()
.frame(width: 60, height: 60)
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.gray)
}
来自 WWDC 2025-241:15:14
使用详情操作:
@State private var showStore = false
var body: some View {
SubscriptionOfferView(id: productID)
.subscriptionOfferViewDetailAction {
showStore = true
}
.sheet(isPresented: $showStore) {
SubscriptionStoreView(groupID: "pro_tier")
}
}
来自 WWDC 2025-241:15:38
可见关系:
// 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
)
来自 WWDC 2025-241:17:44
使用 App 图标:
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .all,
useAppIcon: true
)
来自 WWDC 2025-241:19:06
优惠修饰符
促销优惠(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
}
)
来自 WWDC 2025-241:12:17
subscriptionStatusTask Modifier(iOS 18.4+)
使用 SwiftUI modifier 在应用层级跟踪订阅状态。自动响应状态变化,无需手动轮询。
基本用法:
@main
struct MyApp: App {
@State private var customerStatus: CustomerStatus = .unknown
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.customerSubscriptionStatus, customerStatus)
.subscriptionStatusTask(for: "your.group.id") { statuses in
if statuses.contains(where: { $0.state == .subscribed }) {
customerStatus = .subscribed
} else if statuses.contains(where: { $0.state == .expired }) {
customerStatus = .expired
} else {
customerStatus = .notSubscribed
}
}
}
}
}
关键行为:
- 在应用启动时触发,并提供当前状态
- 订阅状态变化时再次触发(续订、过期、升级)
- 将 StoreKit 状态转换为应用模型,让领域模型保持简单
- 添加到视图层级的顶层(App 或根 WindowGroup)
Offer Codes(iOS 18.2+)
概述
Offer codes 现在支持所有产品类型(此前仅支持订阅):
- 消耗型
- 非消耗型
- 非自动续订订阅
- 自动续订订阅
在应用中兑换
UIKit:
func showOfferCodeSheet() {
guard let scene = view.window?.windowScene else { return }
StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}
来自 WWDC 2025-241:7:38
SwiftUI:
.offerCodeRedemption(isPresented: $showRedeemSheet)
付款模式
新增:.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
}
}
来自 WWDC 2025-241:8:17
旧版访问方式(iOS 15-17.1):
if let offerMode = transaction.offerPaymentModeStringRepresentation {
// String representation for older OS versions
print(offerMode) // "oneTime"
}
来自 WWDC 2025-241:8:49
App Store Server Library
概述
用于签署 IAP 请求和解码服务器 API 响应的开源库。支持 Swift、Java、Python、Node.js。
创建促销优惠签名
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 string
来自 WWDC 2025-241:12:44、2025-249
服务器端点示例:
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
}
来自 WWDC 2025-241:12:52
App Store Server API
设置 App Account Token
端点:
PATCH /inApps/v1/transactions/{originalTransactionId}
请求正文:
{
"appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}
用法:
- 为在应用外进行的购买(offer codes、App Store)设置 appAccountToken
- 账户所有权变更时更新 appAccountToken
- 将交易与服务器上的客户账户关联
来自 WWDC 2025-249:5:19
获取 App Transaction 信息
端点:
GET /inApps/v2/appTransaction/{transactionId}
响应:
{
"signedAppTransactionInfo": "eyJhbGc..."
}
用法:
- 在服务器上获取应用下载信息
- 检查应用版本、平台和环境
- 将于 2025 年晚些时候推出
来自 WWDC 2025-249:10:48
发送消费信息 V2
端点:
PUT /inApps/v2/transactions/consumption/{transactionId}
请求正文:
{
"customerConsented": true,
"sampleContentProvided": false,
"deliveryStatus": "DELIVERED",
"refundPreference": "GRANT_PRORATED",
"consumptionPercentage": 25000
}
字段:
customerConsented(必填):用户同意发送消费数据sampleContentProvided(可选):购买前提供的样例deliveryStatus(必填):"DELIVERED" 或各种 UNDELIVERED 状态refundPreference(可选):"NO_REFUND"、"GRANT_REFUND"、"GRANT_PRORATED"consumptionPercentage(可选):0-100000(千分比,例如 25000 = 25%)
按比例退款:
- 2025 年新增
- 支持部分消费(消耗型、非消耗型、非自动续订)
- 对于自动续订订阅,App Store 会根据剩余时间计算
来自 WWDC 2025-249:16:09
退款通知
REFUND 通知:
{
"notificationType": "REFUND",
"data": {
"signedTransactionInfo": "...",
"refundPercentage": 75,
"revocationType": "REFUND_PRORATED"
}
}
revocationType 值:
REFUND_FULL:全额退款 — 撤销所有访问权限REFUND_PRORATED:部分退款 — 按比例撤销访问权限FAMILY_REVOKE:已移除家庭共享 — 撤销访问权限
来自 WWDC 2025-249:20:17
边缘情况
家庭共享
检测家庭共享交易:
// 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)
}
家庭共享的订阅状态:
// Each family member has unique appTransactionID
// Use appTransactionID to identify individual family members
来自 WWDC 2025-241:1:54
退款
处理退款:
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
Advanced Commerce API 支持:
- 大型内容目录的应用内购买
- 创作者体验(打赏、赞助)
- 提供可选附加项的订阅
检查交易是否使用 Advanced Commerce:
if transaction.advancedCommerceInfo != nil {
// Transaction from Advanced Commerce API
// Large catalogs, creator experiences, subscriptions with add-ons
}
可通过 Transaction 和 RenewalInfo 上的 advancedCommerceInfo 字段访问。标准 IAP 交易会返回 nil。
来自 WWDC 2025-241:4:51
挽回优惠
为已过期的订阅显示挽回优惠:
let renewalInfo: RenewalInfo
if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
// Perfect time for win-back offer!
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .current
)
.preferredSubscriptionOffer(offer: winBackOffer)
}
来自 WWDC 2025-241:5:38
测试
StoreKit 配置文件
创建:
- Xcode → 文件 → 新建 → StoreKit 配置文件
- 添加产品(消耗型、非消耗型、订阅)
- 配置价格、图片和描述
在 Scheme 中启用:
- Scheme → 编辑 Scheme → 运行 → 选项
- StoreKit 配置:选择 .storekit 文件
测试场景:
- 成功购买
- 取消购买
- 订阅续订(加速时间)
- 订阅到期
- 升级/降级
- 兑换优惠码
- 家庭共享(在配置文件中启用)
交易管理器
使用 Xcode 中的交易管理器窗口,在测试期间检查和操作交易:
- 手动创建交易(测试特定的购买流程)
- 修改交易属性(到期时间、续订状态)
- 测试订阅优惠场景
- 检查交易详情和验证状态
打开:调试 → StoreKit → 管理交易(运行时需使用 StoreKit 配置)
沙盒测试
创建沙盒账户:
- App Store Connect → 用户与访问权限 → 沙盒测试员
- 创建测试 Apple ID
- 在设备上登录:设置 → App Store → 沙盒账户
清除购买历史记录:
- 设置 → App Store → 沙盒账户 → 清除购买历史记录
从 StoreKit 1 迁移
主要变化
代理 → 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
}
收据 → 交易:
// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)
// StoreKit 2
let transaction: Transaction // Automatically verified!
产品 → 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)
资源
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
快速参考
产品类型
.consumable- 可多次购买(金币、增益道具).nonConsumable- 购买一次,永久拥有(高级版、关卡包).autoRenewable- 自动续订订阅.nonRenewing- 固定期限订阅
交易状态
success- 购买已完成userCancelled- 用户点击了取消pending- 需要执行操作(询问是否购买)
订阅状态
.subscribed- 订阅有效.expired- 订阅已结束.inGracePeriod- 存在账单问题,但仍保留访问权限.inBillingRetryPeriod- Apple 正在重试付款.revoked- 家庭共享已移除
必要调用
// 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()