关于 storekit
面向 Swift 开发者的 StoreKit 2 内购与订阅实现指南,介绍产品加载、购买验证、权益管理和恢复流程,并涵盖 SwiftUI 商店视图及常见错误。
主要功能
- 01加载内购产品并发起购买
- 02验证交易并处理交易更新
- 03检查订阅状态与用户权益
- 04使用商店视图构建购买界面
- 05恢复购买并处理退款撤销
适用场景
- 01实现消耗型或永久解锁内购
- 02创建自动续订订阅与订阅页
- 03在退款或续订后更新访问权限
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill storekit安装后在 Claude Code 中输入/storekit确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/app-review-guidelines.mdstorekit-advanced.md详细介绍
StoreKit 2 应用内购买和订阅
使用 StoreKit 2 实现应用内购买、订阅、付费墙和 StoreKit 测试。使用现代的基于 Swift 的 Product、Transaction、
PurchaseAction、StoreView 和 SubscriptionStoreView API。除非需要支持旧版操作系统,否则请避免使用原始的
应用内购买 API(SKProduct、SKPaymentQueue)。
StoreKit 视图会自动发起购买。对于自定义控件,在 SwiftUI 中使用
PurchaseAction,在 UIKit/AppKit 中使用 purchase(confirmIn:options:),在 watchOS 上使用 product.purchase(options:)。
目录
- 产品类型
- 加载产品
- 购买流程
- Transaction.updates 监听器
- 权益检查
- SubscriptionStoreView(iOS 17+)
- StoreView(iOS 17+)
- 订阅状态检查
- 恢复购买项目
- App Transaction(应用购买验证)
- 购买选项
- SwiftUI 购买回调
- 常见错误
- 审核清单
- 参考资料
产品类型
| 类型 | 枚举成员 | 行为 |
|---|---|---|
| 消耗型 | .consumable | 使用一次,可再次购买(宝石、金币) |
| 非消耗型 | .nonConsumable | 一次性购买,永久有效(高级功能解锁) |
| 自动续期订阅 | .autoRenewable | 定期计费并自动续期 |
| 非续期订阅 | .nonRenewing | 限时访问,不自动续期 |
加载产品
将产品 ID 定义为常量。使用 Product.products(for:) 获取产品。
import StoreKit
enum ProductID {
static let premium = "com.myapp.premium"
static let gems100 = "com.myapp.gems100"
static let monthlyPlan = "com.myapp.monthly"
static let yearlyPlan = "com.myapp.yearly"
static let all: [String] = [premium, gems100, monthlyPlan, yearlyPlan]
}
let products = try await Product.products(for: ProductID.all)
for product in products {
print("\(product.displayName): \(product.displayPrice)")
}
购买流程
标准付费墙优先使用 StoreKit 视图,因为它们会自动发起购买、恢复购买项目并显示政策控件。对于自定义 SwiftUI 购买按钮,优先使用环境中的 PurchaseAction。在 watchOS 上使用直接的 product.purchase(options:),在 UIKit 或 AppKit 中使用 purchase(confirmIn:options:) 进行确认。务必处理每种 PurchaseResult,在授予访问权限前进行验证,可靠地交付购买内容,然后完成交易。
@Environment(\.purchase) private var purchase
func purchaseProduct(_ product: Product) async throws {
let result = try await purchase(product, options: [
.appAccountToken(userAccountToken)
])
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await deliverContent(for: transaction)
await transaction.finish()
case .userCancelled:
break
case .pending:
// Ask to Buy or deferred approval: show pending UI, no unlock yet.
showPendingApprovalMessage()
@unknown default:
break
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let value): return value
case .unverified(_, let error): throw error
}
}
Transaction.updates 监听器
应在应用启动时启动,而不是等到付费墙出现时再启动。它会捕获来自其他设备的购买、家庭共享变更、续期、“询问购买”批准、退款、撤销,以及 Apple 在启动后立即发送一次的未完成交易。让此任务在应用的整个生命周期内保持持有状态。
@main
struct MyApp: App {
private let transactionListener: Task<Void, Never>
init() {
transactionListener = Self.listenForTransactions()
}
var body: some Scene {
WindowGroup { ContentView() }
}
static func listenForTransactions() -> Task<Void, Never> {
Task(priority: .background) {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
await StoreManager.shared.updateEntitlements()
await transaction.finish()
}
}
}
}
权益检查
Transaction.currentEntitlements 会发出非消耗型产品、有效或处于宽限期的自动续期订阅,以及最新的非续期订阅交易(包括已完成的交易)。它不包括消耗型产品以及已退款或已撤销的产品。请单独跟踪消耗型产品的交付情况,并在授予访问权限前,对非续期订阅应用应用的过期政策。
@Observable
@MainActor
class StoreManager {
static let shared = StoreManager()
var purchasedProductIDs: Set<String> = []
var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }
func updateEntitlements() async {
var purchased = Set<String>()
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result,
transaction.revocationDate == nil {
if transaction.productType == .nonRenewing,
transaction.expirationDate.map({ $0 <= .now }) ?? true {
continue
}
purchased.insert(transaction.productID)
}
}
purchasedProductIDs = purchased
}
}
SwiftUI .currentEntitlementTask 修饰符
struct PremiumGatedView: View {
@State private var state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading
var body: some View {
Group {
switch state {
case .loading: ProgressView()
case .failure: PaywallView()
case .success(.some(.verified(let transaction))) where transaction.revocationDate == nil:
PremiumContentView()
case .success:
PaywallView()
}
}
.currentEntitlementTask(for: ProductID.premium) { state in
self.state = state
}
}
}
SubscriptionStoreView(iOS 17+)
用于订阅付费墙的内置 SwiftUI 视图。它会自动处理产品加载、购买界面和恢复购买项目。
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
.subscriptionStoreControlStyle(.prominentPicker)
.subscriptionStoreButtonLabel(.multiline)
.storeButton(.visible, for: .restorePurchases)
.storeButton(.visible, for: .redeemCode)
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
自定义营销内容
请使用 SubscriptionStoreView 控件样式中的容器背景和页眉模式。
层级布局
使用 SubscriptionOptionGroup、SubscriptionOptionSection 或 SubscriptionPeriodGroupSet 来组织 iOS 18+ 选项;请参阅
订阅组管理。
StoreView(iOS 17+)
以本地化名称、价格和购买按钮展示多个产品。
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
.productViewStyle(.large)
.storeButton(.visible, for: .restorePurchases)
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
单个产品的 ProductView
ProductView(id: ProductID.premium) { iconPhase in
switch iconPhase {
case .success(let image): image.resizable().scaledToFit()
case .loading: ProgressView()
default: Image(systemName: "star.fill")
}
}
.productViewStyle(.large)
订阅状态检查
func checkSubscriptionActive(groupID: String) async throws -> Bool {
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
for status in statuses {
guard case .verified = status.renewalInfo,
case .verified = status.transaction else { continue }
if status.state == .subscribed || status.state == .inGracePeriod {
return true
}
}
return false
}
续订状态
| 状态 | 含义 |
|---|---|
.subscribed | 订阅有效 |
.expired | 订阅已过期 |
.inBillingRetryPeriod | 付款失败,Apple 正在重试 |
.inGracePeriod | 付款失败,但在宽限期内仍可访问 |
.revoked | Apple 已退款或撤销订阅 |
恢复购买
StoreKit 2 通过 Transaction.currentEntitlements 处理恢复。添加恢复按钮,或显式调用 AppStore.sync()。
func restorePurchases() async throws {
try await AppStore.sync()
await StoreManager.shared.updateEntitlements()
}
在商店视图中:.storeButton(.visible, for: .restorePurchases)
App 交易(App 购买验证)
验证 App 安装的合法性。用于业务模式变更或检测被篡改的安装(iOS 16+)。
func verifyAppPurchase() async {
do {
let result = try await AppTransaction.shared
switch result {
case .verified(let appTransaction):
let originalVersion = appTransaction.originalAppVersion
let purchaseDate = appTransaction.originalPurchaseDate
// Migration logic for users who paid before subscription model
case .unverified:
// Potentially tampered -- restrict features as appropriate
break
}
} catch { /* Could not retrieve app transaction */ }
}
购买选项
// App account token for server-side reconciliation
try await product.purchase(options: [.appAccountToken(UUID())])
// Consumable quantity
try await product.purchase(options: [.quantity(5)])
// Simulate Ask to Buy in sandbox
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
SwiftUI 购买回调
.onInAppPurchaseStart { product in
await analytics.trackPurchaseStarted(product.id)
}
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
.inAppPurchaseOptions { product in
[.appAccountToken(userAccountToken)]
}
常见错误
1. 未在 App 启动时启动 Transaction.updates
// WRONG: No listener -- misses renewals, refunds, Ask to Buy approvals
@main struct MyApp: App {
var body: some Scene { WindowGroup { ContentView() } }
}
// CORRECT: Start listener in App init (see Transaction.updates section above)
2. 忘记调用 transaction.finish()
// WRONG: Never finished -- reappears in unfinished queue forever
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.
let transaction = try checkVerified(verification)
try await recordDelivery(transaction)
await transaction.finish()
3. 忽略验证结果
// WRONG: Using unverified transaction -- security risk
let transaction = verification.unsafePayloadValue
// CORRECT: Verify before using
let transaction = try checkVerified(verification)
4. 在新的 StoreKit 2 代码中使用原版 App 内购买 API
// AVOID: Original In-App Purchase APIs
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
// PREFERRED: StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
5. 未检查 revocationDate
// WRONG: Grants access to refunded purchases
if case .verified(let transaction) = result {
purchased.insert(transaction.productID)
}
// CORRECT: Skip revoked transactions
if case .verified(let transaction) = result, transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}
6. 将价格硬编码
// WRONG: Wrong for other currencies and regions
Text("Buy Premium for $4.99")
// CORRECT: Localized price from Product
Text("Buy \(product.displayName) for \(product.displayPrice)")
7. 未处理 .pending 购买结果
// WRONG: Silently drops pending Ask to Buy
default: break
// CORRECT: Explain approval is pending; unlock only after Transaction.updates
case .pending:
showPendingApprovalMessage()
8. 仅在启动时检查一次权益
// WRONG: Check once, never update
func appDidFinish() { Task { await updateEntitlements() } }
// CORRECT: Re-check on Transaction.updates AND on foreground return
// Transaction.updates listener handles mid-session changes.
// Also use .task { await storeManager.updateEntitlements() } on content views.
9. 缺少恢复购买按钮
// WRONG: No restore option -- App Store rejection risk
SubscriptionStoreView(groupID: "group_id")
// CORRECT
SubscriptionStoreView(groupID: "group_id")
.storeButton(.visible, for: .restorePurchases)
10. 订阅视图缺少政策链接
// WRONG: No terms or privacy policy
SubscriptionStoreView(groupID: "group_id")
// CORRECT
SubscriptionStoreView(groupID: "group_id")
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
审核清单
-
Transaction.updates监听器在 App 启动时于 App init 中启动 - 授予访问权限前验证所有交易
- 仅在内容已可靠交付后调用
transaction.finish() - 排除已撤销或退款的交易,并更新权益状态
-
.pending结果显示“询问购买”或延迟审批的反馈 - 付费墙和商店视图中显示恢复购买按钮
- 订阅视图中包含服务条款和隐私政策链接
- 使用
product.displayPrice显示价格,绝不硬编码 - 清楚显示订阅条款(价格、时长、续订)
- 清楚说明免费试用结束后的价格
- 除非旧版 OS 支持需要,否则不使用原版 App 内购买 API(
SKProduct、SKPaymentQueue) - 将产品 ID 定义为常量,而不是散落在各处的字符串
- StoreKit 测试覆盖促销优惠、挽回优惠、优惠码、“询问购买”、续订、退款和撤销
- 在 Transaction.updates 和 App 进入前台时重新检查权益
- 如适用,服务端验证使用
jwsRepresentation - 及时交付并完成消耗型项目的交易
- 跨并发边界共享时,交易观察器类型和产品模型类型均为
Sendable
参考资料
- 有关 IAP 规则(准则 3.1.1)、订阅显示要求和避免被拒的内容,请参阅 references/app-review-guidelines.md。
- 有关订阅控制样式、优惠管理、测试模式和高级订阅处理,请参阅 references/storekit-advanced.md。
- 提交、隐私、元数据、屏幕截图和拒绝风险审查,请使用
app-store-review。 - 关键词、屏幕截图说明、排名和转化策略,请使用
app-store-optimization。 - Apple 官方文档:选择 StoreKit API、Transaction.updates、Transaction.currentEntitlements、 SubscriptionStoreView 和 PurchaseAction。