iOS 内购-Swift

1、Xcode配置
截屏2025-07-18 17.25.44.png
2、代码逻辑
  • 获取产品列表(ProductIDs产品id可以让服务端返回,这样比写在本地灵活)
// MARK: - 自定义错误
enum SubscriptionError: LocalizedError {
    case cannotMakePayments
    case receiptValidationFailed
    case productNotFound
    case networkError
    case unknownError
    
    var errorDescription: String? {
        switch self {
        case .cannotMakePayments:
            return "设备不支持内购"
        case .receiptValidationFailed:
            return "收据验证失败"
        case .productNotFound:
            return "产品未找到"
        case .networkError:
            return "网络错误"
        case .unknownError:
            return "未知错误"
        }
    }
}

// MARK: - 产品ID定义
struct ProductIDs {
    // 自动续订订阅
    static let monthlySubscription = "com.demo.monthly.sub"
    static let quarterlySubscription = "com.demo.quarterly.sub"
    static let yearlySubscription = "com.demo.yearly.sub"

    // 非续订会员
    static let onetimeMonthly = "com.demo.onetime.monthly"
    static let onetimeQuarterly = "com.demo.onetime.quarterly"
    static let onetimeYearly = "com.demo.onetime.yearly"

    static let allProducts = [
        monthlySubscription,
        quarterlySubscription,
        yearlySubscription,
        onetimeMonthly,
        onetimeQuarterly,
        onetimeYearly
    ]
}

// MARK: - 属性
private var products: [String: SKProduct] = [:]
private var productsRequest: SKProductsRequest?
private var purchaseCompletionHandlers: [String: (PurchaseResult) -> Void] = [:]
private var loadProductsCompletion: ((Swift.Result<[SKProduct], Error>) -> Void)?
// 收据刷新完成回调
private var receiptRefreshCompletion: ((Bool) -> Void)?
// 收据刷新超时定时器
private var receiptRefreshTimer: Timer?

// 回调闭包
var onPurchaseSuccess: ((String) -> Void)?
var onPurchaseFailure: ((Error) -> Void)?
var onRestoreComplete: (([String]) -> Void)?

    /// 加载所有产品信息
    func loadProducts(completion: @escaping (Swift.Result<[SKProduct], Error>) -> Void) {
        guard !ProductIDs.allProducts.isEmpty else {
            completion(.failure(SubscriptionError.productNotFound))
            return
        }
        
        loadProductsCompletion = completion
        
        let productIdentifiers = Set(ProductIDs.allProducts)
        productsRequest?.cancel()
        productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productsRequest?.delegate = self
        productsRequest?.start()
    }

