You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 19:13:17