非消耗型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
- Persist the purchase state locally: Use a lightweight storage (like
UserDefaultson iOS orSharedPreferenceson Android) to cache the purchase status for quick access. - 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.
- 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