// MARK: - SKProductsRequestDelegate
extension SubscriptionManager: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let loadedProducts = response.products
        
        DDLogInfo("[\(TAG)] ✅ 获取到所有产品,数量=\(loadedProducts.count)")
        
        // 缓存产品信息
        for product in loadedProducts {
            products[product.productIdentifier] = product
        }
        
        // 打印无效的产品ID
        if !response.invalidProductIdentifiers.isEmpty {
            DDLogInfo("[\(TAG)] ⚠️ 无效的产品ID: \(response.invalidProductIdentifiers)")
        }
        
        // 调用完成回调
        DispatchQueue.main.async {
            self.loadProductsCompletion?(.success(loadedProducts))
            self.loadProductsCompletion = nil
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        if request is SKReceiptRefreshRequest {
            DDLogInfo("[\(TAG)] ❌ 收据刷新失败: \(error.localizedDescription)")
        } else {
            DDLogInfo("[\(TAG)] ❌ 产品请求失败: \(error.localizedDescription)")
            
            DispatchQueue.main.async {
                self.loadProductsCompletion?(.failure(error))
                self.loadProductsCompletion = nil
            }
        }
    }
}

    /// 加载单个产品信息
    /// - Parameters:
    ///   - identifier: 产品标识符
    ///   - completion: 完成回调,返回产品信息或错误
    func loadSingleProduct(with identifier: String, completion: @escaping (Swift.Result<SKProduct, Error>) -> Void) {

        
        // 如果产品已经缓存,直接返回
        if let cachedProduct = products[identifier] {
            DDLogInfo("[\(TAG)] ✅ 从缓存获取产品: \(identifier)")
            completion(.success(cachedProduct))
            return
        }
        
        DDLogInfo("[\(TAG)] 🔄 开始加载单个产品: \(identifier)")
        
        // 创建临时的完成回调
        let originalCompletion = loadProductsCompletion
        loadProductsCompletion = { result in
            switch result {
            case .success(let products):
                // 查找目标产品
                if let targetProduct = products.first(where: { $0.productIdentifier == identifier }) {
                    DDLogInfo("[\(self.TAG)] ✅ 单个产品加载成功: \(identifier)")
                    completion(.success(targetProduct))
                } else {
                    DDLogError("[\(self.TAG)] ❌ 未找到指定产品: \(identifier)")
                    completion(.failure(SubscriptionError.productNotFound))
                }
                
            case .failure(let error):
                DDLogError("[\(self.TAG)] ❌ 单个产品加载失败: \(identifier), 错误: \(error.localizedDescription)")
                completion(.failure(error))
            }
            
            // 恢复原始回调
            self.loadProductsCompletion = originalCompletion
        }
        
        // 发起请求(只请求指定的产品)
        let productIdentifiers = Set([identifier])
        productsRequest?.cancel()
        productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productsRequest?.delegate = self
        productsRequest?.start()
    }
    
    /// 获取指定产品
    func getProduct(with identifier: String) -> SKProduct? {
        return products[identifier]
    }
    
    /// 获取所有已加载的产品
    func getAllProducts() -> [SKProduct] {
        return Array(products.values)
    }
  • 调用示例
      // 调用示例:
        SubscriptionManager.shared.loadProducts { [weak self] result in
            guard let self = self else { return }
            
            // 隐藏加载状态
            // DispatchQueue.main.async {
            //     self.hideLoadingIndicator()
            // }
            
            switch result {
            case .success(let products):
                print("✅ 成功加载 \(products.count) 个产品")
                
                // 打印产品详情
                for product in products {
                    print("产品ID: \(product.productIdentifier)")
                    print("产品名称: \(product.localizedTitle)")
                    print("产品价格: \(product.priceLocale.currencySymbol ?? "")\(product.price)")
                    print("产品描述: \(product.localizedDescription)")
                    print("---")
                   let isIntroductory =   SubscriptionManager.shared.hasIntroductoryOffer(for: product.productIdentifier)
                    
                    if isIntroductory {
                       let IntroductoryInfo =  SubscriptionManager.shared.getIntroductoryOfferInfo(for: product.productIdentifier)
                        print("介绍性优惠信息:\(IntroductoryInfo ?? "")")
                        
                    }
                    
                }
                
                
            case .failure(let error):
                print("❌ 加载产品失败: \(error.localizedDescription)")
                
                DispatchQueue.main.async {
                    // 显示错误提示
                    let alert = UIAlertController(
                        title: "加载失败",
                        message: "无法加载订阅产品,请检查网络连接后重试",
                        preferredStyle: .alert
                    )
                    alert.addAction(UIAlertAction(title: "确定", style: .default))
                    self.present(alert, animated: true)
                }
            }
        }

  • 发起购买
    /// 购买产品
    func purchaseProduct(with identifier: String, completion: @escaping (PurchaseResult) -> Void) {
        guard SKPaymentQueue.canMakePayments() else {
            completion(.failure(SubscriptionError.cannotMakePayments))
            return
        }
        
        guard let product = products[identifier] else {
            completion(.failure(SubscriptionError.productNotFound))
            return
        }
        
        purchaseCompletionHandlers[identifier] = completion
        
        let payment = SKMutablePayment(product: product)
        SKPaymentQueue.default().add(payment)
    }
