App Store审核因IAP收据验证问题被拒,沙盒与TestFlight正常
解决方案:调整收据验证逻辑以符合Apple审核要求
根据Apple的审核要求,你需要在收据验证时先尝试生产环境,若收到Sandbox receipt used in production(状态码21007)的错误,再回退到沙箱环境验证。以下是修改后的PurchaseManager代码,集成了完整的收据验证逻辑:
修改后的代码
import StoreKit // 更新协议,添加验证相关回调 protocol PurchaseProtocol: AnyObject { func showSpinner() func hideSpinner() func sendInfo(itemNum: Int) func purchaseFailed(with error: Error?) func purchaseRestored() } class PurchaseManager: NSObject, SKPaymentTransactionObserver, SKPaymentQueueDelegate { weak var delegate: PurchaseProtocol? var itemNumber = 0 static let shared = PurchaseManager() private override init() { super.init() SKPaymentQueue.default().add(self) } func buyInAppPurchase(productID: String) { if SKPaymentQueue.canMakePayments() { let paymentRequest = SKMutablePayment() paymentRequest.productIdentifier = productID SKPaymentQueue.default().add(paymentRequest) } else { delegate?.purchaseFailed(with: NSError(domain: "PurchaseManager", code: -100, userInfo: [NSLocalizedDescriptionKey: "无法进行内购,请检查设置"])) } } func restorePurchases() { print("restore purchases called") delegate?.showSpinner() SKPaymentQueue.default().restoreCompletedTransactions() } func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { delegate?.hideSpinner() } func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { print("restore failed: \(error.localizedDescription)") delegate?.hideSpinner() delegate?.purchaseFailed(with: error) } func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { case .purchased: print("transaction success") delegate?.showSpinner() // 验证收据后再完成交易 validateReceipt { [weak self] success, error in guard let self = self else { return } self.delegate?.hideSpinner() if success { print("收据验证成功") self.delegate?.sendInfo(itemNum: self.itemNumber) self.itemNumber = 0 } else { print("收据验证失败: \(error?.localizedDescription ?? "未知错误")") self.delegate?.purchaseFailed(with: error) } SKPaymentQueue.default().finishTransaction(transaction) } case .failed: delegate?.hideSpinner() print("transaction failed") let error = transaction.error ?? NSError(domain: "PurchaseManager", code: -200, userInfo: [NSLocalizedDescriptionKey: "交易失败"]) print("Transaction failed due to error: \(error.localizedDescription)") delegate?.purchaseFailed(with: error) SKPaymentQueue.default().finishTransaction(transaction) self.itemNumber = 0 case .restored: print("transaction restored") delegate?.showSpinner() // 恢复购买同样需要验证收据 validateReceipt { [weak self] success, error in guard let self = self else { return } self.delegate?.hideSpinner() if success { print("恢复购买收据验证成功") self.delegate?.purchaseRestored() } else { print("恢复购买收据验证失败: \(error?.localizedDescription ?? "未知错误")") self.delegate?.purchaseFailed(with: error) } SKPaymentQueue.default().finishTransaction(transaction) } case .deferred: print("transaction deferred") delegate?.hideSpinner() case .purchasing: print("transaction purchasing") delegate?.showSpinner() @unknown default: print("transaction defaulted") self.itemNumber = 0 delegate?.hideSpinner() } } } // MARK: - 收据验证核心逻辑 /// 获取App Store收据数据 private func getReceiptData() -> Data? { guard let receiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: receiptURL.path), let receiptData = try? Data(contentsOf: receiptURL) else { return nil } return receiptData } /// 验证收据:先尝试生产环境,失败且状态码为21007时回退到沙箱环境 private func validateReceipt(completion: @escaping (Bool, Error?) -> Void) { guard let receiptData = getReceiptData() else { let error = NSError(domain: "PurchaseManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "未找到收据"]) completion(false, error) return } let receiptString = receiptData.base64EncodedString(options: .endLineWithLineFeed) let requestBody = ["receipt-data": receiptString] // 1. 先请求生产环境验证 let productionURL = URL(string: "https://buy.itunes.apple.com/verifyReceipt")! sendValidationRequest(to: productionURL, body: requestBody) { [weak self] result in switch result { case .success(let response): guard let status = response["status"] as? Int else { completion(false, NSError(domain: "AppleValidation", code: -2, userInfo: [NSLocalizedDescriptionKey: "无效的Apple响应"])) return } if status == 0 { // 验证成功 completion(true, nil) } else if status == 21007 { // 状态码21007表示收据是沙箱环境的,回退到沙箱验证 let sandboxURL = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")! self?.sendValidationRequest(to: sandboxURL, body: requestBody, completion: completion) } else { // 其他验证错误 let error = NSError(domain: "AppleValidation", code: status, userInfo: [NSLocalizedDescriptionKey: "收据验证失败,状态码:\(status)"]) completion(false, error) } case .failure(let error): completion(false, error) } } } /// 发送验证请求到Apple服务器 private func sendValidationRequest(to url: URL, body: [String: String], completion: @escaping (Result<[String: Any], Error>) -> Void) { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") do { request.httpBody = try JSONSerialization.data(withJSONObject: body) } catch { completion(.failure(error)) return } URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NSError(domain: "PurchaseManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "未收到响应数据"]))) return } do { guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { completion(.failure(NSError(domain: "PurchaseManager", code: -4, userInfo: [NSLocalizedDescriptionKey: "无效的JSON响应"]))) return } completion(.success(json)) } catch { completion(.failure(error)) } }.resume() } }
关键改动说明
- 更新协议回调:扩展
PurchaseProtocol,新增验证失败和恢复成功的回调方法,让上层业务能处理验证结果。 - 核心验证流程:
getReceiptData():从App Bundle中读取并返回收据文件数据,确保文件存在才继续。validateReceipt():先调用生产环境验证接口,若返回状态码21007则自动切换到沙箱环境重试,完全符合Apple的要求。sendValidationRequest():封装网络请求逻辑,将收据转为Base64字符串后以JSON格式发送到Apple服务器。
- 交易处理优化:在购买和恢复交易完成后,先执行收据验证,验证通过后再完成交易,确保只有合法的交易才会触发业务逻辑。
注意事项
- 服务器端验证推荐:客户端验证仅作参考,Apple官方推荐使用服务器端验证以提升安全性。若你的App采用服务器验证,需在服务器端实现相同的 fallback 逻辑:先请求生产环境,收到21007错误后再请求沙箱环境。
- 状态码扩展处理:可根据Apple官方文档补充其他状态码的处理逻辑,比如网络重试、用户友好提示等。
- 内存安全:使用
weak self避免循环引用,保证内存管理安全。
内容的提问来源于stack exchange,提问作者slimeforest
相关产品推荐
相关产品推荐

