返回

StoreKit2 购买失败:"未知错误" 排查与解决

IOS

StoreKit2 购买失败:“未知错误” 问题排查与解决

一、问题

在使用 StoreKit2 进行订阅购买时,偶尔会遇到 “an unknown error occurred” 错误,导致支付失败。 这让用户体验大打折扣, 也让我们这些开发者头疼。 下面这块代码是出问题的购买逻辑:

func purchaseProduct(product: Product, source: String) async -> Bool {
        do {
            // Start the purchase
            let result = try await product.purchase()
            
            // Handle the result of the purchase
            switch result {
            case .success(let verificationResult):
                switch verificationResult {
                case .verified(let transaction):
                    self.transactionState = "Purchase Successful"
                    await transaction.finish()
                    return true
                case .unverified(let transaction, let error):
                    self.transactionState = "Purchase Unverified: \(error.localizedDescription)"
                    await transaction.finish()
                  
                    DispatchQueue.main.async {
                        showMessageWithTitle("Error!", "There was an error processing your purchase", .error)
                        
                        Amplitude.sharedInstance.track(
                            eventType: "payment_failed",
                            eventProperties: ["PlanId": product.id, "Source": source, "Error": error.localizedDescription]
                        )
                    }
                    return false
                }
            case .userCancelled:
                self.transactionState = "User cancelled the purchase."
               
                DispatchQueue.main.async {
                    Amplitude.sharedInstance.track(
                        eventType: "payment_cancelled",
                        eventProperties: ["PlanId": product.id, "Source": source]
                    )
                }
                return false
                
            case .pending:
                self.transactionState = "Purchase is pending."
                
                DispatchQueue.main.async {
                    showMessageWithTitle("Error!", "There was an error processing your purchase", .error)
                }
                return false
                
            @unknown default:
                self.transactionState = "Unknown purchase result."
               
                DispatchQueue.main.async {
                    showMessageWithTitle("Error!", "There was an error processing your purchase", .error)
                    
                    Amplitude.sharedInstance.track(
                        eventType: "payment_failed",
                        eventProperties: ["PlanId": product.id, "Source": source, "Error": "unknown"]
                    )
                }
                
                return false
            }
        } catch {
            self.transactionState = "Purchase failed: \(error.localizedDescription)"
        
            DispatchQueue.main.async {
                showMessageWithTitle("Error!", "There was an error processing your purchase", .error)
                
                Amplitude.sharedInstance.track(
                    eventType: "payment_failed",
                    eventProperties: ["PlanId": product.id, "Source": source, "Error": error.localizedDescription]
                )
            }
            return false
        }
    }

二、问题原因分析

“未知错误” (unknown error) 通常意味着 StoreKit 框架内部发生了某种无法明确归类的错误。问题可能出现在多个环节:

  1. 网络问题: 用户的设备可能暂时失去网络连接,或者网络状况不稳定。
  2. Apple 服务器问题: Apple 的服务器可能暂时宕机、维护或过载。
  3. 配置问题:
    • App Store Connect 中的商品配置错误,例如定价、地区等设置有问题。
    • 应用的 Bundle ID 或 entitlements 配置不正确。
    • 沙盒环境测试时,测试账户可能存在问题。
  4. 代码逻辑问题: 尽管代码看起来没啥大问题,但可能存在某些极端情况的处理遗漏。例如, 同时发起了多个购买请求等.
  5. 系统bug: iOS 或 StoreKit 框架本身可能存在的 bug。

三、解决方案

针对上述可能的原因,我们可以采取以下措施来排查和解决这个问题:

1. 增强错误处理和日志记录

完善代码中的错误处理逻辑, 获取更详细的错误信息, 便于问题定位。

func purchaseProduct(product: Product, source: String) async -> Bool {
    do {
        let result = try await product.purchase()

        switch result {
        case .success(let verificationResult):
            switch verificationResult {
            case .verified(let transaction):
                self.transactionState = "Purchase Successful"
                await transaction.finish()
                return true
            case .unverified(_, let error):
                self.transactionState = "Purchase Unverified: \(error)" // 打印更详细的错误
                //  记录更详细的错误信息到日志系统,例如使用 os_log
                os_log("Purchase Unverified: %{public}@", log: .default, type: .error, error.localizedDescription)
                return false
            }
        case .userCancelled:
            self.transactionState = "User cancelled the purchase."
            return false
        case .pending:
            self.transactionState = "Purchase is pending."
            return false
        @unknown default:
            self.transactionState = "Unknown purchase result."
            return false
        }
    } catch {
        self.transactionState = "Purchase failed: \(error)" // 打印更详细的错误
        // 记录更详细的错误信息到日志系统
        os_log("Purchase failed: %{public}@", log: .default, type: .error, error.localizedDescription)
           if let storeKitError = error as? StoreKitError {
              switch storeKitError{
                case .unknown:
                    print("Unknown error.  Please contact support")
                    //处理未知错误
                case .userCancelled:
                   print("User cancelled the request")
                    //用户取消
                case .networkError(_):
                    print("Network error")
                case .systemError(_):
                    print("System error")
                  //...其他case, 可以参考StoreKitError的枚举值进行处理.
                default:
                    print("Other StoreKit error")
                }
           }
        return false
    }
}
  • 原理: 捕获 StoreKitError,并针对不同的错误类型进行更细粒度的处理和日志记录。使用 os_log 可以将错误信息记录到设备的控制台中,方便调试。
  • 额外说明: 使用第三方日志服务(如上面代码的Amplitude)时,注意脱敏处理,不要记录用户的敏感信息。

