1. Introduction
In this codelab you'll focus on creating a one-time product, integrating your app with Play Billing Library (PBL), and analyze reasons for purchase drop-offs.
Audience
This codelab is targeted for Android app developers who are using the Play Billing Library (PBL) or want to use PBL for monetizing their one-time products.
What you'll learn...
- How to create one-time products in the Google Play Console.
- How to integrate your app with PBL.
- How to process consumable and non-consumable one-time product purchases in PBL.
- How to analyze purchase drop-offs.
What you'll need...
- Access to the Google Play Console with a developer account. If you don't have a developer account, you need to create an account.
- A sample app for this codelab which you can download from GitHub.
- Android Studio.
2. Build the sample app
The sample app is designed to be a fully functional android app that has the complete source code which showcases the following aspects:
- Integration the app with PBL
- Fetch one-time products
- Launch purchase flows for the one-time products
- Purchase scenarios which lead to the following billing responses:
BILLING_UNAVAILABLE
USER_CANCELLED
OK
ITEM_ALREADY_OWNED
The following demo video shows how the sample app will look and behave after it's deployed and run.
Pre-requisites
Before you build and deploy the sample app, do the following:
- Create a Google Play Console developer account. If you already have a developer account, skip this step.
- Create a new app in the Play Console. When creating an app, you can specify any app name for the sample app.
- Install Android Studio.
Build
The objective of this build step is to generate a signed Android app bundle file of the sample app.
To generate the Android app bundle, do the following steps:
- Download the sample app from GitHub.
- Build the sample app. Before you build, change the package name of the sample app and then build. If you have packages of other apps in your Play Console, ensure the package name you provide for the sample app is unique.
Note: Building the sample app creates only an APK file that you can use for local testing. However, running the app doesn't fetch products and prices because the products haven't been configured in the Play Console which you will do further in this codelab. - Generate a signed Android app bundle.
The next step is to upload the Android app bundle to Google Play Console.
3. Create one-time product in Play Console
To create one-time products in the Google Play Console, you need to have an app in the Play Console. Create an app in the Play Console, and then upload the previously created signed app bundle.
Create an app
To create an app:
- Log-in to the Google Play Console, using your developer account.
- Click Create app. This opens the Create app page.
- Enter an app name, select the default language, and other app related details.
- Click Create app. This creates an app in the Google Play Console.
Now you can upload the signed app bundle of the sample app.
Upload the signed app bundle
- Upload the signed app bundle to Google Play's Console internal test track. Only after uploading, you can configure the monetization related features in the Play Console.
- Click Test and release > Testing > Internal release > Create new release.
- Enter a release name and upload the signed app bundle file.
- Click Next, and then click Save and publish.
Now, you can create your one-time products.
Create a one-time product
To create a one-time product:
- In the Google Play Console, from the left navigation menu, go to Monetize with Play > Products > One-time products.
- Click Create one-time product.
- Enter the following product details:
- Product ID: Enter a unique product ID. Enter
one_time_product_01
. - (Optional) Tags: Add relevant tags.
- Name: Enter a product name. For example,
Product name
. - Description: Enter a product description. For example,
Product description
. - (Optional) Add an Icon image: Upload an icon that represents your product.
- Product ID: Enter a unique product ID. Enter
- Click Next.
- Add a purchase option and configure its regional availability. A one-time product needs at least one purchase option, which defines how the entitlement is granted, its price, and regional availability. For this codelab, we will add the standard Buy option for the product.In the Purchase option section, enter the following details:
- Purchase Option ID: Enter a purchase option ID. For example,
buy
. - Purchase type: Select Buy.
- (Optional) Tags: Add tags specific to this purchase option.
- (Optional) Click Advanced options to configure the advanced options. For the purpose of this codelab, you can skip the advanced options configuration.
- Purchase Option ID: Enter a purchase option ID. For example,
- In the Availability and pricing section, click Set prices > Bulk edit pricing.
- Select the Country / region option. This selects all the regions.
- Click Continue. This opens a dialog to enter a price. Enter 10 USD and then click Apply.
- Click Save and then click Activate. This creates and activates the purchase option.
For the purpose of this codelab, create 3 additional one-time products with the following product IDs:
- consumable_product_01
- consumable_product_02
- consumable_product_03
The sample app is configured to use these product IDs. You can provide different product IDs, in which case, you will have to modify the sample app to use the product ID you have provided.
Open the sample app in Google Play Console, and navigate to Monetize with Play > Products > One-time products. Then click Create one-time product and repeat steps 3 to 9.
One-time product creation video
The following sample video shows the one-time product creation steps that are previously described.
4. Integrate with PBL
Now, we will see how to integrate your app with the Play Billing Library (PBL). This section describes the high level steps for integration and provides a code snippet for each of the steps. You can use these snippets as a guidance to implement your actual integration.
To integrate your app with PBL, do the following steps:
- Add the Play Billing Library dependency to the sample app.
dependencies { val billing_version = "8.0.0" implementation("com.android.billingclient:billing-ktx:$billing_version") }
- Initialize the BillingClient. The BillingClient is the client SDK that resides on your app and communicates with the Play Billing Library. The following code snippet shows how to initialize the billing client.
protected BillingClient createBillingClient() { return BillingClient.newBuilder(activity) .setListener(purchasesUpdatedListener) .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()) .enableAutoServiceReconnection() .build(); }
- Connect to Google Play.The following code snippet shows how to connect to Google Play.
public void startBillingConnection(ImmutableList<Product> productList) { Log.i(TAG, "Product list sent: " + productList); Log.i(TAG, "Starting connection"); billingClient.startConnection( new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResult) { if (billingResult.getResponseCode() == BillingResponseCode.OK) { // Query product details to get the product details list. queryProductDetails(productList); } else { // BillingClient.enableAutoServiceReconnection() will retry the connection on // transient errors automatically. // We don't need to retry on terminal errors (e.g., BILLING_UNAVAILABLE, // DEVELOPER_ERROR). Log.e(TAG, "Billing connection failed: " + billingResult.getDebugMessage()); Log.e(TAG, "Billing response code: " + billingResult.getResponseCode()); } } @Override public void onBillingServiceDisconnected() { Log.e(TAG, "Billing Service connection lost."); } }); }
- Fetch the one-time product details.After integrating your app with PBL, you must fetch the one-time product details into your app. The following code snippet shows how to fetch the one-time product details in your app.
Fetchingprivate void queryProductDetails(ImmutableList<Product> productList) { Log.i(TAG, "Querying products for: " + productList); QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(productList).build(); billingClient.queryProductDetailsAsync( queryProductDetailsParams, new ProductDetailsResponseListener() { @Override public void onProductDetailsResponse( BillingResult billingResult, QueryProductDetailsResult productDetailsResponse) { // check billingResult Log.i(TAG, "Billing result after querying: " + billingResult.getResponseCode()); // process returned productDetailsList Log.i( TAG, "Print unfetched products: " + productDetailsResponse.getUnfetchedProductList()); setupProductDetailsMap(productDetailsResponse.getProductDetailsList()); billingServiceClientListener.onProductDetailsFetched(productDetailsMap); } }); }
ProductDetails
, gives you a response similar to the following:{ "productId": "consumable_product_01", "type": "inapp", "title": "Shadow Coat (Yolo's Realm | Play Samples)", "name": "Shadow Coat", "description": "A sleek, obsidian coat for stealth and ambushes", "skuDetailsToken": "<---skuDetailsToken--->", "oneTimePurchaseOfferDetails": {}, "oneTimePurchaseOfferDetailsList": [ { "priceAmountMicros": 1990000, "priceCurrencyCode": "USD", "formattedPrice": "$1.99", "offerIdToken": "<--offerIdToken-->", "purchaseOptionId": "buy", "offerTags": [] } ] }, { "productId": "consumable_product_02", "type": "inapp", "title": "Emperor Den (Yolo's Realm | Play Samples)", "name": "Emperor Den", "description": "A fair lair glowing with molten rock and embers", "skuDetailsToken": "<---skuDetailsToken--->", "oneTimePurchaseOfferDetails": {}, "oneTimePurchaseOfferDetailsList": [ { "priceAmountMicros": 2990000, "priceCurrencyCode": "USD", "formattedPrice": "$2.99", "offerIdToken": "<--offerIdToken-->", "purchaseOptionId": "buy", "offerTags": [] } ] }
- Launch the billing flow.
public void launchBillingFlow(String productId) { ProductDetails productDetails = productDetailsMap.get(productId); if (productDetails == null) { Log.e( TAG, "Cannot launch billing flow: ProductDetails not found for productId: " + productId); billingServiceClientListener.onBillingResponse( BillingResponseCode.ITEM_UNAVAILABLE, BillingResult.newBuilder().setResponseCode(BillingResponseCode.ITEM_UNAVAILABLE).build()); return; } ImmutableList<ProductDetailsParams> productDetailsParamsList = ImmutableList.of( ProductDetailsParams.newBuilder().setProductDetails(productDetails).build()); BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetailsParamsList) .build(); billingClient.launchBillingFlow(activity, billingFlowParams); }
- Detect and process purchases. As part of this step, you need to:
- Verify the purchase
- Grant entitlement to the user
- Notify the user
- Notify Google of the purchase process
private void handlePurchase(Purchase purchase) { // Step 1: Send the purchase to your secure backend to verify the purchase following // https://developer.android.com/google/play/billing/security#verify // Step 2: Update your entitlement storage with the purchase. If purchase is // in PENDING state then ensure the entitlement is marked as pending and the // user does not receive benefits yet. It is recommended that this step is // done on your secure backend and can combine in the API call to your // backend in step 1. // Step 3: Notify the user using appropriate messaging. if (purchase.getPurchaseState() == PurchaseState.PURCHASED) { for (String product : purchase.getProducts()) { Log.d(TAG, product + " purchased successfully! "); } } // Step 4: Notify Google the purchase was processed. // For one-time products, acknowledge the purchase. // This sample app (client-only) uses billingClient.acknowledgePurchase(). // For consumable one-time products, consume the purchase // This sample app (client-only) uses billingClient.consumeAsync() // If you have a secure backend, you must acknowledge purchases on your server using the // server-side API. // See https://developer.android.com/google/play/billing/security#acknowledge if (purchase.getPurchaseState() == PurchaseState.PURCHASED && !purchase.isAcknowledged()) { if (shouldConsume(purchase)) { ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); billingClient.consumeAsync(consumeParams, consumeResponseListener); } else { AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); billingClient.acknowledgePurchase( acknowledgePurchaseParams, acknowledgePurchaseResponseListener); } } }
5. Analyze purchase drop-offs
So far in the codelab, the Play Billing responses focussed on limited scenarios such as USER_CANCELLED, BILLING_UNAVAILABLE, OK, and ITEM_ALREADY_OWNED responses. However, Play Billing can return 13 different response codes which can be triggered by various real-world factors.
This section elaborates on the causes for the USER_CANCELLED
and BILLING_UNAVAILABLE
error responses and suggests possible corrective actions that you can implement.
USER_CANCELED response error code
This response code indicates that the user has abandoned the purchase flow UI before completing the purchase.
Probable causes | What actions can you take? |
|
|
BILLING_UNAVAILABLE response error code
This response code means that the purchase couldn't be completed due to an issue with the user's payment provider or their chosen form of payment. For example, the user's credit card has expired or the user is in an unsupported country. This code doesn't indicate an error with the Play Billing system itself.
Probable causes | What actions can you take? |
|
|
Retry strategies for response error codes
Effective retry strategies for recoverable errors from the Play Billing Library (PBL) vary based on the context such as user-in-session interactions (like during a purchase) versus background operations (such as querying purchases on app resume). It is important to implement these strategies because certain BillingResponseCode
values signify temporary issues that can be resolved by retrying, while others are permanent and don't require retries.
For errors encountered when the user is in session, a simple retry strategy with a set maximum number of attempts is advisable to minimize disruption to the user experience. Conversely, for background operations such as acknowledging new purchases, which don't demand immediate execution, exponential backoff is the recommended approach.
For detailed information on specific response codes and their corresponding recommended retry strategies, see Handle BillingResult response codes.
6. Next steps
- Learn how to maximize your play billing integration.
- Remember to follow the best practices for verifying and processing purchases on your secure backend once users start buying these products.
Reference docs
7. Congratulations!
Congratulations! You've successfully navigated the Google Play Console to create a new one-time product, test billing response codes, analyzed the purchase drop-offs.
Survey
Your feedback on this codelab is highly valued. Consider taking a few minutes to complete our survey.