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

非消耗型In App Purchase按钮状态持久化问题及配置方案咨询

Hey there! Let's break down your problem and fix it step by step.

The Core Issue

You're currently only enabling the button right after a successful purchase, but you're not persisting that purchase state or verifying it when the app restarts. Non-consumable IAPs are tied to the user's account (Apple ID/Google Account), so every time your app launches, you need to check if the user has already purchased the feature—not just rely on the temporary in-memory button state.

Step-by-Step Fix

  1. Persist the purchase state locally: Use a lightweight storage (like UserDefaults on iOS or SharedPreferences on Android) to cache the purchase status for quick access.
  2. Validate purchases on app launch: Every time the app starts, query the platform's IAP service (StoreKit for iOS, BillingClient for Android) to confirm the user's purchase history, then update the button state accordingly.
  3. Add a "Restore Purchases" option: Let users recover their purchases if they switch devices or reinstall the app.

Full Non-Consumable IAP Implementation

iOS (Swift)

First, create a singleton IAP manager to handle all purchase logic:

import StoreKit

class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    static let shared = IAPManager()
    private let premiumProductID = "com.yourapp.premium.feature" // Match your App Store Connect ID
    private var premiumProduct: SKProduct?
    
    override init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }
    
    // Fetch product details from App Store
    func fetchProducts() {
        let request = SKProductsRequest(productIdentifiers: [premiumProductID])
        request.delegate = self
        request.start()
    }
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        premiumProduct = response.products.first
        response.invalidProductIdentifiers.forEach { print("Invalid product ID: \($0)") }
    }
    
    // Initiate purchase
    func purchasePremiumFeature() {
        guard let product = premiumProduct, SKPaymentQueue.canMakePayments() else {
            print("Purchase unavailable")
            return
        }
        SKPaymentQueue.default().add(SKPayment(product: product))
    }
    
    // Restore completed purchases
    func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
    
    // Handle transaction updates
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased, .restored:
                handleSuccessfulPurchase()
                queue.finishTransaction(transaction)
            case .failed:
                print("Purchase failed: \(transaction.error?.localizedDescription ?? "Unknown error")")
                queue.finishTransaction(transaction)
            case .deferred, .purchasing: break
            @unknown default: queue.finishTransaction(transaction)
            }
        }
    }
    
    // Update purchase state and notify UI
    private func handleSuccessfulPurchase() {
        UserDefaults.standard.set(true, forKey: "hasPurchasedPremium")
        NotificationCenter.default.post(name: NSNotification.Name("PremiumPurchased"), object: nil)
    }
    
    // Check if user owns the premium feature
    func hasPurchasedPremium() -> Bool {
        let localStatus = UserDefaults.standard.bool(forKey: "hasPurchasedPremium")
        // Optional: Add server-side validation here to prevent tampering
        return localStatus
    }
}

Then use it in your view controller:

import UIKit

class MainViewController: UIViewController {
    @IBOutlet weak var premiumButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, selector: #selector(updateButtonState), name: NSNotification.Name("PremiumPurchased"), object: nil)
        