2. 检查网络连接

在发起购买请求前, 简单检查下网络连接是否正常。

import Network

func isNetworkAvailable() -> Bool {
    let monitor = NWPathMonitor()
    let semaphore = DispatchSemaphore(value: 0)
    var isAvailable = false

    monitor.pathUpdateHandler = { path in
        isAvailable = path.status == .satisfied
        semaphore.signal()
    }

    let queue = DispatchQueue(label: "NetworkMonitor")
    monitor.start(queue: queue)

    semaphore.wait()
    monitor.cancel()

    return isAvailable
}

func purchaseProduct(product: Product, source: String) async -> Bool {

    guard isNetworkAvailable() else {
        //网络不可用的一些提示。
        print("网络连接不可用")
          self.transactionState = "Purchase failed: Network unavailable"
        return false
    }

     //原来的代码逻辑.
    do{
     //...
    }catch{
     //...
    }

    return false;
}
  • 原理: 使用 NWPathMonitor 检查网络连接状态。如果网络不可用,直接返回,避免不必要的请求。
  • 注意点: 只是简单的检查, 对于复杂的网络状况, 建议进行重试机制(在4里会提及).

3. 检查 App Store Connect 配置

仔细检查 App Store Connect 中的商品配置:

  • 商品 ID: 确保代码中使用的商品 ID 与 App Store Connect 中配置的完全一致。
  • 定价: 检查商品的定价是否正确,是否已在所有目标地区生效。
  • 状态: 确保商品的状态为“已批准”或“已准备好提交”。
  • 沙盒环境: 如果在沙盒环境中测试,确保使用了正确的测试账户,并且该账户已启用沙盒测试。

4. 实现重试机制

对于网络错误或 Apple 服务器暂时不可用的情况,可以实现重试机制。

func purchaseProductWithRetry(product: Product, source: String, maxRetries: Int = 3) async -> Bool {
    for attempt in 0..<maxRetries {
        if await purchaseProduct(product: product, source: source) {
            return true
        }

         //  如果是特定类型的错误(如网络错误),则进行重试
         if  let error = self.transactionState.components(separatedBy: ": ").last,
                error.contains("Network") || error.contains("timed out"){
            // 指数退避, 延迟增加重试间隔
              let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000 // 纳秒
               do{
                 try await Task.sleep(nanoseconds: delay)
             }catch{
                print("sleep error:\(error)") //可以忽略此error, 或记录日志.
              }
         }else{
            //如果不是网络类型的错误, 不重试.
            return false;
         }
    }
     // 重试全部失败
      self.transactionState = "Purchase failed after multiple retries"
      print("Purchase failed after multiple retries")

    return false
}

  • 原理:purchaseProduct 方法的基础上,增加重试逻辑。如果购买失败,并且错误类型指示可以重试,就进行有限次数的重试。采用指数退避算法逐渐增加重试间隔,避免对服务器造成过大压力。

5. 监听交易更新

即使 product.purchase() 没有抛出错误,交易也可能在稍后失败。StoreKit2 提供了 Transaction.updates 来监听交易状态的更新。

import StoreKit

class TransactionManager: ObservableObject {
    @Published var transactions: [Transaction] = []
    var updates: Task<Void, Never>? = nil

    init() {
        updates = listenForTransactions()
    }
    
    deinit{
        updates?.cancel()
        updates = nil
    }

    func listenForTransactions() -> Task<Void, Never> {
        return Task.detached { [unowned self] in
            for await result in Transaction.updates {
                switch result {
                case .verified(let transaction):
                    // 交易成功, 可以更新UI, 并完成交易
                    print("Transaction verified: \(transaction.productID)")
                    await transaction.finish()
                     self.transactions.append(transaction)
                case .unverified(_, let error):
                    // 交易验证失败. 需要进行日志记录并提示给用户(或者进行客服引导)
                      print("Transaction unverified: \(error)")
                }
            }
        }
    }
}
  • 原理: 使用 Transaction.updates 异步序列来监听交易状态的变化。对于验证失败的交易,记录日志并采取相应措施。
  • 如何使用: 可以在App启动的时候创建一个TransactionManager 的实例。这个实例将持续监听交易状态的变化。
  • 注意点: Transaction.updates是一个无限的异步序列,为了避免内存泄露,需要在合适的时机(如上面代码的deinit)将其取消掉.

6. 用户支持和反馈

如果以上方法都无法解决问题,建议在应用内提供用户支持渠道,例如反馈表单或客服邮箱。收集用户的设备信息、错误日志、购买时间等详细信息,以便进一步排查。

进阶技巧:

  • 检查服务器收据:
    在交易完成后, 将收据发送到自己的服务器进行验证. 苹果提供了验证收据的API, 确保购买的真实有效性。

  • 使用 StoreKitTest (Xcode 14+)
    Xcode 14及更新版本,苹果提供了 StoreKitTest framework 来进行本地的StoreKit测试, 可以更全面的模拟各种情况。

通过这些步骤,应该能够定位并解决大部分 StoreKit2 购买失败的问题, 给用户一个流畅的购买体验.