axiom-in-app-purchases

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

StoreKit 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
.storekit
configuration, you have three options:
如果您在创建
.storekit
配置前就编写了购买代码,有以下三种选择:

Option 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
.storekit
file now with your existing product IDs. Test everything works locally. Document in your PR that you tested in sandbox first.
Trade-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 创建
.storekit
文件。在本地测试所有功能正常运行。在您的 PR 中记录已先在沙盒环境测试。
利弊权衡
  • ✅ 保留可运行的代码
  • ✅ 增加本地测试能力
  • ❌ 失去商品 ID 验证的优势
  • ❌ 强化测试后置的开发模式
  • ❌ 代码审查时需要额外警惕
若选择此路径:立即创建 .storekit 文件,在本地验证,并提交备注说明该实现方式。

Option C: Skip .storekit Entirely (Not Recommended)

选项 C:完全跳过 .storekit 配置(不推荐)

Commit without
.storekit
configuration, test only in sandbox.
Why 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
.storekit
configuration before writing any purchase code. This isn't arbitrary - it provides concrete benefits:
Immediate 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 test
Why this order helps:
  1. StoreKit Config First: Defines products without App Store Connect dependency
  2. Local Testing: Validates product IDs and purchase flows immediately
  3. Production Code: Implements against validated product configuration
  4. Unit Tests: Verifies business logic with mocked store responses
  5. 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    模拟商店    集成测试
为何此顺序更合理
  1. 先做 StoreKit 配置:无需依赖 App Store Connect 即可定义商品
  2. 本地测试:立即验证商品 ID 和购买流程
  3. 编写生产代码:基于已验证的商品配置实现功能
  4. 单元测试:通过模拟商店响应验证业务逻辑
  5. 沙盒测试:在 App Store 环境中完成最终验证
遵循此工作流的好处
  • 编写代码前先验证商品 ID
  • 更快的开发迭代
  • 更轻松的团队协作
  • 更好的测试覆盖率

Mandatory Checklist

必检清单

Before marking IAP implementation complete, ALL items must be verified:
在标记应用内购买实现完成前,必须验证以下所有项:

Phase 1: Testing Foundation

阶段 1:测试基础

  • Created
    .storekit
    configuration file with all products
  • 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
    .finish()
    after entitlement granted
  • 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
    purchase(confirmIn:options:)
    with UI context (iOS 18.2+)
  • Purchase handles all
    PurchaseResult
    cases (success, userCancelled, pending)
  • Purchase verifies transaction signature before granting entitlement
  • Purchase stores transaction receipt/identifier for support
  • appAccountToken set for all purchases (if using server backend)
  • 购买使用新增的
    purchase(confirmIn:options:)
    并传入 UI 上下文(iOS 18.2+)
  • 购买处理所有
    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
    Transaction.currentEntitlements
    or
    Transaction.all
  • Family Sharing transactions identified (if supported)
  • Server sync implemented (if using backend)
  • Cross-device entitlement sync tested
  • 实现购买恢复功能(App Store 审核要求)
  • 恢复使用
    Transaction.currentEntitlements
    Transaction.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

创建配置文件

  1. Xcode → File → New → File → StoreKit Configuration File
  2. Save as:
    Products.storekit
    (or your app name)
  3. Add to target: ✅ (include in app bundle for testing)
  1. Xcode → 文件 → 新建 → 文件 → StoreKit 配置文件
  2. 保存为
    Products.storekit
    (或您的应用名称)
  3. 添加到目标:✅(包含在应用包中用于测试)

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.99

Non-Consumable

非消耗品

Product ID: com.yourapp.premium
Reference Name: Premium Upgrade
Price: $4.99
商品 ID: com.yourapp.premium
参考名称: 高级版升级
价格: $4.99

Auto-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_tier

Test Immediately

立即测试

  1. Run app in simulator
  2. Scheme → Edit Scheme → Run → Options
  3. StoreKit Configuration: Select
    Products.storekit
  4. Verify: Products load, purchases complete, transactions appear

  1. 在模拟器中运行应用
  2. 方案 → 编辑方案 → 运行 → 选项
  3. StoreKit 配置:选择
    Products.storekit
  4. 验证:商品加载成功、购买完成、交易记录显示正常

Step 2: Implement StoreManager Architecture

步骤 2:实现 StoreManager 架构

Required Pattern: Centralized StoreManager

必选模式:集中式 StoreManager

All purchase logic must go through a single StoreManager. No scattered
Product.purchase()
calls throughout app.
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:
App.init()
or first view's
.task
modifier
swift
extension StoreManager {
    func loadProducts() async {
        do {
            // 从 App Store 加载商品
            let loadedProducts = try await Product.products(for: productIDs)

            // 在主线程更新发布属性
            self.products = loadedProducts

        } catch {
            print("加载商品失败: \(error)")
            // 向用户显示错误信息
        }
    }
}
调用时机
App.init()
或首个视图的
.task
修饰器

Listen 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
.storekit
file FIRST, test in Xcode, THEN implement.
swift
// ❌ 错误:未创建 .storekit 文件就编写购买代码
let products = try await Product.products(for: productIDs)
// 没有 App Store Connect 配置就无法测试!
正确做法:先创建
.storekit
文件,在 Xcode 中测试,再实现代码。

❌ 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
.storekit
config first, then write code.
If 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
transaction.finish()
after granting entitlement.
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
VerificationResult
before granting.
swift
// ❌ 错误:使用未验证的交易
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
Transaction.updates
for ALL transaction sources.
swift
// ❌ 错误:仅在 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
undefined

Check 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
undefined
rg "AppStore.sync|Transaction.all" --type swift
undefined

Functional 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