目录
- [StoreKit 2 核心概念与优势](#StoreKit 2 核心概念与优势 "#1-storekit-2-%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E4%B8%8E%E4%BC%98%E5%8A%BF")
- 基础准备:产品类型与配置
- [核心实战 I:获取商品与购买](#核心实战 I:获取商品与购买 "#3-%E6%A0%B8%E5%BF%83%E5%AE%9E%E6%88%98-i%E8%8E%B7%E5%8F%96%E5%95%86%E5%93%81%E4%B8%8E%E8%B4%AD%E4%B9%B0")
- [核心实战 II:交易验证与监听](#核心实战 II:交易验证与监听 "#4-%E6%A0%B8%E5%BF%83%E5%AE%9E%E6%88%98-ii%E4%BA%A4%E6%98%93%E9%AA%8C%E8%AF%81%E4%B8%8E%E7%9B%91%E5%90%AC")
- 订阅管理:状态、续期与退款
- [深度讲解:恢复购买 (Restore Purchases)](#深度讲解:恢复购买 (Restore Purchases) "#6-%E6%B7%B1%E5%BA%A6%E8%AE%B2%E8%A7%A3%E6%81%A2%E5%A4%8D%E8%B4%AD%E4%B9%B0-restore-purchases")
- [营销功能:折扣与优惠 (Offers)](#营销功能:折扣与优惠 (Offers) "#7-%E8%90%A5%E9%94%80%E5%8A%9F%E8%83%BD%E6%8A%98%E6%89%A3%E4%B8%8E%E4%BC%98%E6%83%A0-offers")
- [测试指南:沙盒 (Sandbox) 与 TestFlight](#测试指南:沙盒 (Sandbox) 与 TestFlight "#8-%E6%B5%8B%E8%AF%95%E6%8C%87%E5%8D%97%E6%B2%99%E7%9B%92-sandbox-%E4%B8%8E-testflight")
- 最佳实践与常见坑点
1. StoreKit 2 核心概念与优势
在 StoreKit 2 之前,我们进行内购开发充满了痛苦:复杂的收据验证、晦涩的 API、漏单等... StoreKit 2 利用 Swift 的现代特性(Concurrency)重构了整个框架。
核心优势
StoreKit 2 是 Apple 在 iOS 15+ / macOS 12.0+ 引入的全新内购框架,相比于旧版 StoreKit 具有以下优势:
- 基于 Swift 并发 :使用
async/await替代回调地狱。 - 自动交易验证:无需手动解析复杂的 Receipt 文件,系统自动处理 JWS(JSON Web Signature)验证。
- 交易历史管理:直接通过 API 获取完整的用户购买历史,无需维护复杂的本地数据库。
- 状态同步:跨设备同步更加顺滑,用户换个手机登录,权益自动同步。
核心概念
| 概念 | 说明 |
|---|---|
| Product | 商品对象,包含价格、名称、描述等信息 |
| Transaction | 交易记录,每次购买产生一个 Transaction |
| PurchaseResult | 购买结果,包含成功、待处理、用户取消等状态 |
| VerificationResult | 验证结果,确保交易来自 Apple 服务器 |
| Product.SubscriptionInfo | 订阅信息,包含订阅组、续期信息等 |
流程图解
Transaction Updates"] C["用户点击购买"] --> D["获取商品
Products"] D --> E["发起购买
Purchase"] E --> F{支付结果} F -- 成功 --> G["验证交易
Verify"] G -- 通过 --> H["发放权益
Unlock Content"] H --> I["结束交易
Finish Transaction"] F -- 失败/取消 --> J["处理错误 UI"] %% 样式定义 classDef start fill:#E3F2FD,stroke:#2196F3,stroke-width:2px,color:#0D47A1; classDef action fill:#FFFFFF,stroke:#90A4AE,stroke-width:2px,color:#37474F; classDef decision fill:#FFF8E1,stroke:#FFC107,stroke-width:2px,color:#FF6F00; classDef endState fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px,color:#1B5E20; classDef error fill:#FFEBEE,stroke:#F44336,stroke-width:2px,color:#B71C1C; %% 样式应用 class A start; class B,C,D,E,H action; class F decision; class I endState; class J error;
2. 基础准备:产品类型与配置
在编写代码前,我们首先需要了解 Apple 定义的四种商品类型:
| 类型 | 英文名 | 特点 | 典型场景 |
|---|---|---|---|
| 消耗型 | Consumable | 可重复购买,购买后即消耗 | 游戏金币、道具 |
| 非消耗型 | Non-Consumable | 一次购买,永久拥有,支持恢复购买 | 解锁完整版、移除广告、终身会员 |
| 自动续期订阅 | Auto-Renewing Subscription | 按周期扣费,自动续订 | 视频会员、SaaS 服务 |
| 非续期订阅 | Non-Renewing Subscription | 有效期固定,不自动续费 | 赛季通行证 |
环境配置
你可能以为必须先去 App Store Connect 创建商品才能写代码,其实无需这么麻烦,Xcode 提供了一个本地配置文件 (.storekit),让你在没有开发者账号、没联网的情况下也能开发。 操作步骤:
- Xcode -> File -> New -> File from Template... -> 搜索 StoreKit Configuration File (或者用快捷键
Command + N). - 不要勾选 "Sync this file with an app in App Store Connect" (除非你已经在 App Store Connect 配置好了商品信息)。
- 建好后,在 Xcode 底部点
+按钮,配置你的商品信息。 - 关键一步:点击 Xcode 顶部菜单
Product -> Scheme -> Edit Scheme -> Run -> Options -> StoreKit Configuration,选择你刚才创建的文件。
💡 老鸟经验:建议使用这个本地配置!它不仅能模拟购买成功,还能模拟扣费失败、退款、订阅过期等真实环境很难复现的场景。
3. 核心实战 I:获取商品与购买
我们将创建一个 StoreKitManager 类来管理所有逻辑。
3.1 获取商品信息
swift
import StoreKit
// 定义你的商品 ID 列表
enum ProductID: String, CaseIterable {
case proMonthly = "com.myapp.pro.monthly" // 订阅
case removeAds = "com.myapp.remove.ads" // 非消耗型
case coins100 = "com.myapp.coins.100" // 消耗型
}
@MainActor
class StoreKitManager: ObservableObject {
@Published var products: [Product] = []
// 获取商品列表
func fetchProducts() async {
do {
// 将 String 转换为 Set<String>
let productIds = Set(ProductID.allCases.map { $0.rawValue })
// 异步请求商品详情
let fetchedProducts = try await Product.products(for: productIds)
// 按价格排序(可选)
self.products = fetchedProducts.sorted(by: { $0.price < $1.price })
} catch {
print("Failed to fetch products: \(error)")
}
}
}
3.2 发起购买流程
StoreKit 2 的购买结果是一个枚举:success, userCancelled, pending。
swift
extension StoreKitManager {
// 购买方法
func purchase(_ product: Product) async throws {
// 1. 发起购买
let result = try await product.purchase()
// 2. 处理结果
switch result {
case .success(let verification):
// 购买成功,需要验证签名
try await handlePurchaseVerification(verification)
case .userCancelled:
// 用户点击了取消
print("User cancelled the purchase")
case .pending:
// 交易挂起(例如家长控制需要审批)
print("Transaction pending")
@unknown default:
break
}
}
// 验证与权益发放
private func handlePurchaseVerification(_ verification: VerificationResult<Transaction>) async throws {
switch verification {
case .unverified(let transaction, let error):
// 签名验证失败,不要发放权益
print("Verification failed: \(error)")
// 建议:结束交易,但不发货
// 如果不 finish,这笔脏数据会每次启动 App 都发过来,卡在队列里
await transaction.finish()
case .verified(let transaction):
// 验证通过
print("Purchase verified: \(transaction.productID)")
// 3. 发放权益(更新本地状态)
await updateUserEntitlements(transaction)
// 4. 重要:通知 App Store 交易已完成
await transaction.finish()
}
}
}
4. 核心实战 II:交易验证与监听
StoreKit 2 有两个关键的数据源:
- Transaction.updates:监听实时的交易流(购买发生时、续订成功时、退款时)。
- Transaction.currentEntitlements:查询用户当前拥有的权益(用于恢复购买)。
4.1 监听交易更新 (Transaction Updates)
最佳实践:必须在 App 启动时立即开始监听,以处理应用在后台或未运行时发生的交易(如订阅自动续期)。
swift
extension StoreKitManager {
// 启动监听任务
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
// 遍历异步序列
for await result in Transaction.updates {
do {
// 收到新交易(续费、购买、恢复)
// 这里复用之前的验证逻辑
try await self.handlePurchaseVerification(result)
} catch {
print("Transaction update handling failed")
}
}
}
}
}
确保它随 App 启动而运行:
swift
// 在 App 入口处调用
@main
struct MyApp: App {
let storeKitManager = StoreKitManager()
var body: some Scene {
WindowGroup {
ContentView()
.task {
// 开启监听
await storeKitManager.listenForTransactions()
}
}
}
}
4.2 检查当前权益 (Entitlements)
如何判断用户是不是会员呢? StoreKit 2,你不需要自己存本地数据库,直接调用 Transaction.currentEntitlements 来查询,它只返回当前有效的权益(过期的、退款的会自动过滤掉)。
swift
extension StoreKitManager {
// 更新用户权益状态
func updateCustomerProductStatus() async {
var purchasedIds: [String] = []
// 遍历当前有效的权益(已自动过滤掉过期订阅、被撤销的交易)
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
// 检查是否被撤销(退款)
if transaction.revocationDate == nil {
purchasedIds.append(transaction.productID)
}
}
}
// 更新 UI 状态
// self.isPro = purchasedIds.contains(ProductID.proMonthly.rawValue)
print("User has active entitlements: \(purchasedIds)")
}
}
5. 订阅管理:状态、续期与退款
订阅比一次性购买复杂,因为需要处理过期、宽限期等状态。
5.1 获取订阅详细
swift
extension StoreKitManager {
func checkSubscriptionStatus() async {
// 假设我们只关心 proMonthly 这个组的订阅状态
guard let product = products.first(where: { $0.id == ProductID.proMonthly.rawValue }) else { return }
guard let subscriptionInfo = product.subscription else { return }
do {
// 获取该订阅组的状态
let statuses = try await subscriptionInfo.status
for status in statuses {
switch status.state {
case .subscribed:
print("用户处于订阅期")
case .expired:
print("订阅已过期")
case .inGracePeriod:
print("处于宽限期(扣费失败但Apple暂未关停),应视为已订阅")
case .revoked:
print("订阅被撤销(退款)")
case .inBillingRetryPeriod:
print("扣费重试中,通常应暂停服务")
default:
break
}
// 获取续订信息
if let renewalInfo = try? verify(status.renewalInfo) {
print("有自动续订: \(renewalInfo.willAutoRenew)")
}
}
} catch {
print("Error checking subscription status: \(error)")
}
}
// 辅助泛型方法:解包 VerificationResult
func verify<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw error
case .verified(let safe):
// ✅ 验证通过,返回解包后的数据
return safe
}
}
}
5.2 识别退款 (Refunds)
当 Transaction.updates 收到更新,或遍历 currentEntitlements 时:
- 检查
transaction.revocationDate是否不为 nil。 - 检查
transaction.revocationReason。
swift
if let date = transaction.revocationDate {
print("该交易已于 \(date) 被撤销/退款")
// 移除对应的权益
removeEntitlement(for: transaction.productID)
}
6. 深度讲解:恢复购买 (Restore Purchases)
苹果审核要求必须有"恢复购买"按钮。
概念与误区
- SK1 vs SK2 : 在旧版 SK1 中,必须调用
restoreCompletedTransactions触发系统弹窗输入密码。 - SK2 的机制 :
Transaction.currentEntitlements已经包含了用户所有的有效权益。通常情况下,应用启动时刷新这个属性,就等同于"静默恢复"。 - AppStore.sync(): 这是 StoreKit 2 的"显式恢复"接口。只有当用户在 UI 上点击"恢复购买"按钮时,或者你确信数据未同步时,才调用它。它可能会强制弹出 Apple ID 登录框。
示例代码
swift
extension StoreKitManager {
// UI 绑定的"恢复购买"按钮动作
func restorePurchases() async {
do {
// 1. 强制同步 App Store 交易记录
// 这可能会提示用户输入 Apple ID 密码
try await AppStore.sync()
// 2. 同步完成后,重新检查权益
await updateCustomerProductStatus()
// 3. UI 提示
print("Restore completed successfully")
} catch {
print("Restore failed: \(error)")
}
}
}
最佳实践
- 自动恢复 : App 启动时调用
updateCustomerProductStatus()(遍历currentEntitlements),不要弹窗,静默让老用户获取权益。 - 手动恢复 : 在设置页提供 "Restore Purchases" 按钮,点击后调用
restorePurchases()。 - 多设备同步 : StoreKit 2 自动处理。只要登录同一个 Apple ID,
currentEntitlements会包含所有设备上的购买。
7. 营销功能:折扣与优惠 (Offers)
想给新用户"首月免费"?或者给老用户"回归半价"? StoreKit 2 支持显示推介促销(Introductory Offers)和促销代码(Offer Codes)。
7.1 判断是否展示首购优惠
SK2 会自动判断用户是否有资格享受优惠。你不需要手写复杂的逻辑。
swift
func checkIntroOffer(for product: Product) async {
// 检查是否有优惠
if let subscription = product.subscription,
let introOffer = subscription.introductoryOffer {
// 检查用户是否有资格享受这个优惠
// StoreKit 2 会自动根据用户历史判断 isEligible
let isEligible = await subscription.isEligibleForIntroOffer
if isEligible {
print("原价: \(product.price),优惠价: \(introOffer.price)")
} else {
print("原价: \(product.price)")
}
}
}
7.2 在购买时应用优惠
通常不需要额外代码,product.purchase() 会自动应用用户符合条件的最佳优惠。如果是 Offer Codes(兑换码),用户通常在 App Store 系统级界面输入,或者你可以提供 UI:
swift
// 弹出系统兑换码输入框
func presentCodeRedemptionSheet() {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
Task {
try? await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
}
}
}
8. 测试指南:沙盒 (Sandbox) 与 TestFlight
8.1 沙盒测试流程
- 创建账号 : 登录 App Store Connect -> 用户和访问 -> 沙盒 -> 新增测试员。
- 注意:不要在 iOS 设置中登录此账号!
- 登录: 在 App 内点击购买时,系统弹窗要求登录,此时输入沙盒账号。
- 管理订阅 : iOS 设置 -> App Store -> 沙盒账户 -> 管理。
- 可以在这里修改续订速率(例如 1 个月的订阅在沙盒中每 5 分钟续订一次,重复 6 次后自动过期)。
8.2 测试场景 Checklist
- 新购: 首次购买流程是否顺畅。
- 续期 : 保持 App 打开,观察
Transaction.updates是否收到续订通知。 - 过期: 等待沙盒订阅自动过期,检查 App 权益是否收回。
- 中断购买 : 点击购买后,在支付界面取消,App 是否处理了
.userCancelled。 - 退款: 在沙盒设置中找不到退款?需要去 Xcode -> Debug -> StoreKit -> Manage Transactions (如果是本地配置) 或通过 App Store Connect 模拟。
8.3 调试技巧
在 Xcode 中使用 .storekit 配置文件时:
- Debug -> StoreKit -> Manage Transactions: 可以看到所有本地交易。
- 模拟退款: 选中交易,右键点击 "Refund Transaction"。
- 模拟 Ask to Buy: 开启 "Enable Ask to Buy" 模拟家长审批流程。
9. 最佳实践与常见坑点
常见坑 (Pitfalls)
- 验证失败 : 遇到
VerificationResult.unverified怎么办?- 原因: 可能是越狱设备、中间人攻击或者 Xcode 本地配置证书不匹配。
- 处理 : 绝对不要解锁权益。提示用户"验证失败,请重试"。
- App Store Server Notifications :
- 虽然 StoreKit 2 客户端很强,但为了数据准确性(特别是退款、续费失败),建议后端对接 Server Notifications V2。
- 漏单 :
- 如果 App 闪退,
transaction.finish()未调用,下次启动监听updates时会再次收到该交易,确保逻辑幂等(重复处理同一笔交易不会出错)。
- 如果 App 闪退,
错误处理最佳实践
swift
enum StoreError: Error {
case failedVerification
case userCancelled
case pending
case unknown
}
// 友好的错误提示
func errorMessage(for error: Error) -> String {
if let storeError = error as? StoreKitError {
switch storeError {
case .userCancelled: return "您取消了购买"
case .networkError: return "网络连接失败,请检查网络"
default: return "购买发生未知错误,请稍后重试"
}
}
return error.localizedDescription
}
发布前 Checklist
最后,发布前请对照这张清单:
- App 启动监听了吗? 确保
listenForTransactions在最早的时机运行。 -
Finish 所有的交易了吗?不管成功还是失败(验证不过),都要调用.finish(),否则队列会堵死。 - 是否处理了
.pending状态(家长控制)? - "恢复购买"按钮是否能正常找回权益?
- 是否正确处理了订阅过期和退款?
- 是否在 TestFlight 环境下验证过真实服务器的商品?
- 不要自己存 Bool 值。 尽量每次启动 App 时通过
Transaction.currentEntitlements动态计算用户是不是 VIP。本地存个 isPro = true 很容易因为卸载重装或跨设备导致数据不一致。 - UI 交互。 购买过程中给个 Loading 转圈圈,不要让用户连续点击或因为网络环境以为卡住了。