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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 16:30:35