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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 07:27:33