StoreKit2 购买失败:"未知错误" 排查与解决
2025-03-21 05:56:28
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 框架内部发生了某种无法明确归类的错误。问题可能出现在多个环节:
- 网络问题: 用户的设备可能暂时失去网络连接,或者网络状况不稳定。
- Apple 服务器问题: Apple 的服务器可能暂时宕机、维护或过载。
- 配置问题:
- App Store Connect 中的商品配置错误,例如定价、地区等设置有问题。
- 应用的 Bundle ID 或 entitlements 配置不正确。
- 沙盒环境测试时,测试账户可能存在问题。
- 代码逻辑问题: 尽管代码看起来没啥大问题,但可能存在某些极端情况的处理遗漏。例如, 同时发起了多个购买请求等.
- 系统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 购买失败的问题, 给用户一个流畅的购买体验.