iOS内购显示成功但未向用户扣费问题求助
问题描述
应用数小时前上架App Store,内购流程显示成功提示,但未要求用户提供支付信息也未扣费。具体流程如下:
- 用户点击商品
- Apple StoreKit显示包含价格及购买选项的提示
- Apple要求用户输入登录信息以完成购买
- Apple显示购买成功提示
沙箱模式下流程一致,但会提示“因处于沙箱模式不会扣费”;生产环境无扣费提示且未实际扣费,内购商品已通过App Store Connect审核。
关联IAP代码如下:
import Foundation import StoreKit public struct InAppPurchasesCallbacks { var onFetchCompleted: (([SKProduct]) -> Void)? var onProductsNotFound: (([String]?) -> Void)? var onPurchaseSucceeded: ((SKProduct, SKPaymentTransaction) -> Void)? var onPurchaseFailed: ((SKProduct, Error?) -> Void)? var onRestoreCompleted: (([SKPaymentTransaction]) -> Void)? var onRestoreFailed: ((Error?) -> Void)? public init(onFetchCompleted: (([SKProduct]) -> Void)? = nil, onProductsNotFound: (([String]?) -> Void)? = nil, onPurchaseSucceeded: ((SKProduct, SKPaymentTransaction) -> Void)? = nil, onPurchaseFailed: ((SKProduct, Error?) -> Void)? = nil, onRestoreCompleted: (([SKPaymentTransaction]) -> Void)? = nil, onRestoreFailed: ( (Error?) -> Void)? = nil) { self.onFetchCompleted = onFetchCompleted self.onProductsNotFound = onProductsNotFound self.onPurchaseSucceeded = onPurchaseSucceeded self.onPurchaseFailed = onPurchaseFailed self.onRestoreCompleted = onRestoreCompleted self.onRestoreFailed = onRestoreFailed } } public class InAppPurchaseWrapper: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver { private var products = [SKProduct]() private var callbacks: InAppPurchasesCallbacks private var productIdentifiers: Set<String> public init(productIdentifiers: Set<String>, callbacks: InAppPurchasesCallbacks) { self.productIdentifiers = productIdentifiers self.callbacks = callbacks super.init() SKPaymentQueue.default().add(self) fetchProductInfo() } private func fetchProductInfo() { let request = SKProductsRequest(productIdentifiers: productIdentifiers) request.delegate = self request.start() } public func purchaseProduct(_ product: SKProduct) { let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) } public func restorePurchases() { SKPaymentQueue.default().restoreCompletedTransactions() } private func validateTransaction(transaction: SKPaymentTransaction, product: SKProduct) { // some backend server verification calls, removed for security here. } } // MARK: - Delegate methods extension InAppPurchaseWrapper { public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { products = response.products let unrecognized = response.invalidProductIdentifiers if products.count == 0 { if unrecognized.count > 0 { callbacks.onProductsNotFound?(unrecognized) } else { callbacks.onProductsNotFound?(nil) } } else { callbacks.onFetchCompleted?(products) if unrecognized.count > 0{ callbacks.onProductsNotFound?(unrecognized) } } } public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { case .purchasing: break case .purchased: let product = products.first { $0.productIdentifier == transaction.payment.productIdentifier } if let p = product { validateTransaction(transaction: transaction, product: p) callbacks.onPurchaseSucceeded?(p, transaction) } else { callbacks.onProductsNotFound?(nil) queue.finishTransaction(transaction) } case .failed: let product = products.first { $0.productIdentifier == transaction.payment.productIdentifier } if let p = product, let e = transaction.error { callbacks.onPurchaseFailed?(p, e) } else { callbacks.onProductsNotFound?(nil) } queue.finishTransaction(transaction) case .restored: let product = products.first { $0.productIdentifier == transaction.original?.payment.productIdentifier } if let p = product { callbacks.onPurchaseSucceeded?(p, transaction) } else { callbacks.onProductsNotFound?(nil) } queue.finishTransaction(transaction) case .deferred: break @unknown default: break } } } public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { callbacks.onRestoreFailed?(error) } public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { callbacks.onRestoreCompleted?(queue.transactions) } }
排查与修复方案
1. 代码未完成交易(核心问题)
在paymentQueue(_:updatedTransactions:)方法的.purchased分支中,当找到对应产品并调用验证、成功回调后,未调用queue.finishTransaction(transaction)。这会导致交易停留在SKPaymentQueue中,Apple不会触发实际扣费流程,且下次启动应用会重复触发该交易的purchased状态。
修复代码:
case .purchased: let product = products.first { $0.productIdentifier == transaction.payment.productIdentifier } if let p = product { validateTransaction(transaction: transaction, product: p) callbacks.onPurchaseSucceeded?(p, transaction) // 新增:完成交易,触发Apple扣费流程 queue.finishTransaction(transaction) } else { callbacks.onProductsNotFound?(nil) queue.finishTransaction(transaction) }
2. 测试账号或账号已拥有商品
- 若使用沙箱测试账号,即使在生产环境下载应用,购买时也不会扣费(Apple会识别沙箱账号),需使用全新的非沙箱Apple ID测试。
- 用户账号可能已通过其他渠道购买过该内购商品,此时恢复购买会直接显示成功,无需再次扣费,可检查用户账号的已购项目确认。
3. App Store生产环境同步延迟
刚上架的应用,内购配置可能需要1-24小时同步到Apple生产服务器。可等待一段时间后再测试,或在App Store Connect中确认:
- 内购商品状态为“已批准”
- 应用版本与内购商品的关联正确
4. 内购商品类型配置错误
若内购商品被误配置为免费商品,Apple会跳过支付步骤直接完成交易。需在App Store Connect中检查该商品的定价层级,确认设置为付费层级。
额外建议
- 务必保留后端交易凭证验证逻辑:即使前端显示成功,也要通过Apple官方接口确认交易真实性,避免恶意绕过支付的情况。
- 生产环境测试时,使用真实用户账号(非沙箱)验证完整扣费流程。
内容的提问来源于stack exchange,提问作者A Mir
相关产品推荐
相关产品推荐

