storekit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStoreKit 2 In-App Purchases and Subscriptions
StoreKit 2 应用内购买与订阅
Implement in-app purchases, subscriptions, and paywalls using StoreKit 2 on
iOS 26+. Use only the modern , , , and
APIs. Never use the deprecated original StoreKit
(, , ).
ProductTransactionStoreViewSubscriptionStoreViewSKProductSKPaymentQueueSKStoreReviewController在 iOS 26+ 平台上使用 StoreKit 2 实现应用内购买、订阅和支付墙功能。仅允许使用现代化的 、、 和 API,禁止使用已弃用的旧版 StoreKit 接口(、、)。
ProductTransactionStoreViewSubscriptionStoreViewSKProductSKPaymentQueueSKStoreReviewControllerProduct Types
产品类型
| Type | Enum Case | Behavior |
|---|---|---|
| Consumable | | Used once, can be repurchased (gems, coins) |
| Non-consumable | | Purchased once permanently (premium unlock) |
| Auto-renewable | | Recurring billing with automatic renewal |
| Non-renewing | | Time-limited access without automatic renewal |
| 类型 | 枚举值 | 行为特性 |
|---|---|---|
| 消耗型 | | 单次使用,可重复购买(如宝石、金币) |
| 非消耗型 | | 一次购买永久生效(如解锁高级功能) |
| 自动续费型 | | 定期扣费,自动续费 |
| 非续费型 | | 限时访问权限,不会自动续费 |
Loading Products
加载产品
Define product IDs as constants. Fetch products with .
Product.products(for:)swift
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)")
}将产品ID定义为常量,通过 方法获取产品信息。
Product.products(for:)swift
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)")
}Purchase Flow
购买流程
Call and handle all three cases.
Always verify and finish transactions.
product.purchase(options:)PurchaseResultswift
func purchase(_ product: Product) async throws {
let result = try await product.purchase(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 -- do not unlock content yet
break
@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
}
}调用 方法,处理 的所有三种返回情况,始终记得验证并完成交易。
product.purchase(options:)PurchaseResultswift
func purchase(_ product: Product) async throws {
let result = try await product.purchase(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:
// 购买请求待审批或 deferred 状态 -- 暂不解锁内容
break
@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 Listener
Transaction.updates 监听器
Start at app launch. Catches purchases from other devices, Family Sharing
changes, renewals, Ask to Buy approvals, refunds, and revocations.
swift
@main
struct MyApp: App {
private var transactionListener: Task<Void, Error>?
init() {
transactionListener = listenForTransactions()
}
var body: some Scene {
WindowGroup { ContentView() }
}
func listenForTransactions() -> Task<Void, Error> {
Task.detached {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
await StoreManager.shared.updateEntitlements()
await transaction.finish()
}
}
}
}在应用启动时启动监听器,可捕获其他设备的购买行为、家庭共享变更、续费、购买请求审批、退款、权限撤销等事件。
swift
@main
struct MyApp: App {
private var transactionListener: Task<Void, Error>?
init() {
transactionListener = listenForTransactions()
}
var body: some Scene {
WindowGroup { ContentView() }
}
func listenForTransactions() -> Task<Void, Error> {
Task.detached {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
await StoreManager.shared.updateEntitlements()
await transaction.finish()
}
}
}
}Entitlement Checking
权限校验
Use for non-consumable purchases and active
subscriptions. Always check .
Transaction.currentEntitlementsrevocationDateswift
@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 {
purchased.insert(transaction.productID)
}
}
purchasedProductIDs = purchased
}
}使用 校验非消耗型购买和有效订阅状态,始终检查 字段。
Transaction.currentEntitlementsrevocationDateswift
@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 {
purchased.insert(transaction.productID)
}
}
purchasedProductIDs = purchased
}
}SwiftUI .currentEntitlementTask Modifier
SwiftUI .currentEntitlementTask 修饰符
swift
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(let transaction):
if transaction != nil { PremiumContentView() }
else { PaywallView() }
}
}
.currentEntitlementTask(for: ProductID.premium) { state in
self.state = state
}
}
}swift
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(let transaction):
if transaction != nil { PremiumContentView() }
else { PaywallView() }
}
}
.currentEntitlementTask(for: ProductID.premium) { state in
self.state = state
}
}
}SubscriptionStoreView (iOS 17+)
SubscriptionStoreView(iOS 17+)
Built-in SwiftUI view for subscription paywalls. Handles product loading,
purchase UI, and restore purchases automatically.
swift
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(.verified(let transaction)) = result {
await transaction.finish()
}
}SwiftUI 内置的订阅支付墙视图,可自动处理产品加载、购买UI和购买恢复功能。
swift
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(.verified(let transaction)) = result {
await transaction.finish()
}
}Custom Marketing Content
自定义营销内容
swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
VStack {
Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
Text("Unlock Premium").font(.largeTitle.bold())
Text("Access all features").foregroundStyle(.secondary)
}
}
.containerBackground(.blue.gradient, for: .subscriptionStore)swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
VStack {
Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
Text("解锁高级版").font(.largeTitle.bold())
Text("访问全部功能").foregroundStyle(.secondary)
}
}
.containerBackground(.blue.gradient, for: .subscriptionStore)Hierarchical Layout
分层布局
swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
SubscriptionPeriodGroupSet()
}
.subscriptionStoreControlStyle(.picker)swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
SubscriptionPeriodGroupSet()
}
.subscriptionStoreControlStyle(.picker)StoreView (iOS 17+)
StoreView(iOS 17+)
Merchandises multiple products with localized names, prices, and purchase buttons.
swift
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
.productViewStyle(.large)
.storeButton(.visible, for: .restorePurchases)
.onInAppPurchaseCompletion { product, result in
if case .success(.verified(let transaction)) = result {
await transaction.finish()
}
}可展示多个产品,自带本地化名称、价格和购买按钮。
swift
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
.productViewStyle(.large)
.storeButton(.visible, for: .restorePurchases)
.onInAppPurchaseCompletion { product, result in
if case .success(.verified(let transaction)) = result {
await transaction.finish()
}
}ProductView for Individual Products
单个产品的 ProductView
swift
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)swift
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)Subscription Status Checking
订阅状态检查
swift
func checkSubscriptionActive(groupID: String) async throws -> Bool {
let statuses = try await Product.SubscriptionInfo.Status.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
}swift
func checkSubscriptionActive(groupID: String) async throws -> Bool {
let statuses = try await Product.SubscriptionInfo.Status.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
}Renewal States
续费状态说明
| State | Meaning |
|---|---|
| Active subscription |
| Subscription has expired |
| Payment failed, Apple is retrying |
| Payment failed but access continues during grace period |
| Apple refunded or revoked the subscription |
| 状态 | 含义 |
|---|---|
| 订阅有效 |
| 订阅已过期 |
| 支付失败,苹果正在重试扣款 |
| 支付失败,宽限期内仍可正常使用功能 |
| 苹果已退款或撤销订阅 |
Restore Purchases
恢复购买
StoreKit 2 handles restoration via . Add a
restore button or call explicitly.
Transaction.currentEntitlementsAppStore.sync()swift
func restorePurchases() async throws {
try await AppStore.sync()
await StoreManager.shared.updateEntitlements()
}On store views:
.storeButton(.visible, for: .restorePurchases)StoreKit 2 通过 处理恢复逻辑,可添加恢复按钮或显式调用 。
Transaction.currentEntitlementsAppStore.sync()swift
func restorePurchases() async throws {
try await AppStore.sync()
await StoreManager.shared.updateEntitlements()
}在商店视图上添加:
.storeButton(.visible, for: .restorePurchases)App Transaction (App Purchase Verification)
App 交易(应用购买验证)
Verify the legitimacy of the app installation. Use for business model changes
or detecting tampered installations (iOS 16+).
swift
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 */ }
}验证应用安装的合法性,可用于业务模式变更或检测篡改安装包(iOS 16+)。
swift
func verifyAppPurchase() async {
do {
let result = try await AppTransaction.shared
switch result {
case .verified(let appTransaction):
let originalVersion = appTransaction.originalAppVersion
let purchaseDate = appTransaction.originalPurchaseDate
// 针对订阅模式推出前付费的老用户做迁移逻辑
case .unverified:
// 可能是篡改安装包 -- 按需限制功能
break
}
} catch { /* 无法获取应用交易信息 */ }
}Purchase Options
购买选项
swift
// 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)])swift
// 用于服务端对账的应用账号令牌
try await product.purchase(options: [.appAccountToken(UUID())])
// 消耗型商品购买数量
try await product.purchase(options: [.quantity(5)])
// 沙箱环境模拟购买请求
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])SwiftUI Purchase Callbacks
SwiftUI 购买回调
swift
.onInAppPurchaseStart { product in
return true // Return false to cancel
}
.onInAppPurchaseCompletion { product, result in
if case .success(.verified(let transaction)) = result {
await transaction.finish()
}
}
.inAppPurchaseOptions { product in
[.appAccountToken(userAccountToken)]
}swift
.onInAppPurchaseStart { product in
return true // 返回 false 可取消购买
}
.onInAppPurchaseCompletion { product, result in
if case .success(.verified(let transaction)) = result {
await transaction.finish()
}
}
.inAppPurchaseOptions { product in
[.appAccountToken(userAccountToken)]
}Common Mistakes
常见错误
1. Not starting Transaction.updates at app launch
1. 应用启动时未启动 Transaction.updates 监听器
swift
// 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)swift
// 错误:没有监听器 -- 会遗漏续费、退款、购买请求审批等事件
@main struct MyApp: App {
var body: some Scene { WindowGroup { ContentView() } }
}
// 正确:在 App 初始化方法中启动监听器(参考上文 Transaction.updates 章节)2. Forgetting transaction.finish()
2. 忘记调用 transaction.finish()
swift
// WRONG: Never finished -- reappears in unfinished queue forever
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
// CORRECT: Always finish after delivering content
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
await transaction.finish()swift
// 错误:未完成交易 -- 会一直出现在未完成交易队列中
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
// 正确:交付内容后始终调用 finish 方法
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
await transaction.finish()3. Ignoring verification result
3. 忽略验证结果
swift
// WRONG: Using unverified transaction -- security risk
let transaction = verification.unsafePayloadValue
// CORRECT: Verify before using
let transaction = try checkVerified(verification)swift
// 错误:使用未验证的交易 -- 存在安全风险
let transaction = verification.unsafePayloadValue
// 正确:使用前先验证
let transaction = try checkVerified(verification)4. Using deprecated original StoreKit APIs
4. 使用已弃用的旧版 StoreKit API
swift
// WRONG: Original StoreKit (deprecated)
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
SKStoreReviewController.requestReview()
// CORRECT: StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
try await AppStore.requestReview(in: windowScene)swift
// 错误:旧版 StoreKit(已弃用)
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
SKStoreReviewController.requestReview()
// 正确:使用 StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
try await AppStore.requestReview(in: windowScene)5. Not checking revocationDate
5. 未检查 revocationDate
swift
// 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)
}swift
// 错误:会给已退款的购买授予权限
if case .verified(let transaction) = result {
purchased.insert(transaction.productID)
}
// 正确:跳过已撤销的交易
if case .verified(let transaction) = result, transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}6. Hardcoding prices
6. 硬编码价格
swift
// 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)")swift
// 错误:不适用于其他货币和地区
Text("购买高级版仅需 $4.99")
// 正确:使用 Product 提供的本地化价格
Text("购买 \(product.displayName) 仅需 \(product.displayPrice)")7. Not handling .pending purchase result
7. 未处理 .pending 购买结果
swift
// WRONG: Silently drops pending Ask to Buy
default: break
// CORRECT: Inform user purchase is awaiting approval
case .pending:
showPendingApprovalMessage()swift
// 错误:静默忽略待审批的购买请求
default: break
// 正确:告知用户购买正在等待审批
case .pending:
showPendingApprovalMessage()8. Checking entitlements only once at launch
8. 仅在启动时校验一次权限
swift
// 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.swift
// 错误:仅校验一次,不会更新
func appDidFinish() { Task { await updateEntitlements() } }
// 正确:在 Transaction.updates 触发和应用回到前台时重新校验
// Transaction.updates 监听器会处理会话中的状态变更
// 同时在内容视图上使用 .task { await storeManager.updateEntitlements() }9. Missing restore purchases button
9. 缺失恢复购买按钮
swift
// WRONG: No restore option -- App Store rejection risk
SubscriptionStoreView(groupID: "group_id")
// CORRECT
SubscriptionStoreView(groupID: "group_id")
.storeButton(.visible, for: .restorePurchases)swift
// 错误:没有恢复选项 -- 有被 App Store 拒绝的风险
SubscriptionStoreView(groupID: "group_id")
// 正确写法
SubscriptionStoreView(groupID: "group_id")
.storeButton(.visible, for: .restorePurchases)10. Subscription views without policy links
10. 订阅视图没有政策链接
swift
// 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)swift
// 错误:没有服务条款和隐私政策链接
SubscriptionStoreView(groupID: "group_id")
// 正确写法
SubscriptionStoreView(groupID: "group_id")
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)Review Checklist
审核检查清单
- listener starts at app launch in App init
Transaction.updates - All transactions verified before granting access
- called after content delivery
transaction.finish() - Revoked transactions excluded from entitlements
- purchase result handled for Ask to Buy
.pending - Restore purchases button visible on paywall and store views
- Terms of Service and Privacy Policy links on subscription views
- Prices shown using , never hardcoded
product.displayPrice - Subscription terms (price, duration, renewal) clearly displayed
- Free trial states post-trial pricing clearly
- No original StoreKit APIs (,
SKProduct)SKPaymentQueue - Product IDs defined as constants, not scattered strings
- StoreKit configuration file set up for testing
- Entitlements re-checked on Transaction.updates and app foreground
- Server-side validation uses if applicable
jwsRepresentation - Consumables delivered and finished promptly
- Transaction observer types and product model types are when shared across concurrency boundaries
Sendable
- 监听器在 App 初始化时启动
Transaction.updates - 所有交易在授予权限前都经过验证
- 内容交付后调用
transaction.finish() - 已撤销的交易不会被计入有效权限
- 已处理 购买结果应对购买请求场景
.pending - 支付墙和商店页面显示恢复购买按钮
- 订阅页面有服务条款和隐私政策链接
- 价格通过 展示,没有硬编码
product.displayPrice - 订阅条款(价格、时长、续费规则)清晰展示
- 免费试用明确说明试用后的定价
- 没有使用旧版 StoreKit API(、
SKProduct)SKPaymentQueue - 产品ID定义为常量,没有散落的字符串
- 已配置 StoreKit 测试用的配置文件
- 在 Transaction.updates 触发和应用回到前台时重新校验权限
- 服务端验证按需使用
jwsRepresentation - 消耗型商品及时交付并标记交易完成
- 跨并发边界共享的交易观察者类型和产品模型类型遵循 协议
Sendable
Cross-References
交叉参考
- See for IAP rules (Guideline 3.1.1), subscription display requirements, and rejection prevention.
references/app-review-guidelines.md - See for subscription control styles, offer management, testing patterns, and advanced subscription handling.
references/storekit-advanced.md
- 查看 了解应用内购买规则(指南3.1.1)、订阅展示要求和避免被拒的方法。
references/app-review-guidelines.md - 查看 了解订阅控件样式、优惠管理、测试模式和高级订阅处理方案。
references/storekit-advanced.md