axiom-in-app-purchases
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStoreKit 2 In-App Purchase Implementation
StoreKit 2 应用内购买实现指南
Purpose: Guide robust, testable in-app purchase implementation
StoreKit Version: StoreKit 2
iOS Version: iOS 15+ (iOS 18.4+ for latest features)
Xcode: Xcode 13+ (Xcode 16+ recommended)
Context: WWDC 2025-241, 2025-249, 2023-10013, 2021-10114
用途:指导构建健壮、可测试的应用内购买实现方案
StoreKit 版本:StoreKit 2
iOS 版本:iOS 15+(iOS 18.4+ 支持最新功能)
Xcode:Xcode 13+(推荐使用 Xcode 16+)
参考资料:WWDC 2025-241、2025-249、2023-10013、2021-10114
When to Use This Skill
何时使用本技能
✅ Use this skill when:
- Implementing any in-app purchase functionality (new or existing)
- Adding consumable products (coins, hints, boosts)
- Adding non-consumable products (premium features, level packs)
- Adding auto-renewable subscriptions (monthly/annual plans)
- Debugging purchase failures, missing transactions, or restore issues
- Setting up StoreKit testing configuration
- Implementing subscription status tracking
- Adding promotional offers or introductory offers
- Server-side receipt validation
- Family Sharing support
❌ Do NOT use this skill for:
- StoreKit 1 (legacy API) - this skill focuses on StoreKit 2
- App Store Connect product configuration (separate documentation)
- Pricing strategy or business model decisions
✅ 建议使用场景:
- 实现任何应用内购买功能(新增或已有)
- 添加消耗品(金币、提示、增益道具等)
- 添加非消耗品(高级功能、关卡包等)
- 添加自动续订订阅(月度/年度套餐)
- 调试购买失败、交易丢失或恢复问题
- 配置 StoreKit 测试环境
- 实现订阅状态跟踪
- 添加促销优惠或首月优惠
- 服务端收据验证
- 家庭共享支持
❌ 请勿使用场景:
- StoreKit 1(旧版 API)——本技能聚焦 StoreKit 2
- App Store Connect 商品配置(参考独立文档)
- 定价策略或商业模式决策
⚠️ Already Wrote Code Before Creating .storekit Config?
⚠️ 已编写购买代码但未创建 .storekit 配置?
If you wrote purchase code before creating configuration, you have three options:
.storekit如果您在创建 配置前就编写了购买代码,有以下三种选择:
.storekitOption A: Delete and Start Over (Strongly Recommended)
选项 A:删除代码重新开始(强烈推荐)
Delete all IAP code and follow the testing-first workflow below. This reinforces correct habits and ensures you experience the full benefit of .storekit-first development.
Why this is best:
- Validates that you understand the workflow
- Catches product ID issues you might have missed
- Builds muscle memory for future IAP implementations
- Takes only 15-30 minutes for experienced developers
删除所有应用内购买代码,遵循下文的测试优先工作流重新实现。这能强化正确的开发习惯,确保您充分体验 .storekit 优先开发的优势。
为何这是最优选择:
- 验证您对工作流的理解
- 排查可能遗漏的商品 ID 问题
- 为未来的应用内购买实现积累经验
- 经验丰富的开发者仅需 15-30 分钟即可完成
Option B: Create .storekit Config Now (Acceptable with Caution)
选项 B:立即创建 .storekit 配置(谨慎选择)
Create the file now with your existing product IDs. Test everything works locally. Document in your PR that you tested in sandbox first.
.storekitTrade-offs:
- ✅ Keeps working code
- ✅ Adds local testing capability
- ❌ Misses product ID validation benefit
- ❌ Reinforces testing-after pattern
- ❌ Requires extra vigilance in code review
If choosing this path: Create .storekit immediately, verify locally, and commit a note explaining the approach.
使用现有商品 ID 创建 文件。在本地测试所有功能正常运行。在您的 PR 中记录已先在沙盒环境测试。
.storekit利弊权衡:
- ✅ 保留可运行的代码
- ✅ 增加本地测试能力
- ❌ 失去商品 ID 验证的优势
- ❌ 强化测试后置的开发模式
- ❌ 代码审查时需要额外警惕
若选择此路径:立即创建 .storekit 文件,在本地验证,并提交备注说明该实现方式。
Option C: Skip .storekit Entirely (Not Recommended)
选项 C:完全跳过 .storekit 配置(不推荐)
Commit without configuration, test only in sandbox.
.storekitWhy this is problematic:
- Teammates can't test purchases locally
- No validation of product IDs before runtime
- Harder iteration (requires App Store Connect)
- Missing documentation of product structure
Bottom line: Choose Option A if possible, Option B if pragmatic, never Option C.
不配置 ,仅在沙盒环境测试。
.storekit为何这一做法存在问题:
- 团队成员无法在本地测试购买流程
- 运行前无法验证商品 ID
- 迭代难度更大(依赖 App Store Connect)
- 缺失商品结构的文档记录
总结:尽可能选择选项 A,务实情况下可选择选项 B,绝对不要选择选项 C。
Core Philosophy: Testing-First Workflow
核心理念:测试优先工作流
Best Practice: Create and test StoreKit configuration BEFORE writing production purchase code.
最佳实践:在编写生产环境购买代码前,先创建并测试 StoreKit 配置。
Why .storekit-First Matters
为何 .storekit 优先至关重要
The recommended workflow is to create configuration before writing any purchase code. This isn't arbitrary - it provides concrete benefits:
.storekitImmediate product ID validation:
- Typos caught in Xcode, not at runtime
- Product configuration visible in project
- No App Store Connect dependency for testing
Faster iteration:
- Test purchases in simulator instantly
- No network requests during development
- Accelerated subscription renewal for testing
Team benefits:
- Anyone can test purchase flows locally
- Product catalog documented in code
- Code review includes purchase testing
Common objections addressed:
❓ "I already tested in sandbox" - Sandbox testing is valuable but comes later. Local testing with .storekit is faster and enables true TDD.
❓ "My code works" - Working code is great! Adding .storekit makes it easier for teammates to verify and maintain.
❓ "I've done this before" - Experience is valuable. The .storekit-first workflow makes experienced developers even more productive.
❓ "Time pressure" - Creating .storekit takes 10-15 minutes. The time saved in iteration pays back immediately.
推荐的工作流是先创建 配置,再编写任何购买代码。这并非随意要求——它能带来切实的好处:
.storekit即时的商品 ID 验证:
- 在 Xcode 中即可发现拼写错误,无需等到运行时
- 商品配置在项目中可见
- 测试无需依赖 App Store Connect
更快的迭代速度:
- 在模拟器中即时测试购买流程
- 开发期间无需网络请求
- 加速订阅续订的测试
团队协作优势:
- 任何人都能在本地测试购买流程
- 商品目录在代码中得到文档化
- 代码审查包含购买流程测试
常见疑问解答:
❓ "我已经在沙盒环境测试过了"——沙盒测试很有价值,但应在后期进行。使用 .storekit 进行本地测试速度更快,支持真正的测试驱动开发(TDD)。
❓ "我的代码可以正常运行"——能运行的代码很棒!添加 .storekit 能让团队成员更易验证和维护代码。
❓ "我之前做过类似的实现"——经验很宝贵。.storekit 优先的工作流能让经验丰富的开发者效率更高。
❓ "时间紧张"——创建 .storekit 文件仅需 10-15 分钟,迭代过程中节省的时间能立即回本。
The Recommended Workflow
推荐的工作流
StoreKit Config → Local Testing → Production Code → Unit Tests → Sandbox Testing
↓ ↓ ↓ ↓ ↓
.storekit Test purchases StoreManager Mock store Integration testWhy this order helps:
- StoreKit Config First: Defines products without App Store Connect dependency
- Local Testing: Validates product IDs and purchase flows immediately
- Production Code: Implements against validated product configuration
- Unit Tests: Verifies business logic with mocked store responses
- Sandbox Testing: Final validation in App Store environment
Benefits of following this workflow:
- Product IDs validated before writing code
- Faster development iteration
- Easier team collaboration
- Better test coverage
StoreKit 配置 → 本地测试 → 生产代码 → 单元测试 → 沙盒测试
↓ ↓ ↓ ↓ ↓
.storekit 测试购买流程 StoreManager 模拟商店 集成测试为何此顺序更合理:
- 先做 StoreKit 配置:无需依赖 App Store Connect 即可定义商品
- 本地测试:立即验证商品 ID 和购买流程
- 编写生产代码:基于已验证的商品配置实现功能
- 单元测试:通过模拟商店响应验证业务逻辑
- 沙盒测试:在 App Store 环境中完成最终验证
遵循此工作流的好处:
- 编写代码前先验证商品 ID
- 更快的开发迭代
- 更轻松的团队协作
- 更好的测试覆盖率
Mandatory Checklist
必检清单
Before marking IAP implementation complete, ALL items must be verified:
在标记应用内购买实现完成前,必须验证以下所有项:
Phase 1: Testing Foundation
阶段 1:测试基础
- Created configuration file with all products
.storekit - Verified each product type renders correctly in StoreKit preview
- Tested successful purchase flow for each product in Xcode
- Tested purchase failure scenarios (insufficient funds, cancelled)
- Tested restore purchases flow
- For subscriptions: tested renewal, expiration, and upgrade/downgrade
- 创建包含所有商品的 配置文件
.storekit - 验证每种商品类型在 StoreKit 预览中正确显示
- 在 Xcode 中测试每种商品的成功购买流程
- 测试购买失败场景(余额不足、用户取消等)
- 测试购买恢复流程
- 若涉及订阅:测试续订、过期和升级/降级流程
Phase 2: Architecture
阶段 2:架构设计
- Centralized StoreManager class exists (single source of truth)
- StoreManager is ObservableObject (SwiftUI) or uses NotificationCenter
- Transaction observer listens for updates via
Transaction.updates - All transaction verification uses
VerificationResult - All transactions call after entitlement granted
.finish() - Product loading happens at app launch or before displaying store
- 存在集中式 StoreManager 类(单一事实来源)
- StoreManager 是 ObservableObject(SwiftUI)或使用 NotificationCenter
- 交易监听器通过 监听更新
Transaction.updates - 所有交易验证均使用
VerificationResult - 所有交易在授予权限后调用
.finish() - 商品加载在应用启动时或显示商店前完成
Phase 3: Purchase Flow
阶段 3:购买流程
- Purchase uses new with UI context (iOS 18.2+)
purchase(confirmIn:options:) - Purchase handles all cases (success, userCancelled, pending)
PurchaseResult - Purchase verifies transaction signature before granting entitlement
- Purchase stores transaction receipt/identifier for support
- appAccountToken set for all purchases (if using server backend)
- 购买使用新增的 并传入 UI 上下文(iOS 18.2+)
purchase(confirmIn:options:) - 购买处理所有 情况(成功、用户取消、待处理)
PurchaseResult - 购买在授予权限前验证交易签名
- 购买存储交易收据/标识符以用于支持服务
- 所有购买均设置 appAccountToken(若使用服务端后端)
Phase 4: Subscription Management (if applicable)
阶段 4:订阅管理(如适用)
- Subscription status tracked via
Product.SubscriptionInfo.Status - Current entitlements checked via
Transaction.currentEntitlements(for:) - Renewal info accessed for expiration, renewal date, offer status
- Subscription views use ProductView or SubscriptionStoreView
- Win-back offers implemented for expired subscriptions
- Grace period and billing retry states handled
- 通过 跟踪订阅状态
Product.SubscriptionInfo.Status - 通过 检查当前权限
Transaction.currentEntitlements(for:) - 获取续订信息(过期时间、续订日期、优惠状态)
- 订阅视图使用 ProductView 或 SubscriptionStoreView
- 为过期订阅实现赢回优惠
- 处理宽限期和账单重试状态
Phase 5: Restore & Sync
阶段 5:恢复与同步
- Restore purchases implemented (required by App Store Review)
- Restore uses or
Transaction.currentEntitlementsTransaction.all - Family Sharing transactions identified (if supported)
- Server sync implemented (if using backend)
- Cross-device entitlement sync tested
- 实现购买恢复功能(App Store 审核要求)
- 恢复使用 或
Transaction.currentEntitlementsTransaction.all - 识别家庭共享交易(若支持)
- 实现服务端同步(若使用后端)
- 测试跨设备权限同步
Phase 6: Error Handling
阶段 6:错误处理
- Network errors handled gracefully (retries, user messaging)
- Invalid product IDs detected and logged
- Purchase failures show user-friendly error messages
- Transaction verification failures logged and reported
- Refund notifications handled (via App Store Server Notifications)
- 优雅处理网络错误(重试、用户提示)
- 检测并记录无效商品 ID
- 购买失败时显示用户友好的错误信息
- 记录并上报交易验证失败
- 处理退款通知(通过 App Store 服务端通知)
Phase 7: Testing & Validation
阶段 7:测试与验证
- Unit tests verify purchase logic with mocked Product/Transaction
- Unit tests verify subscription status determination
- Integration tests with StoreKit configuration pass
- Sandbox testing with real Apple ID completed
- TestFlight testing completed before production release
- 单元测试通过模拟 Product/Transaction 验证购买逻辑
- 单元测试验证订阅状态判断
- 基于 StoreKit 配置的集成测试通过
- 使用真实 Apple ID 完成沙盒测试
- 生产版本发布前完成 TestFlight 测试
Step 1: Create StoreKit Configuration (FIRST!)
步骤 1:创建 StoreKit 配置(优先完成!)
DO THIS BEFORE WRITING ANY PURCHASE CODE.
在编写任何购买代码前完成此步骤。
Create Configuration File
创建配置文件
- Xcode → File → New → File → StoreKit Configuration File
- Save as: (or your app name)
Products.storekit - Add to target: ✅ (include in app bundle for testing)
- Xcode → 文件 → 新建 → 文件 → StoreKit 配置文件
- 保存为:(或您的应用名称)
Products.storekit - 添加到目标:✅(包含在应用包中用于测试)
Add Products
添加商品
Click "+" and add each product type:
点击 "+" 添加每种商品类型:
Consumable
消耗品
Product ID: com.yourapp.coins_100
Reference Name: 100 Coins
Price: $0.99商品 ID: com.yourapp.coins_100
参考名称: 100 金币
价格: $0.99Non-Consumable
非消耗品
Product ID: com.yourapp.premium
Reference Name: Premium Upgrade
Price: $4.99商品 ID: com.yourapp.premium
参考名称: 高级版升级
价格: $4.99Auto-Renewable Subscription
自动续订订阅
Product ID: com.yourapp.pro_monthly
Reference Name: Pro Monthly
Price: $9.99/month
Subscription Group ID: pro_tier商品 ID: com.yourapp.pro_monthly
参考名称: 专业版月度订阅
价格: $9.99/月
订阅组 ID: pro_tierTest Immediately
立即测试
- Run app in simulator
- Scheme → Edit Scheme → Run → Options
- StoreKit Configuration: Select
Products.storekit - Verify: Products load, purchases complete, transactions appear
- 在模拟器中运行应用
- 方案 → 编辑方案 → 运行 → 选项
- StoreKit 配置:选择
Products.storekit - 验证:商品加载成功、购买完成、交易记录显示正常
Step 2: Implement StoreManager Architecture
步骤 2:实现 StoreManager 架构
Required Pattern: Centralized StoreManager
必选模式:集中式 StoreManager
All purchase logic must go through a single StoreManager. No scattered calls throughout app.
Product.purchase()swift
import StoreKit
@MainActor
final class StoreManager: ObservableObject {
// Published state for UI
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedProductIDs: Set<String> = []
// Product IDs from StoreKit configuration
private let productIDs = [
"com.yourapp.coins_100",
"com.yourapp.premium",
"com.yourapp.pro_monthly"
]
private var transactionListener: Task<Void, Never>?
init() {
// Start transaction listener immediately
transactionListener = listenForTransactions()
Task {
await loadProducts()
await updatePurchasedProducts()
}
}
deinit {
transactionListener?.cancel()
}
}Why @MainActor: Published properties must update on main thread for UI binding.
所有购买逻辑必须通过单一 StoreManager 处理。 禁止在应用中分散调用 。
Product.purchase()swift
import StoreKit
@MainActor
final class StoreManager: ObservableObject {
// 用于 UI 绑定的发布状态
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedProductIDs: Set<String> = []
// StoreKit 配置中的商品 ID
private let productIDs = [
"com.yourapp.coins_100",
"com.yourapp.premium",
"com.yourapp.pro_monthly"
]
private var transactionListener: Task<Void, Never>?
init() {
// 立即启动交易监听器
transactionListener = listenForTransactions()
Task {
await loadProducts()
await updatePurchasedProducts()
}
}
deinit {
transactionListener?.cancel()
}
}为何使用 @MainActor:发布的属性必须在主线程更新以支持 UI 绑定。
Load Products (At Launch)
加载商品(应用启动时)
swift
extension StoreManager {
func loadProducts() async {
do {
// Load products from App Store
let loadedProducts = try await Product.products(for: productIDs)
// Update published property on main thread
self.products = loadedProducts
} catch {
print("Failed to load products: \(error)")
// Show error to user
}
}
}Call from: or first view's modifier
App.init().taskswift
extension StoreManager {
func loadProducts() async {
do {
// 从 App Store 加载商品
let loadedProducts = try await Product.products(for: productIDs)
// 在主线程更新发布属性
self.products = loadedProducts
} catch {
print("加载商品失败: \(error)")
// 向用户显示错误信息
}
}
}调用时机: 或首个视图的 修饰器
App.init().taskListen for Transactions (REQUIRED)
监听交易(必须实现)
swift
extension StoreManager {
func listenForTransactions() -> Task<Void, Never> {
Task.detached { [weak self] in
// Listen for ALL transaction updates
for await verificationResult in Transaction.updates {
await self?.handleTransaction(verificationResult)
}
}
}
@MainActor
private func handleTransaction(_ result: VerificationResult<Transaction>) async {
// Verify transaction signature
guard let transaction = try? result.payloadValue else {
print("Transaction verification failed")
return
}
// Grant entitlement to user
await grantEntitlement(for: transaction)
// CRITICAL: Always finish transaction
await transaction.finish()
// Update purchased products
await updatePurchasedProducts()
}
}Why detached: Transaction listener runs independently of view lifecycle
swift
extension StoreManager {
func listenForTransactions() -> Task<Void, Never> {
Task.detached { [weak self] in
// 监听所有交易更新
for await verificationResult in Transaction.updates {
await self?.handleTransaction(verificationResult)
}
}
}
@MainActor
private func handleTransaction(_ result: VerificationResult<Transaction>) async {
// 验证交易签名
guard let transaction = try? result.payloadValue else {
print("交易验证失败")
return
}
// 为用户授予权限
await grantEntitlement(for: transaction)
// 关键:务必完成交易
await transaction.finish()
// 更新已购买商品列表
await updatePurchasedProducts()
}
}为何使用 detached:交易监听器独立于视图生命周期运行
Step 3: Implement Purchase Flow
步骤 3:实现购买流程
Purchase with UI Context (iOS 18.2+)
带 UI 上下文的购买(iOS 18.2+)
swift
extension StoreManager {
func purchase(_ product: Product, confirmIn scene: UIWindowScene) async throws -> Bool {
// Perform purchase with UI context for payment sheet
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verificationResult):
// Verify the transaction
guard let transaction = try? verificationResult.payloadValue else {
print("Transaction verification failed")
return false
}
// Grant entitlement
await grantEntitlement(for: transaction)
// CRITICAL: Finish transaction
await transaction.finish()
// Update state
await updatePurchasedProducts()
return true
case .userCancelled:
// User tapped "Cancel" in payment sheet
return false
case .pending:
// Purchase requires action (Ask to Buy, payment issue)
// Will be delivered via Transaction.updates when approved
return false
@unknown default:
return false
}
}
}swift
extension StoreManager {
func purchase(_ product: Product, confirmIn scene: UIWindowScene) async throws -> Bool {
// 使用 UI 上下文执行购买以显示支付表单
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verificationResult):
// 验证交易
guard let transaction = try? verificationResult.payloadValue else {
print("交易验证失败")
return false
}
// 授予权限
await grantEntitlement(for: transaction)
// 关键:完成交易
await transaction.finish()
// 更新状态
await updatePurchasedProducts()
return true
case .userCancelled:
// 用户在支付表单中点击“取消”
return false
case .pending:
// 购买需要额外操作(请求购买、支付问题)
// 批准后将通过 Transaction.updates 传递
return false
@unknown default:
return false
}
}
}SwiftUI Purchase (Using Environment)
SwiftUI 购买(使用环境变量)
swift
struct ProductRow: 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)")
}
}
}
}
}swift
struct ProductRow: View {
let product: Product
@Environment(\.purchase) private var purchase
var body: some View {
Button("购买 \(product.displayPrice)") {
Task {
do {
let result = try await purchase(product)
// 处理结果
} catch {
print("购买失败: \(error)")
}
}
}
}
}Set appAccountToken (If Using Backend)
设置 appAccountToken(若使用后端)
swift
func purchase(
_ product: Product,
confirmIn scene: UIWindowScene,
accountToken: UUID
) async throws -> Bool {
// Purchase with appAccountToken for server-side association
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)
// ... handle result
}When to use: When your backend needs to associate purchases with user accounts
swift
func purchase(
_ product: Product,
confirmIn scene: UIWindowScene,
accountToken: UUID
) async throws -> Bool {
// 使用 appAccountToken 进行购买,用于服务端关联
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)
// ... 处理结果
}使用场景:当您的后端需要将购买记录与用户账户关联时
Step 4: Verify Transactions (MANDATORY)
步骤 4:验证交易(必须实现)
Always Use VerificationResult
始终使用 VerificationResult
swift
func handleTransaction(_ result: VerificationResult<Transaction>) async {
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 transaction: \(error)")
// DO NOT grant entitlement
// DO finish transaction to clear from queue
await transaction.finish()
}
}Why verify: Prevents granting entitlements for:
- Fraudulent receipts
- Jailbroken device receipts
- Man-in-the-middle attacks
swift
func handleTransaction(_ result: VerificationResult<Transaction>) async {
switch result {
case .verified(let transaction):
// ✅ 交易由 App Store 签名
await grantEntitlement(for: transaction)
await transaction.finish()
case .unverified(let transaction, let error):
// ❌ 交易签名无效
print("未验证的交易: \(error)")
// 请勿授予权限
// 务必完成交易以从队列中清除
await transaction.finish()
}
}为何需要验证:防止为以下情况授予权限:
- 欺诈性收据
- 越狱设备的收据
- 中间人攻击
Check Transaction Fields
检查交易字段
swift
func grantEntitlement(for transaction: Transaction) async {
// Check transaction hasn't been revoked
guard transaction.revocationDate == nil else {
print("Transaction was refunded")
await revokeEntitlement(for: transaction.productID)
return
}
// Grant based on product type
switch transaction.productType {
case .consumable:
await addConsumable(productID: transaction.productID)
case .nonConsumable:
await unlockFeature(productID: transaction.productID)
case .autoRenewable:
await activateSubscription(productID: transaction.productID)
default:
break
}
}swift
func grantEntitlement(for transaction: Transaction) async {
// 检查交易是否已被撤销
guard transaction.revocationDate == nil else {
print("交易已退款")
await revokeEntitlement(for: transaction.productID)
return
}
// 根据商品类型授予权限
switch transaction.productType {
case .consumable:
await addConsumable(productID: transaction.productID)
case .nonConsumable:
await unlockFeature(productID: transaction.productID)
case .autoRenewable:
await activateSubscription(productID: transaction.productID)
default:
break
}
}Step 5: Track Current Entitlements
步骤 5:跟踪当前权限
Check What User Owns
检查用户已购买的商品
swift
extension StoreManager {
func updatePurchasedProducts() async {
var purchased: Set<String> = []
// Iterate through all current entitlements
for await result in Transaction.currentEntitlements {
guard let transaction = try? result.payloadValue else {
continue
}
// Only include active entitlements (not revoked)
if transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}
}
self.purchasedProductIDs = purchased
}
}swift
extension StoreManager {
func updatePurchasedProducts() async {
var purchased: Set<String> = []
// 遍历所有当前权限
for await result in Transaction.currentEntitlements {
guard let transaction = try? result.payloadValue else {
continue
}
// 仅包含未撤销的有效权限
if transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}
}
self.purchasedProductIDs = purchased
}
}Check Specific Product
检查特定商品
swift
func isEntitled(to productID: String) async -> Bool {
// Check current entitlements for specific product
for await result in Transaction.currentEntitlements(for: productID) {
if let transaction = try? result.payloadValue,
transaction.revocationDate == nil {
return true
}
}
return false
}swift
func isEntitled(to productID: String) async -> Bool {
// 检查特定商品的当前权限
for await result in Transaction.currentEntitlements(for: productID) {
if let transaction = try? result.payloadValue,
transaction.revocationDate == nil {
return true
}
}
return false
}Step 6: Implement Subscription Management
步骤 6:实现订阅管理
Track Subscription Status
跟踪订阅状态
swift
extension StoreManager {
func checkSubscriptionStatus(for groupID: String) async -> Product.SubscriptionInfo.Status? {
// Get subscription statuses for group
guard let result = try? await Product.SubscriptionInfo.status(for: groupID),
let status = result.first else {
return nil
}
return status.state
}
}swift
extension StoreManager {
func checkSubscriptionStatus(for groupID: String) async -> Product.SubscriptionInfo.Status? {
// 获取订阅组的状态
guard let result = try? await Product.SubscriptionInfo.status(for: groupID),
let status = result.first else {
return nil
}
return status.state
}
}Handle Subscription States
处理订阅状态
swift
func updateSubscriptionUI(for status: Product.SubscriptionInfo.Status) {
switch status.state {
case .subscribed:
// User has active subscription
showSubscribedContent()
case .expired:
// Subscription expired - show win-back offer
showResubscribeOffer()
case .inGracePeriod:
// Billing issue - show payment update prompt
showUpdatePaymentPrompt()
case .inBillingRetryPeriod:
// Apple retrying payment - maintain access
showBillingRetryMessage()
case .revoked:
// Family Sharing access removed
removeAccess()
@unknown default:
break
}
}swift
func updateSubscriptionUI(for status: Product.SubscriptionInfo.Status) {
switch status.state {
case .subscribed:
// 用户拥有有效订阅
showSubscribedContent()
case .expired:
// 订阅已过期 - 显示赢回优惠
showResubscribeOffer()
case .inGracePeriod:
// 账单问题 - 显示支付更新提示
showUpdatePaymentPrompt()
case .inBillingRetryPeriod:
// Apple 正在重试支付 - 保持权限可用
showBillingRetryMessage()
case .revoked:
// 家庭共享权限被移除
removeAccess()
@unknown default:
break
}
}Use StoreKit Views (iOS 17+)
使用 StoreKit 视图(iOS 17+)
swift
struct SubscriptionView: View {
var body: some View {
SubscriptionStoreView(groupID: "pro_tier") {
// Marketing content
VStack {
Image("premium-icon")
Text("Unlock all features")
}
}
.subscriptionStoreControlStyle(.prominentPicker)
}
}swift
struct SubscriptionView: View {
var body: some View {
SubscriptionStoreView(groupID: "pro_tier") {
// 营销内容
VStack {
Image("premium-icon")
Text("解锁所有功能")
}
}
.subscriptionStoreControlStyle(.prominentPicker)
}
}Step 7: Implement Restore Purchases (REQUIRED)
步骤 7:实现购买恢复(必须实现)
Restore Flow
恢复流程
swift
extension StoreManager {
func restorePurchases() async {
// Sync all transactions from App Store
try? await AppStore.sync()
// Update current entitlements
await updatePurchasedProducts()
}
}swift
extension StoreManager {
func restorePurchases() async {
// 从 App Store 同步所有交易
try? await AppStore.sync()
// 更新当前权限
await updatePurchasedProducts()
}
}UI Button
UI 按钮
swift
struct SettingsView: View {
@StateObject private var store = StoreManager()
var body: some View {
Button("Restore Purchases") {
Task {
await store.restorePurchases()
}
}
}
}App Store Requirement: Apps with IAP must provide restore functionality for non-consumables and subscriptions.
swift
struct SettingsView: View {
@StateObject private var store = StoreManager()
var body: some View {
Button("恢复购买") {
Task {
await store.restorePurchases()
}
}
}
}App Store 要求:包含应用内购买的应用必须提供恢复功能。
Step 8: Handle Refunds
步骤 8:处理退款
Listen for Refund Notifications
监听退款通知
swift
extension StoreManager {
func listenForTransactions() -> Task<Void, Never> {
Task.detached { [weak self] in
for await verificationResult in Transaction.updates {
await self?.handleTransaction(verificationResult)
}
}
}
@MainActor
private func handleTransaction(_ result: VerificationResult<Transaction>) async {
guard let transaction = try? result.payloadValue else {
return
}
// Check if transaction was refunded
if let revocationDate = transaction.revocationDate {
print("Transaction refunded on \(revocationDate)")
await revokeEntitlement(for: transaction.productID)
} else {
await grantEntitlement(for: transaction)
}
await transaction.finish()
}
}swift
extension StoreManager {
func listenForTransactions() -> Task<Void, Never> {
Task.detached { [weak self] in
for await verificationResult in Transaction.updates {
await self?.handleTransaction(verificationResult)
}
}
}
@MainActor
private func handleTransaction(_ result: VerificationResult<Transaction>) async {
guard let transaction = try? result.payloadValue else {
return
}
// 检查交易是否已退款
if let revocationDate = transaction.revocationDate {
print("交易已于 \(revocationDate) 退款")
await revokeEntitlement(for: transaction.productID)
} else {
await grantEntitlement(for: transaction)
}
await transaction.finish()
}
}Step 9: Unit Testing
步骤 9:单元测试
Mock Store Responses
模拟商店响应
swift
protocol StoreProtocol {
func products(for ids: [String]) async throws -> [Product]
func purchase(_ product: Product) async throws -> PurchaseResult
}
// Production
final class StoreManager: StoreProtocol {
func products(for ids: [String]) async throws -> [Product] {
try await Product.products(for: ids)
}
}
// Testing
final class MockStore: StoreProtocol {
var mockProducts: [Product] = []
var mockPurchaseResult: PurchaseResult?
func products(for ids: [String]) async throws -> [Product] {
mockProducts
}
func purchase(_ product: Product) async throws -> PurchaseResult {
mockPurchaseResult ?? .userCancelled
}
}swift
protocol StoreProtocol {
func products(for ids: [String]) async throws -> [Product]
func purchase(_ product: Product) async throws -> PurchaseResult
}
// 生产环境实现
final class StoreManager: StoreProtocol {
func products(for ids: [String]) async throws -> [Product] {
try await Product.products(for: ids)
}
}
// 测试用模拟实现
final class MockStore: StoreProtocol {
var mockProducts: [Product] = []
var mockPurchaseResult: PurchaseResult?
func products(for ids: [String]) async throws -> [Product] {
mockProducts
}
func purchase(_ product: Product) async throws -> PurchaseResult {
mockPurchaseResult ?? .userCancelled
}
}Test Purchase Logic
测试购买逻辑
swift
@Test func testSuccessfulPurchase() async {
let mockStore = MockStore()
let manager = StoreManager(store: mockStore)
// Given: Mock successful purchase
mockStore.mockPurchaseResult = .success(.verified(mockTransaction))
// When: Purchase product
let result = await manager.purchase(mockProduct)
// Then: Entitlement granted
#expect(result == true)
#expect(manager.purchasedProductIDs.contains("com.app.premium"))
}
@Test func testCancelledPurchase() async {
let mockStore = MockStore()
let manager = StoreManager(store: mockStore)
// Given: User cancels
mockStore.mockPurchaseResult = .userCancelled
// When: Purchase product
let result = await manager.purchase(mockProduct)
// Then: No entitlement granted
#expect(result == false)
#expect(manager.purchasedProductIDs.isEmpty)
}swift
@Test func testSuccessfulPurchase() async {
let mockStore = MockStore()
let manager = StoreManager(store: mockStore)
// 给定:模拟成功购买
mockStore.mockPurchaseResult = .success(.verified(mockTransaction))
// 当:购买商品
let result = await manager.purchase(mockProduct)
// 则:权限已授予
#expect(result == true)
#expect(manager.purchasedProductIDs.contains("com.app.premium"))
}
@Test func testCancelledPurchase() async {
let mockStore = MockStore()
let manager = StoreManager(store: mockStore)
// 给定:用户取消购买
mockStore.mockPurchaseResult = .userCancelled
// 当:购买商品
let result = await manager.purchase(mockProduct)
// 则:未授予权限
#expect(result == false)
#expect(manager.purchasedProductIDs.isEmpty)
}Common Anti-Patterns (NEVER DO THIS)
常见反模式(绝对不要这样做)
❌ No StoreKit Configuration
❌ 未配置 StoreKit
swift
// ❌ WRONG: Writing purchase code without .storekit file
let products = try await Product.products(for: productIDs)
// Can't test this without App Store Connect setup!✅ Correct: Create file FIRST, test in Xcode, THEN implement.
.storekitswift
// ❌ 错误:未创建 .storekit 文件就编写购买代码
let products = try await Product.products(for: productIDs)
// 没有 App Store Connect 配置就无法测试!✅ 正确做法:先创建 文件,在 Xcode 中测试,再实现代码。
.storekit❌ Code Before .storekit Config
❌ 先写代码再配置 .storekit
swift
// ❌ Less ideal: Write code, test in sandbox, add .storekit later
let products = try await Product.products(for: productIDs)
let result = try await product.purchase(confirmIn: scene)
// "I tested this in sandbox, it works! I'll add .storekit config later."✅ Recommended: Create config first, then write code.
.storekitIf you're in this situation: See "Already Wrote Code Before Creating .storekit Config?" section above for your options (A, B, or C).
Why .storekit-first is better:
- Product ID typos caught in Xcode, not at runtime
- Faster iteration without network requests
- Teammates can test locally
- Documents product structure in code
Sandbox testing is valuable - it validates against real App Store infrastructure. But starting with .storekit makes sandbox testing easier because you've already validated product IDs locally.
swift
// ❌ 次优选择:先写代码,在沙盒测试,之后添加 .storekit
let products = try await Product.products(for: productIDs)
let result = try await product.purchase(confirmIn: scene)
// "我在沙盒测试过了,可以运行!之后再添加 .storekit 配置。"✅ 推荐做法:先创建 配置,再编写代码。
.storekit若您已处于此情况:参考上文「已编写购买代码但未创建 .storekit 配置?」部分的选项(A、B 或 C)。
为何 .storekit 优先更优:
- 在 Xcode 中即可发现商品 ID 拼写错误,无需等到运行时
- 无需网络请求即可快速迭代
- 团队成员可在本地测试
- 在代码中记录商品结构
沙盒测试很有价值——它能验证与真实 App Store 基础设施的兼容性。但从 .storekit 开始能让沙盒测试更轻松,因为您已在本地验证了商品 ID。
❌ Scattered Purchase Calls
❌ 分散的购买调用
swift
// ❌ WRONG: Purchase calls scattered throughout app
Button("Buy") {
try await product.purchase() // In view 1
}
Button("Subscribe") {
try await subscriptionProduct.purchase() // In view 2
}✅ Correct: All purchases through centralized StoreManager.
swift
// ❌ 错误:购买调用分散在应用各处
Button("购买") {
try await product.purchase() // 在视图 1 中
}
Button("订阅") {
try await subscriptionProduct.purchase() // 在视图 2 中
}✅ 正确做法:所有购买请求通过集中式 StoreManager 处理。
❌ Forgetting to Finish Transactions
❌ 忘记完成交易
swift
// ❌ WRONG: Never calling finish()
func handleTransaction(_ transaction: Transaction) {
grantEntitlement(for: transaction)
// Missing: await transaction.finish()
}✅ Correct: ALWAYS call after granting entitlement.
transaction.finish()swift
// ❌ 错误:从未调用 finish()
func handleTransaction(_ transaction: Transaction) {
grantEntitlement(for: transaction)
// 缺失:await transaction.finish()
}✅ 正确做法:授予权限后务必调用 。
transaction.finish()❌ Not Verifying Transactions
❌ 未验证交易
swift
// ❌ WRONG: Using unverified transaction
for await transaction in Transaction.all {
grantEntitlement(for: transaction) // Unsafe!
}✅ Correct: Always check before granting.
VerificationResultswift
// ❌ 错误:使用未验证的交易
for await transaction in Transaction.all {
grantEntitlement(for: transaction) // 不安全!
}✅ 正确做法:授予权限前始终检查 。
VerificationResult❌ Ignoring Transaction Listener
❌ 忽略交易监听器
swift
// ❌ WRONG: Only handling purchases in purchase() method
func purchase() {
let result = try await product.purchase()
// What about pending purchases, family sharing, restore?
}✅ Correct: Listen to for ALL transaction sources.
Transaction.updatesswift
// ❌ 错误:仅在 purchase() 方法中处理购买
func purchase() {
let result = try await product.purchase()
// 待处理购买、家庭共享、恢复购买这些情况怎么办?
}✅ 正确做法:监听 以处理所有交易来源。
Transaction.updates❌ Not Implementing Restore
❌ 未实现购买恢复
swift
// ❌ WRONG: No restore button
// App Store will REJECT your app!✅ Correct: Provide visible "Restore Purchases" button in settings.
swift
// ❌ 错误:没有恢复按钮
// App Store 会拒绝您的应用!✅ 正确做法:在设置页面提供可见的「恢复购买」按钮。
Validation
验证
Before marking IAP implementation complete, verify:
在标记应用内购买实现完成前,验证以下内容:
Code Inspection
代码检查
Run these searches to verify compliance:
bash
undefined运行以下搜索以验证合规性:
bash
undefinedCheck StoreKit configuration exists
检查 StoreKit 配置是否存在
find . -name "*.storekit"
find . -name "*.storekit"
Check transaction.finish() is called
检查是否调用了 transaction.finish()
rg "transaction.finish()" --type swift
rg "transaction.finish()" --type swift
Check VerificationResult usage
检查 VerificationResult 的使用
rg "VerificationResult" --type swift
rg "VerificationResult" --type swift
Check Transaction.updates listener
检查 Transaction.updates 监听器
rg "Transaction.updates" --type swift
rg "Transaction.updates" --type swift
Check restore implementation
检查恢复功能的实现
rg "AppStore.sync|Transaction.all" --type swift
undefinedrg "AppStore.sync|Transaction.all" --type swift
undefinedFunctional Testing
功能测试
- Can purchase each product type in StoreKit configuration
- Can cancel purchase and state remains consistent
- Can restore purchases and regain access
- Subscription renewal/expiration works as expected
- Refunded transactions revoke access
- Family Sharing transactions identified (if supported)
- 可在 StoreKit 配置中购买每种商品类型
- 可取消购买且状态保持一致
- 可恢复购买并重新获得权限
- 订阅续订/过期按预期工作
- 已退款的交易会收回权限
- 可识别家庭共享交易(若支持)
Sandbox Testing
沙盒测试
- Real Apple ID sandbox purchases complete
- TestFlight beta testers confirm purchase flows work
- Server-side validation works (if using backend)
- 使用真实 Apple ID 完成沙盒购买
- TestFlight 测试人员确认购买流程正常
- 服务端验证正常工作(若使用后端)
Resources
资源
WWDC: 2025-241, 2025-249, 2023-10013, 2021-10114
Docs: /storekit, /appstoreserverapi
Skills: axiom-storekit-ref
WWDC:2025-241, 2025-249, 2023-10013, 2021-10114
文档:/storekit, /appstoreserverapi
技能:axiom-storekit-ref