Implementing In-App Purchases
1. Setting Up Google Play Billing
Example: Basic Setup in AndroidManifest.xml
<uses-permission android:name="com.android.vending.BILLING" />
| Permission | Description | Required |
|---|---|---|
| com.android.vending.BILLING | Allows app to communicate with Google Play Billing service | Yes |
Note: Ensure your app is signed with a release key for testing in production.
Setup Steps
- Add the billing permission to
AndroidManifest.xml - Include the Play Billing library in your
build.gradle - Initialize the BillingClient in your activity or fragment
- Connect to Google Play Billing service
2. Connecting to BillingClient
Example: Initialize BillingClient
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
class BillingManager(private val context: Context) {
private lateinit var billingClient: BillingClient
fun initializeBillingClient() {
billingClient = BillingClient.newBuilder(context)
.setListener { billingResult, purchases -> handlePurchases(purchases) }
.enablePendingPurchases()
.build()
}
fun startConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponse.OK) {
// The BillingClient is ready. You can query purchases here.
}
}
override fun onBillingServiceDisconnected() {
// Try to reconnect to the billing service.
}
})
}
private fun handlePurchases(purchases: List<Purchase>?) {
// Handle purchases here
}
}
| Method | Description | Return Type |
|---|---|---|
| BillingClient.newBuilder(Context) | Creates a new BillingClient.Builder instance. | BillingClient.Builder |
| BillingClient.Builder.setListener() | Sets the listener for purchase updates. | BillingClient.Builder |
| BillingClient.Builder.enablePendingPurchases() | Enables pending purchases for the app. | BillingClient.Builder |
| BillingClient.build() | Builds the BillingClient instance. | BillingClient |
| BillingClient.startConnection() | Initiates the connection to Google Play Billing service. | void |
Note: Ensure that the app has internet access and Google Play services are available before attempting to connect.
Warning: The
BillingClient must be initialized and connected before querying product details or making purchases.3. Querying Product Details
Example: Querying Product Details
val productIds = listOf("premium_upgrade", "subscription_monthly")
val queryProductDetails = QueryProductDetailsParams
.Builder()
.setProductIds(productIds)
.build()
billingClient.queryProductDetailsAsync(queryProductDetails) { billingResult, productDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
for (productDetails in productDetailsList) {
Log.d("Product", "Title: " + productDetails.title)
Log.d("Product", "Price: " + productDetails.price)
}
} else {
Log.e("Billing", "Error: " + billingResult.responseCode)
}
}
| Property | Description | Values |
|---|---|---|
| productId | Unique identifier for the product | string |
| title | Display name of the product | string |
| description | Longer description of the product | string |
| price | Formatted price string (e.g., "$4.99") | string |
| sku | SKU identifier for the product | string |
| productType | Type of product (e.g., IN_APP, SUBS) |
ProductType |
Note: Ensure that all product IDs are registered in the Google Play Console before querying.
Warning: Product details may vary based on the user's region or currency settings.
4. Launching Purchase Flow
| Property | Description | Values |
|---|---|---|
| skuDetails | Product details retrieved from Google Play Billing | SkuDetails |
| activity | Current activity context to launch purchase flow | Activity |
| billingClient | Instance of BillingClient initialized with startConnection() |
BillingClient |
Example: Launching Purchase Flow
val billingClient = BillingClient.newBuilder(context)
.setListener { _, _ -> }
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponse.OK) {
val skuDetails = SkuDetails("inapp:product1")
val purchaseFlow = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build()
billingClient.launchBillingFlow(activity, purchaseFlow)
}
}
override fun onBillingServiceDisconnected() { }
})
Note: Ensure
BillingClient is connected before launching the purchase flow.
Warning: The
launchBillingFlow method must be called from the main thread.
5. Processing Purchases
| Property | Description | Values |
|---|---|---|
| purchase | Represents a purchase object returned by the billing client | Purchase |
| purchaseToken | Unique identifier for the purchase transaction | String |
| sku | Product identifier (SKU) of the purchased item | String |
| purchaseState | Current state of the purchase (e.g., purchased, refunded) | PurchaseState |
Example: Handling Purchase
class PurchaseHandler : PurchasesUpdatedListener {
override fun onPurchasesUpdated(
responseCode: Int,
purchases: List<Purchase>?
) {
if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
for (purchase in purchases) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
// Process the purchase
val sku = purchase.sku
val token = purchase.purchaseToken
}
}
}
}
}
Note: Always verify the purchase on your backend before granting access to in-app content.
| State | Description | Usage |
|---|---|---|
| PURCHASED | User has successfully purchased the item | Grant access to content |
| PENDING | Transaction is pending confirmation from Google Play | Wait for final state before granting access |
| UNSPECIFIED_STATE | Unknown or unexpected state | Log and handle appropriately |
Example: Verifying Purchase Token
fun verifyPurchase(token: String, sku: String): Boolean {
// Send token and SKU to your server for verification
val verificationResult = sendToBackend(token, sku)
return verificationResult.isValid
}
6. Acknowledging Purchases
After a purchase is successfully processed, it's crucial to acknowledge the purchase on the server to prevent duplicate transactions and ensure proper handling.
| Method | Description | Notes |
|---|---|---|
| acknowledgePurchase | Acknowledges a purchase for a specific product ID. | Must be called within 30 days of purchase to avoid automatic refunds. |
Example: Acknowledging a Purchase
val purchase = purchasesResponse.purchasesList.firstOrNull()
if (purchase != null) {
val acknowledgePurchaseRequest = AcknowledgePurchaseRequest(
purchaseToken = purchase.purchaseToken,
productId = purchase.productId
)
billingClient.acknowledgePurchase(acknowledgePurchaseRequest) { response ->
if (response.responseCode == BillingClient.BillingResponseCode.OK) {
// Purchase acknowledged successfully
} else {
// Handle error
}
}
}
Note: The
acknowledgePurchase method must be called for consumable and non-consumable in-app purchases to confirm that the purchase has been processed correctly.
7. Consuming Consumables
Consumable in-app purchases are items that can be purchased multiple times. They are typically used for virtual currency, game items, or other non-permanent goods.
| Property | Description | Notes |
|---|---|---|
| productId | Unique identifier for the consumable product | Must match the product ID registered in the store |
| purchaseToken | Unique token identifying the purchase | Used to consume the purchase on the server or client |
| purchaseTime | Timestamp of the purchase | Used for tracking and analytics |
Example: Consuming a Consumable
val purchase = purchases.firstOrNull { it.productId == "consumable_item" }
if (purchase != null) {
val purchaseToken = purchase.purchaseToken
// Send to backend or consume directly on client
consumePurchase(purchaseToken)
}
Note: After consuming a consumable, it can be purchased again.
Warning: Always verify purchases on the server to prevent fraud.
Consuming Consumables - Key Points
- Consumables can be purchased multiple times
- Each purchase generates a unique
purchaseToken - Consumables must be consumed to allow re-purchase
- Server-side verification is highly recommended for security
8. Handling Subscriptions
| Property | Description | Values | Notes |
|---|---|---|---|
| sku | Unique identifier for the subscription | "monthly_subscription" |
Must match SKU in Google Play Console |
| type | Product type | "subs" |
Indicates subscription product |
| price | Subscription price | "$4.99" |
Formatted as per locale |
| billingPeriod | Subscription billing cycle | "P1M" |
ISO 8601 duration format |
Example: Querying Subscription Details
val query = QuerySkuDetailsParams
.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.setSkusList(listOf("monthly_subscription"))
.build()
billingClient.querySkuDetailsAsync(query) { _, detailsList ->
detailsList?.forEach { detail ->
Log.d("Subscription", "Title: ${detail.title}, Price: ${detail.price}")
}
}
Example: Purchasing a Subscription
val purchaseParams = BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetails)
.build()
billingClient.launchBillingFlow(activity, purchaseParams)
Note: Subscriptions must be managed through Google Play Billing Library v5+ for automatic renewal handling.
Warning: Ensure proper handling of subscription state changes to avoid revenue loss or user confusion.
| State | Description | Action Required |
|---|---|---|
ACTIVE |
User has an active subscription | Grant access to premium content |
PENDING |
Subscription is pending confirmation | Wait for state change before granting access |
EXPIRED |
Subscription has expired | Revoke access and prompt renewal |
CANCELLED |
User cancelled subscription | Revoke access immediately |
9. Verifying Purchases
Verifying purchases is crucial for ensuring the integrity of in-app transactions and preventing fraud. Android provides mechanisms to verify purchase data through the Google Play Billing Library.
| Component | Description | Notes |
|---|---|---|
| Purchase | Represents a user's purchase of an in-app product. | Includes product ID, purchase token, and purchase time. |
| PurchaseState | Indicates the state of a purchase (e.g., purchased, refunded). | Values: PURCHASED, UNSPECIFIED_STATE. |
| Signature | Used to verify the authenticity of purchase data. | Generated using the app's private key and the purchase data. |
Example: Verifying Purchase Data
import com.android.billingclient.api.Purchase
import java.security.Signature
import java.security.PublicKey
fun verifyPurchase(purchase: Purchase, publicKey: PublicKey): Boolean {
val signature = Signature.getInstance("SHA256withRSA")
signature.initVerify(publicKey)
signature.update(purchase.originalJson.toByteArray())
return signature.verify(Base64.decode(purchase.signature, Base64.NO_WRAP))
}
Note: Always verify purchases on a secure backend server to prevent tampering.
Warning: Do not rely solely on client-side verification for security-critical operations.
| Verification Method | Security Level | Use Case |
|---|---|---|
verifyPurchase() |
Client-side | Quick checks in app UI |
| Server-side validation | High | Secure transaction handling |
10. Restoring Purchases
Restoring purchases allows users to recover previously bought in-app items, especially useful after reinstalling an app or switching devices.
| Method | Description | Notes |
|---|---|---|
| restorePurchases | Initiates the purchase restoration process for a user's previous purchases. | Must be called after connecting to the billing client and ensuring the user is authenticated. |
Example: Restoring Purchases
val billingClient = BillingClient.newBuilder(context)
.setListener { _, _ -> }
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponse.OK) {
val restoreRequest = BillingRequest.newBuilder()
.setProductType(BillingClient.ProductType.IN_APP)
.build()
billingClient.acknowledgePurchase(
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken("purchase_token")
.build()
)
}
}
override fun onBillingServiceDisconnected() {
// Handle disconnection
}
})
Note: The restore process requires the user to be signed into their Google account associated with the purchase.
Warning: Ensure that you handle the restore flow securely to prevent unauthorized access or duplication of purchases.