StoreKit内购恢复异常:未购买用户可成功恢复至高级版
非消耗型内购恢复功能异常问题排查与修复
已上线应用中使用StoreKit实现移除广告的非消耗型内购,出现异常:从未购买过的新用户点击恢复购买按钮后,会被判定为恢复成功并切换至高级版。
核心错误点
1. paymentQueueRestoreCompletedTransactionsFinished 方法逻辑错误
这个方法是恢复流程全部结束时的回调,但当前代码只要进入这个方法就直接返回.restored状态,完全忽略用户是否有实际可恢复的购买记录。哪怕用户从未购买过,恢复流程结束后该方法依然会被调用,直接触发高级版状态切换。
正确逻辑:只有当updatedTransactions中出现.restored状态的交易时,才判定为恢复成功;paymentQueueRestoreCompletedTransactionsFinished仅用于处理无恢复交易的情况(比如提示用户未购买过)。
2. ViewModel中恢复流程的回调设置顺序错误
在restoreAction里,先调用restorePurchase()再设置purchaseProductComplition,若恢复流程执行速度快,会出现回调还未设置就触发的情况,导致逻辑混乱(虽不是当前问题的直接原因,但属于潜在bug)。应先设置回调,再发起恢复请求。
3. 未验证恢复交易的有效性
当前代码没有检查恢复的交易是否对应目标内购商品,也未统计实际恢复的交易数量,无法区分“真正恢复购买”和“无任何可恢复交易”两种场景。
修复后的代码示例
修改IAPManager的恢复相关方法
// 修改恢复完成的回调方法 func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { // 检查是否有实际恢复成功的交易 let hasRestoredTransactions = queue.transactions.contains { $0.transactionState == .restored } if !hasRestoredTransactions { // 无可用恢复交易,返回失败状态 if let completion = self.purchaseProductComplition { completion(IAPHandlerAlertType.restoreFailed, NSError(domain: "IAPError", code: -1, userInfo: [NSLocalizedDescriptionKey: "没有可恢复的购买记录"])) } } // 若有恢复交易,已在updatedTransactions中处理 } // 完善updatedTransactions中的.restored逻辑,增加商品ID验证 func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for trans in transactions { switch trans.transactionState { case .restored: // 验证恢复的商品是否为目标内购 if trans.payment.productIdentifier == "com.identifier.appName.removeAds" { log("Product restored") SKPaymentQueue.default().finishTransaction(trans) if let completion = self.purchaseProductComplition { completion(IAPHandlerAlertType.restored, nil) } } else { // 非目标商品,直接结束交易 SKPaymentQueue.default().finishTransaction(trans) } break case .purchased: log("Product purchase done") SKPaymentQueue.default().finishTransaction(trans) if let completion = self.purchaseProductComplition { completion(IAPHandlerAlertType.purchased, nil) } break case .failed: log("Product purchase failed") SKPaymentQueue.default().finishTransaction(trans) if let completion = self.purchaseProductComplition { completion(IAPHandlerAlertType.failed, trans.error) } break default: break } } }
修改ViewModel的restoreAction方法
func restoreAction() { Spinner.start() IAPManager.shared.fetchAvailableProducts { products in if products.count > 0 { // 先设置回调,再发起恢复请求 IAPManager.shared.purchaseProductComplition = { [self] result, error in self.handlePurchaseRestoreResult(result: result, error: error) Spinner.stop() } IAPManager.shared.restorePurchase() } else { Spinner.stop() } } }
优化handlePurchaseRestoreResult方法
func handlePurchaseRestoreResult(result: IAPHandlerAlertType, error: Error?) { if let err = error { showAlert = .init(id: .error) print("恢复错误:\(err.localizedDescription)") return } switch result { case .disabled: showAlert = .init(id: .disabled) case .purchased, .restored: Defaults.isPremiumPurchased = true if case .purchased = result { Defaults.totalCoins += 1000 } isPremiumPurchased = 1 case .failed, .restoreFailed: showAlert = .init(id: .failed) default: break } }
内容的提问来源于stack exchange,提问作者Coder
相关产品推荐
相关产品推荐