//  SKPaymentTransactionObserver 代理方法
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchasing:
                DDLogInfo("[\(TAG)] 🔄 正在购买: \(transaction.payment.productIdentifier)")
                
            case .purchased:
                DDLogInfo("[\(TAG)] ✅ 购买成功: \(transaction.payment.productIdentifier)")
                handlePurchased(transaction)
                
            case .failed:
                DDLogInfo("[\(TAG)] ❌ 购买失败: \(transaction.payment.productIdentifier)")
                handleFailed(transaction)
                
            case .restored:
                DDLogInfo("[\(TAG)] 🔄 恢复购买: \(transaction.payment.productIdentifier)")
                handleRestored(transaction)
                
            case .deferred:
                DDLogInfo("[\(TAG)] ⏳ 购买延迟: \(transaction.payment.productIdentifier)")
                handleDeferred(transaction)
                
            @unknown default:
                DDLogInfo("[\(TAG)] ❓ 未知交易状态: \(transaction.transactionState.rawValue)")
                break
            }
        }
    }
    ```
    
-  ######**购买成功处理**
/// 购买成功处理
/// - Parameter transaction: SKPaymentTransaction
private func handlePurchased(_ transaction: SKPaymentTransaction) {
    let productIdentifier = transaction.payment.productIdentifier
    // 💰 到这里时,用户的钱已经被扣了
    // 验证收据
    validateReceipt(for: transaction) { [weak self] isValid in
        DispatchQueue.main.async {
            if isValid {
                // 设置购买状态
                self?.setPurchased(productIdentifier, purchased: true)
                
                // 调用成功回调
                self?.onPurchaseSuccess?(productIdentifier)
                self?.completePurchase(for: productIdentifier, result: .success(transaction))
            } else {
                self?.onPurchaseFailure?(SubscriptionError.receiptValidationFailed)
                self?.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.receiptValidationFailed))
            }
            
            // 完成交易,释放资源
            SKPaymentQueue.default().finishTransaction(transaction)
        }
    }
}
```
  • 购买失败处理
    /// 购买失败处理
    /// - Parameter transaction: SKPaymentTransaction
    private func handleFailed(_ transaction: SKPaymentTransaction) {
        let productIdentifier = transaction.payment.productIdentifier
        let error = transaction.error
        
        DispatchQueue.main.async {
            if let skError = error as? SKError {
                switch skError.code {
                case .paymentCancelled:
                    // 用户主动取消购买 - 这是正常行为,不需要显示错误信息
                    DDLogInfo("[\(self.TAG)] 💡 用户取消购买: \(productIdentifier)")
                    self.completePurchase(for: productIdentifier, result: .cancelled)
                    
                case .paymentNotAllowed:
                    // 设备不允许内购 - 可能是家长控制限制或设备限制
                    DDLogInfo("[\(self.TAG)] 🚫 设备不允许内购: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
                    
                case .paymentInvalid:
                    // 支付信息无效 - 产品ID错误或产品不存在
                    DDLogInfo("[\(self.TAG)] ❌ 支付信息无效: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.productNotFound)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
                    
                case .cloudServiceNetworkConnectionFailed:
                    // iCloud服务网络连接失败 - 网络问题
                    DDLogInfo("[\(self.TAG)] 🌐 iCloud服务网络连接失败: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.networkError)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.networkError))
                    
                case .cloudServicePermissionDenied:
                    // iCloud服务权限被拒绝 - 用户未登录iCloud或权限问题
                    DDLogInfo("[\(self.TAG)] 🔒 iCloud服务权限被拒绝: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
                    
                case .cloudServiceRevoked:
                    // iCloud服务被撤销 - 用户的iCloud账户可能被停用
                    DDLogInfo("[\(self.TAG)] ⚠️ iCloud服务被撤销: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
                    
                case .privacyAcknowledgementRequired:
                    // 需要确认隐私协议 - 用户需要在App Store中确认隐私条款
                    DDLogInfo("[\(self.TAG)] 📋 需要确认隐私协议: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
                    
                case .unauthorizedRequestData:
                    // 未授权的请求数据 - 请求包含无效数据
                    DDLogInfo("[\(self.TAG)] 🔐 未授权的请求数据: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.unknownError)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.unknownError))
                    
                case .invalidOfferIdentifier:
                    // 无效的优惠标识符 - 促销优惠ID错误
                    DDLogInfo("[\(self.TAG)] 🎟️ 无效的优惠标识符: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.productNotFound)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
                    
                case .invalidSignature:
                    // 无效的签名 - 促销优惠签名验证失败
                    DDLogInfo("[\(self.TAG)] ✍️ 无效的签名: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.receiptValidationFailed)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.receiptValidationFailed))
                    
                case .missingOfferParams:
                    // 缺少优惠参数 - 促销优惠必需参数缺失
                    DDLogInfo("[\(self.TAG)] 📝 缺少优惠参数: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.productNotFound)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
                    
                case .invalidOfferPrice:
                    // 无效的优惠价格 - 促销价格与服务器不匹配
                    DDLogInfo("[\(self.TAG)] 💰 无效的优惠价格: \(productIdentifier)")
                    self.onPurchaseFailure?(SubscriptionError.productNotFound)
                    self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
                    
                default:
                    // 其他未知的SKError类型
                    DDLogInfo("[\(self.TAG)] ❓ 其他SKError类型: \(skError.code.rawValue), \(productIdentifier)")
                    self.onPurchaseFailure?(error ?? SubscriptionError.unknownError)
                    self.completePurchase(for: productIdentifier, result: .failure(error ?? SubscriptionError.unknownError))
                }
            } else {
                // 非SKError类型的错误 - 可能是网络错误或其他系统错误
                DDLogInfo("[\(self.TAG)] 🔥 非SKError类型错误: \(error?.localizedDescription ?? "未知错误"), \(productIdentifier)")
                self.onPurchaseFailure?(error ?? SubscriptionError.unknownError)
                self.completePurchase(for: productIdentifier, result: .failure(error ?? SubscriptionError.unknownError))
            }
            
            // 完成交易,释放资源
            SKPaymentQueue.default().finishTransaction(transaction)
        }

  • 支付成功收据验证处理(到这里时,用户的钱已经被扣了)
    private func validateReceipt(for transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
        guard let receiptURL = Bundle.main.appStoreReceiptURL else {
            DDLogInfo("[\(TAG)] ⚠️ 无法获取收据URL")
            completion(false)
            return
        }

        guard let receiptData = try? Data(contentsOf: receiptURL) else {
            // 如果收据不存在,尝试刷新(带超时处理)
            refreshReceiptWithTimeout { success in
                if success {
                    self.validateReceipt(for: transaction, completion: completion)
                } else {
                    completion(false)
                }
            }
            return
        }
        
        // 这里应该发送到你的服务器进行验证
        // 示例:调用服务器验证API
        validateReceiptWithServer(receiptData, transaction: transaction) { isValid in
            completion(isValid)
        }
    }
    
    /// 带超时处理的收据刷新
    private func refreshReceiptWithTimeout(completion: @escaping (Bool) -> Void) {
        DDLogInfo("[\(TAG)] 🔄 开始刷新收据...")
        
        // 设置超时定时器(15秒)
        receiptRefreshTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
            guard let self = self else { return }
            DDLogWarn("[\(self.TAG)] ⏰ 收据刷新超时")
            self.receiptRefreshCompletion?(false)
            self.receiptRefreshCompletion = nil
            self.receiptRefreshTimer?.invalidate()
            self.receiptRefreshTimer = nil
        }
        
        // 保存完成回调
        receiptRefreshCompletion = completion
        
        // 开始刷新请求
        let request = SKReceiptRefreshRequest()
        request.delegate = self
        request.start()
    }
    
    private func validateReceiptWithServer(_ receiptData: Data, transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
        // 这里是服务器验证的示例实现
        // 实际开发中应该调用你的服务器API
        let receiptString = receiptData.base64EncodedString(options: [])
        
        // 验证编码结果
        guard !receiptString.isEmpty,
              Data(base64Encoded: receiptString) != nil else {
            DDLogError("[\(TAG)] ❌ 收据base64编码失败")
            completion(false)
            return
        }
        
        #if DEBUG
        let previewLength = min(30, receiptString.count)
        let preview = String(receiptString.prefix(previewLength))
        DDLogInfo("[\(TAG)] 📜 收据数据预览: \(preview)...")
        #else
        let previewLength = min(10, receiptString.count)
        let preview = String(receiptString.prefix(previewLength))
        DDLogInfo("[\(TAG)] 📜 收据数据预览: \(preview)...")
        #endif
       

        
        // TODO: 后面替换成调用服务器API校验
        // 构建请求参数
        let parameters: [String: Any] = [
            "receipt-data": receiptString,
            "password": "从App Store Connect获取", // 从App Store Connect获取
            "exclude-old-transactions": true
        ]
        
        DDLogInfo("[\(TAG)] 📋 验证参数:")
        DDLogInfo("[\(TAG)]   - 收据数据: \(receiptData.count) 字节")
        DDLogInfo("[\(TAG)]   - Base64长度: \(receiptString.count)")
        DDLogInfo("[\(TAG)]   - 交易ID: \(transaction.transactionIdentifier ?? "无")")
        DDLogInfo("[\(TAG)] 📍 从交易收据transactionIdentifier: \(transaction.transactionIdentifier ?? "")")
            
        validateWithAppleServer(parameters: parameters, isProduction: false, completion: completion)
    }
    
    /// 向Apple服务器发送验证请求
    private func validateWithAppleServer(parameters: [String: Any], isProduction: Bool, completion: @escaping (Bool) -> Void) {
        // 优先使用生产环境
        validateWithSpecificServer(parameters: parameters, isProduction: true) { [weak self] success, status in
            guard let self = self else {
                DDLogError("[\("SubscriptionManager")] ❌ 已释放,无法处理请求")
                completion(false)
                return
            }
            
            // 如果生产环境验证成功,直接返回
            if success {
                completion(true)
                return
            }
            
            // 如果返回状态码 21007(沙盒收据发送到生产环境),则尝试沙盒环境
            if status == 21007 {
                DDLogInfo("[\(self.TAG)] 🔄 检测到沙盒收据,切换到沙盒环境验证")
                self.validateWithSpecificServer(parameters: parameters, isProduction: false) { success, _ in
                    completion(success)
                }
            } else {
                // 其他错误直接返回失败
                completion(false)
            }
        }
    }
    

    /// 向指定的Apple服务器发送验证请求
    private func validateWithSpecificServer(parameters: [String: Any], isProduction: Bool, completion: @escaping (Bool, Int?) -> Void) {
        let urlString = isProduction ?
            "https://buy.itunes.apple.com/verifyReceipt" :
            "https://sandbox.itunes.apple.com/verifyReceipt"
        
        guard let verifyURL = URL(string: urlString) else {
            DDLogError("[\(TAG)] ❌ 无效的验证URL: \(urlString)")
            completion(false, nil)
            return
        }
        
        var request = URLRequest(url: verifyURL)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.timeoutInterval = 15.0 // 设置超时时间
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
        } catch {
            DDLogError("[\(TAG)] ❌ 请求参数序列化失败: \(error)")
            completion(false, nil)
            return
        }
        
        let environment = isProduction ? "生产环境" : "沙盒环境"
        DDLogInfo("[\(TAG)] 🌐 向Apple \(environment) 发送验证请求...")
        
        URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard let self = self else {
                DDLogError("[\("SubscriptionManager")] ❌ 已释放,无法处理请求")
                completion(false, nil)
                return
            }
            
            // 检查网络错误
            if let error = error {
                DDLogError("[\(self.TAG)] ❌ \(environment) 网络请求失败: \(error.localizedDescription)")
                completion(false, nil)
                return
            }
            
            // 检查HTTP响应
            if let httpResponse = response as? HTTPURLResponse {
                DDLogInfo("[\(self.TAG)] 📡 \(environment) HTTP状态码: \(httpResponse.statusCode)")
                if httpResponse.statusCode != 200 {
                    completion(false, nil)
                    return
                }
            }
            
            // 解析响应数据
            guard let data = data else {
                DDLogError("[\(self.TAG)] ❌ \(environment) 响应数据为空")
                completion(false, nil)
                return
            }
            
            do {
                if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
                    self.handleAppleVerificationResponseWithStatus(json, environment: environment, completion: completion)
                } else {
                    DDLogError("[\(self.TAG)] ❌ \(environment) 响应格式无效")
                    completion(false, nil)
                }
            } catch {
                DDLogError("[\(self.TAG)] ❌ \(environment) 响应解析失败: \(error)")
                completion(false, nil)
            }
        }.resume()
    }

    /// 处理Apple验证响应(带状态码返回)
    private func handleAppleVerificationResponseWithStatus(_ json: [String: Any], environment: String, completion: @escaping (Bool, Int?) -> Void) {
        guard let status = json["status"] as? Int else {
            DDLogError("[\(TAG)] ❌ \(environment) 响应中没有status字段")
            completion(false, nil)
            return
        }
        
        DDLogInfo("[\(TAG)] 📊 \(environment) 验证状态码: \(status)")
        
        switch status {
        case 0:
            DDLogInfo("[\(TAG)] ✅ \(environment) 收据验证成功")
            
            // 可选:解析详细的收据信息
            if let receipt = json["receipt"] as? [String: Any] {
                logReceiptDetails(receipt, environment: environment)
            }
            setPurchased(purchased: true)
            completion(true, status)
            
        case 21007:
            DDLogWarn("[\(TAG)] ⚠️ \(environment) 这是沙盒收据但发送到了生产环境")
            completion(false, status)
            
        case 21008:
            DDLogWarn("[\(TAG)] ⚠️ \(environment) 这是生产收据但发送到了沙盒环境")
            completion(false, status)
            
        case 21000:
            DDLogError("[\(TAG)] ❌ \(environment) App Store无法读取提供的JSON对象")
            completion(false, status)
            
        case 21002:
            DDLogError("[\(TAG)] ❌ \(environment) receipt-data属性中的数据格式错误或丢失")
            completion(false, status)
            
        case 21003:
            DDLogError("[\(TAG)] ❌ \(environment) 收据无法验证")
            completion(false, status)
            
        case 21004:
            DDLogError("[\(TAG)] ❌ \(environment) 提供的共享密钥与账户文件中的共享密钥不匹配")
            completion(false, status)
            
        case 21005:
            DDLogError("[\(TAG)] ❌ \(environment) 收据服务器暂时无法提供收据")
            completion(false, status)
            
        case 21006:
            DDLogError("[\(TAG)] ❌ \(environment) 此收据有效但订阅已过期")
            completion(false, status)
            
        case 21010:
            DDLogError("[\(TAG)] ❌ \(environment) 此收据无法验证")
            completion(false, status)
            
        default:
            DDLogError("[\(TAG)] ❌ \(environment) 未知验证状态码: \(status)")
            completion(false, status)
        }
    }
    
    /// 记录收据详细信息(用于调试)
    private func logReceiptDetails(_ receipt: [String: Any], environment: String) {
        DDLogInfo("[\(TAG)] 📋 \(environment) 收据详细信息:")
        
        if let bundleId = receipt["bundle_id"] as? String {
            DDLogInfo("[\(TAG)]   - Bundle ID: \(bundleId)")
        }
        
        if let applicationVersion = receipt["application_version"] as? String {
            DDLogInfo("[\(TAG)]   - 应用版本: \(applicationVersion)")
        }
        
        if let receiptType = receipt["receipt_type"] as? String {
            DDLogInfo("[\(TAG)]   - 收据类型: \(receiptType)")
        }
        
        if let inApp = receipt["in_app"] as? [[String: Any]] {
            DDLogInfo("[\(TAG)]   - 内购交易数量: \(inApp.count)")
            
            // 只记录最近的几个交易
            for (index, transaction) in inApp.prefix(3).enumerated() {
                if let productId = transaction["product_id"] as? String,
                   let transactionId = transaction["transaction_id"] as? String {
                    DDLogInfo("[\(TAG)]     交易\(index + 1): \(productId) (\(transactionId))")
                }
            }
        }
    }
  • password 从App Store Connect获取
截屏2025-07-22 14.58.19.png
  • 支付收据凭证校验处理
截屏2025-07-22 14.32.58.png

截屏2025-07-22 14.33.09.png
  • 服务端+客户端开发相关文档

StoreKit框架文档
https://developer.apple.com/documentation/storekit
App Store服务器通知
https://developer.apple.com/documentation/appstoreservernotifications
App Store服务器API
https://developer.apple.com/documentation/appstoreserverapi
收据字段参考
https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html

订阅产品配置: https://www.jianshu.com/p/ba4ce240a085
银行账户添加: https://www.jianshu.com/p/b5b2e89210b5
Swift代码: https://www.jianshu.com/p/c26f62b1ac39

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容