StoreKit 2中消耗型内购是否需保留Transaction监听器?
消耗型内购的Transaction.updates监听器作用与金币发放逻辑解析
1. 监听器的核心作用
这个监听器是用来兜底处理未被即时捕获的交易的,常见触发场景包括:
- 用户发起购买后,App在交易完成前崩溃/被杀死,此时
purchase方法的回调根本没机会执行; - 交易需要额外验证(比如SCA强认证、家庭共享的Ask to Buy审批),用户切到其他App完成验证后再返回,
purchase的回调已经失效; - App重启后,还有之前未标记为完成的交易(比如上次处理时没调用
transaction.finish())。
如果没有这个监听器,这些场景下用户付了钱却拿不到金币,会直接引发用户信任危机——这也是Xcode弹出警告的核心原因。
2. 两处"Grant 200 coins"会不会重复发放?
处理不当会,但正确操作可完全避免:
- 每个交易的
transaction.id是唯一标识,且Transaction.updates只会返回**未调用过finish()**的交易。只要你在发放金币后立刻调用transaction.finish(),这个交易就不会再出现在监听器的数据流里。 - 但如果没记录已处理的交易ID,极端情况(比如
finish()调用失败、App在发放后立刻崩溃)下,监听器可能会再次捕获到同一个交易,导致重复发放。所以必须额外记录已处理的交易ID(比如存在UserDefaults或后端服务器),处理前先检查该ID是否已被处理过。
你当前的代码里,purchase和监听器都有发放逻辑,确实存在重复风险:比如用户购买成功后,purchase里发了金币并调用finish(),但如果App在finish()执行前崩溃,监听器重启后会再次处理这个交易,导致重复发币。
3. 应该在哪里发放金币?
最佳实践是统一在Transaction.updates监听器里处理发放逻辑,原因如下:
- 所有交易场景(即时成功、后台完成、重启后遗留)都能被统一处理,不会遗漏;
- 避免
purchase方法和监听器的逻辑重复,减少出错概率; purchase方法只负责UI层面的反馈:比如显示加载状态、提示用户交易pending/取消/失败,不用碰金币发放的核心逻辑。
修改后的代码示例:
调整purchase方法(仅处理UI反馈)
func purchase(_ product: Product) async { do { let result = try await product.purchase() switch result { case .success(.verified), .success(.unverified): print("购买请求已提交,请等待金币到账") case .pending: print("交易等待验证,请完成身份验证或等待家长审批") case .userCancelled: print("你取消了购买") @unknown default: print("购买失败,请重试") } } catch { print("购买失败:\(error.localizedDescription)") } }
完善监听器(处理发放+去重逻辑)
extension AppDelegate { func observeTransactionUpdates() { Task { for await result in Transaction.updates { switch result { case .verified(let transaction): let transactionID = transaction.id.uuidString // 检查是否已处理过该交易(示例用UserDefaults,建议后端存储更可靠) let processedTransactions = UserDefaults.standard.stringArray(forKey: "ProcessedTransactionIDs") ?? [] guard !processedTransactions.contains(transactionID) else { await transaction.finish() continue } if transaction.productID == ProductID.coins_200_price_1_95.rawValue { print("发放200金币") print("交易ID:\(transactionID)") // 记录已处理的交易ID var updatedTransactions = processedTransactions updatedTransactions.append(transactionID) UserDefaults.standard.set(updatedTransactions, forKey: "ProcessedTransactionIDs") } await transaction.finish() case .unverified(_, let error): print("交易验证失败:\(error.localizedDescription)") } } } } }
内容的提问来源于stack exchange,提问作者Pratik
相关产品推荐
相关产品推荐

