内购类型
App 内购买是 iOS 上变现的官方通道。类型分四种:消耗型(一次用掉)、非消耗型(买断功能)、自动续期订阅、非自动续期订阅。选错类型,功能逻辑全要返工。
内购不是"加个支付",是商品、订阅和账务三套体系。
消耗型适合金币、道具;非消耗型适合解锁专业版;订阅适合持续服务。苹果对虚拟商品强制走内购,实物商品走自己的支付,别混。
配置与沙盒
先到 App Store Connect 建商品:标识符、价格、本地化、审核信息。开发期用 StoreKit 配置文件在 Xcode 里本地模拟,不碰真钱;真机测试用沙盒账号。
// StoreKit 配置文件(.storekit)本地模拟购买
let products = try await Product.products(for: ["com.app.pro"])
for product in products {
print(product.displayName, product.displayPrice)
}
- com.app.pro非消耗型。
- com.app.coins.100消耗型。
- com.app.sub.month订阅。
沙盒账号在 App Store 里退出登录后创建,测试购买不真实扣款。订阅测试还有快速续期按钮,方便测周期逻辑。
商品列表
别把价格写死在代码里:从 StoreKit 拉商品,displayPrice 已经带货币和本地化。商品拉不到就隐藏购买入口,并提示稍后重试。
Task {
let products = try await Product.products(for: productIDs)
await MainActor.run {
self.products = products
// 刷新 UI
}
}
商品状态用 Product.ProductType 区分,订阅商品还要拉 subscription group 和周期信息。服务端要同步一份商品配置,客户端和服务端各自校验。
商品列表展示顺序和文案也要由服务端下发,改价改名不用重新发版。
购买流程
StoreKit 2 的购买是异步的:purchase() 返回结果,判断 success、pending、userCancelled 三种状态。用户取消不是失败,别弹错误。
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish()
// 发放权益
case .userCancelled:
break // 用户取消,安静退出
case .pending:
print("等待家长批准或外部支付")
}
pending 是家长控制或外部支付(如部分国家)的等待态,别当成失败。购买完成先校验再发权益,顺序不能反。
购买完成后的权益发放要讲究幂等:交易 ID 只处理一次,重复收到同一笔交易直接跳过,防止用户重复购买同一商品时权益被叠加或错乱。消耗型商品的余额变动放服务端记账,客户端展示即可,避免本地篡改。非消耗型解锁则要和服务端对账,用户换设备重装后能通过恢复找回已购权益。所有购买结果都要记录日志,但别记录交易票据全文,摘要即可,完整票据只发往服务端校验,别留在客户端日志里。
UI 上,购买按钮在流程进行中要禁用并显示加载态,防止连点;交易完成、失败、取消三种结果给出不同的提示文案,取消时保持静默即可。用户等待时给个明确进度,购买弹窗是系统级的,应用内只需要做好前置校验。
校验与收据
StoreKit 2 的 Transaction 自带验证:VerificationResult 判断签名有效性。服务端推荐用 App Store Server API 校验,客户端校验只做第一道防线。
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw StoreError.failedVerification
case .verified(let safe):
return safe
}
}
校验失败一律不发权益。服务端要验交易 ID 是否被重复使用,防止刷单。旧版收据校验 URL 已弃用,新项目直接用 App Store Server API。
订阅与恢复
订阅要处理到期、续订、升级降级、退款撤销。App 启动时用 currentEntitlements 判断当前权益,订阅变更即时更新 UI。
Task {
for await result in Transaction.updates {
let transaction = try checkVerified(result)
await transaction.finish()
// 重新计算订阅权益并刷新界面
}
}
恢复购买按钮调 currentEntitlements 或 Transaction.latest,把已购权益找回来。订阅组的服务端通知(Server Notification V2)要接入,退款撤销靠它及时处理。
小结
StoreKit 2 把内购做成清晰的异步流:Product 拉商品、purchase 发起购买、VerificationResult 校验、Transaction 落账。订阅靠 currentEntitlements 判断权益、Transaction.updates 监听变化。别忘了沙盒测试、服务端二次校验和退款监听,这三样做到位,内购才算真的上线。