        IAPManager.shared.fetchProducts()
        updateButtonState() // Set initial state on app launch
    }
    
    @objc private func updateButtonState() {
        let hasPurchased = IAPManager.shared.hasPurchasedPremium()
        premiumButton.isEnabled = !hasPurchased
        premiumButton.setTitle(hasPurchased ? "已解锁" : "解锁高级功能", for: .normal)
    }
    
    @IBAction func purchaseTapped(_ sender: UIButton) {
        IAPManager.shared.purchasePremiumFeature()
    }
    
    @IBAction func restorePurchasesTapped(_ sender: UIButton) {
        IAPManager.shared.restorePurchases()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

Android (Kotlin)

Add the BillingClient dependency to your module-level build.gradle:

dependencies {
    implementation "com.android.billingclient:billing:6.0.1"
}

Create an IAP manager class:

import android.app.Activity
import android.content.Context
import com.android.billingclient.api.*

class IAPManager(private val context: Context) : PurchasesUpdatedListener {
    private lateinit var billingClient: BillingClient
    private val PREMIUM_SKU = "com.yourapp.premium.feature" // Match Google Play Console ID
    
    init {
        setupBillingClient()
    }
    
    private fun setupBillingClient() {
        billingClient = BillingClient.newBuilder(context)
            .enablePendingPurchases()
            .setListener(this)
            .build()
        connectToBillingService()
    }
    
    private fun connectToBillingService() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(result: BillingResult) {
                if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                    queryPurchases()
                }
            }
            
            override fun onBillingServiceDisconnected() {
                connectToBillingService()
            }
        })
    }
    
    // Query existing purchases
    fun queryPurchases() {
        billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(BillingClient.SkuType.INAPP).build()) { result, purchases ->
            if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                val hasPurchased = purchases.any { it.skus.contains(PREMIUM_SKU) }
                val prefs = context.getSharedPreferences("IAPPrefs", Context.MODE_PRIVATE)
                prefs.edit().putBoolean("hasPurchasedPremium", hasPurchased).apply()
                
                (context as? Activity)?.runOnUiThread {
                    (context as? MainActivity)?.updateButtonState(hasPurchased)
                }
            }
        }
    }
    
    // Launch purchase flow
    fun launchPurchaseFlow(activity: Activity) {
        val params = QueryProductDetailsParams.newBuilder()
            .setProductList(listOf(
                QueryProductDetailsParams.Product.newBuilder()
                    .setProductId(PREMIUM_SKU)
                    .setProductType(BillingClient.SkuType.INAPP)
                    .build()
            ))
            .build()
        
        billingClient.queryProductDetailsAsync(params) { result, details ->
            if (result.responseCode == BillingClient.BillingResponseCode.OK && details.isNotEmpty()) {
                val flowParams = BillingFlowParams.newBuilder()
                    .setProductDetailsParamsList(listOf(
                        BillingFlowParams.ProductDetailsParams.newBuilder()
                            .setProductDetails(details.first())
                            .build()
                    ))
                    .build()
                billingClient.launchBillingFlow(activity, flowParams)
            }
        }
    }
    
    // Restore purchases (same as querying purchases)
    fun restorePurchases() {
        queryPurchases()
    }
    
    override fun onPurchasesUpdated(result: BillingResult, purchases: MutableList<Purchase>?) {
        when (result.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.forEach { purchase ->
                    if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
                        val acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
                            .setPurchaseToken(purchase.purchaseToken)
                            .build()
                        billingClient.acknowledgePurchase(acknowledgeParams) {
                            if (it.responseCode == BillingClient.BillingResponseCode.OK) {
                                queryPurchases()
                            }
                        }
                    }
                }
            }
            BillingClient.BillingResponseCode.USER_CANCELED -> {}
            else -> print("Purchase failed: ${result.debugMessage}")
        }
    }
    
    fun hasPurchasedPremium(): Boolean {
        val prefs = context.getSharedPreferences("IAPPrefs", Context.MODE_PRIVATE)
        return prefs.getBoolean("hasPurchasedPremium", false)
    }
    
    fun destroy() {
        if (billingClient.isReady) billingClient.endConnection()
    }
}

Use it in your activity:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

class MainActivity : AppCompatActivity() {
    private lateinit var iapManager: IAPManager
    private lateinit var premiumButton: Button
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        premiumButton = findViewById(R.id.premium_button)
        iapManager = IAPManager(this)
        
        updateButtonState(iapManager.hasPurchasedPremium())
        
        premiumButton.setOnClickListener {
            iapManager.launchPurchaseFlow(this)
        }
        
        findViewById<Button>(R.id.restore_button).setOnClickListener {
            iapManager.restorePurchases()
        }
    }
    
    fun updateButtonState(hasPurchased: Boolean) {
        premiumButton.isEnabled = !hasPurchased
        premiumButton.text = if (hasPurchased) "已解锁" else "解锁高级功能"
    }
    
    override fun onDestroy() {
        super.onDestroy()
        iapManager.destroy()
    }
}

Critical Notes

  • Server-Side Validation: Local storage can be tampered with. For production apps, send the purchase receipt to your backend server and validate it against Apple/Google's official APIs to ensure the purchase is legitimate.
  • Platform Configuration: Make sure your non-consumable product is correctly set up in App Store Connect (iOS) or Google Play Console, with the exact product ID used in your code.
  • Error Handling: Expand the error handling logic to show user-friendly messages for common issues (e.g., IAP disabled, network errors).

内容的提问来源于stack exchange,提问作者Suresh Srinivasalu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:50:45