跨会话/设备处理Google In-App Billing API消耗型应用内购买的技术问询
Hey there, let’s break down how to handle consumable in-app purchases using Google’s In-App Billing API—this is something I’ve worked through multiple times, so I’ll share the core workflow, key code snippets, and pitfalls to watch out for.
First, let’s ground ourselves in what consumables are (since it’s foundational):
Consumable products are items that can be purchased multiple times, usually providing temporary benefits—like extra lives in a game, one-time premium features, or in-app currency that gets spent. Unlike non-consumables, they need to be "consumed" after purchase so the user can buy them again.
Step 1: Set Up the Billing Client
Start by initializing the BillingClient in your Android app. Make sure you’re using the latest version of the billing library. Here’s a basic Kotlin setup:
private lateinit var billingClient: BillingClient fun setupBillingClient(context: Context, activity: Activity) { billingClient = BillingClient.newBuilder(context) .setListener(purchaseUpdateListener) .enablePendingPurchases() // Required for handling pending transactions .build() billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(result: BillingResult) { if (result.responseCode == BillingClient.BillingResponseCode.OK) { // Billing client is ready—query your consumable products next queryConsumableProducts() } } override fun onBillingServiceDisconnected() { // Try restarting the connection on the next interaction with Google Play } }) }
Step 2: Query Consumable Products
Before letting users purchase, fetch your consumable SKUs (set up in the Play Console) using querySkuDetailsAsync:
private fun queryConsumableProducts() { val skuList = listOf("extra_lives", "inapp_gold_coins") // Replace with your actual SKUs val params = SkuDetailsParams.newBuilder() .setSkusList(skuList) .setType(BillingClient.SkuType.INAPP) .build() billingClient.querySkuDetailsAsync(params) { result, skuDetailsList -> if (result.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null) { // Display these products to users (e.g., in a RecyclerView with buy buttons) } } }
Step 3: Launch the Purchase Flow
When a user taps to buy a consumable, trigger the purchase flow with the selected product’s SkuDetails:
fun launchPurchaseFlow(activity: Activity, skuDetails: SkuDetails) { val billingFlowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails) .build() val result = billingClient.launchBillingFlow(activity, billingFlowParams) // Check result.responseCode to handle immediate errors (e.g., user not logged into Play) }
Step 4: Handle Purchases & Consumption
This is the most critical part for consumables. The PurchasesUpdatedListener will notify you when a purchase completes—you must consume the purchase so the user can buy it again.
First, define the listener:
private val purchaseUpdateListener = PurchasesUpdatedListener { result, purchases -> if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { for (purchase in purchases) { handleValidPurchase(purchase) } } else if (result.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { // User canceled the purchase—no action needed } else { // Handle other errors (e.g., network issues, insufficient funds) } }
Then, implement the logic to verify, grant benefits, and consume the purchase:
private fun handleValidPurchase(purchase: Purchase) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { // 1. Verify the purchase signature (critical for security) if (verifyPurchaseSignature(purchase.originalJson, purchase.signature)) { // 2. Grant the user the consumable benefit (e.g., add 5 lives to their account) grantConsumableBenefit(purchase.sku) // 3. Consume the purchase so it can be bought again val consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() billingClient.consumeAsync(consumeParams) { consumeResult, _ -> if (consumeResult.responseCode == BillingClient.BillingResponseCode.OK) { // Consumption successful—log this or update the UI } else { // Handle consumption failure (retry later if needed) } } } else { // Invalid signature—don't grant benefits (fraudulent purchase attempt) } } else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) { // Purchase is pending (e.g., user needs to complete carrier billing) // Wait for a subsequent update before granting benefits } } // Helper: Verify purchase signature using your Play Console public key private fun verifyPurchaseSignature(originalJson: String, signature: String): Boolean { // Implement signature verification logic here—never skip this! // Use the public key from your app's Play Console page return true // Replace with actual verification } // Helper: Grant the consumable benefit to the user private fun grantConsumableBenefit(sku: String) { when(sku) { "extra_lives" -> { /* Add lives to user's profile */ } "inapp_gold_coins" -> { /* Add coins to user's balance */ } } }
Key Pitfalls to Avoid
- Never skip signature verification: This stops fraudulent purchases from being processed. Use your app’s public key from the Play Console to validate the purchase.
- Consume only after granting benefits: If your app crashes after granting the benefit but before consuming, you can recover the purchase later by querying unconsumed purchases on app launch.
- Handle pending purchases: Some transactions (like carrier billing) might stay pending. Don’t grant benefits until the purchase is marked as
PURCHASED. - Query unconsumed purchases on launch: Use
queryPurchasesAsyncwhen your app starts to catch any purchases that were left unconsumed (e.g., app crashed mid-process):fun checkUnconsumedPurchases() { billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP) { result, purchases -> if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { for (purchase in purchases) { if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) { handleValidPurchase(purchase) } } } } }
That’s the full workflow! If you’re hitting specific edge cases (like handling retries for failed consumption), feel free to ask more details.
内容的提问来源于stack exchange,提问作者Mark

