Analyze product purchase drop-offs in Play Billing

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...

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:

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:

  1. Download the sample app from GitHub.
  2. 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.
  3. Generate a signed Android app bundle.
    1. Generate an upload key and keystore
    2. Sign your app with your upload key
    3. Configure Play App Signing

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:

  1. Log-in to the Google Play Console, using your developer account.
  2. Click Create app. This opens the Create app page.
  3. Enter an app name, select the default language, and other app related details.
  4. 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

  1. 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.
  2. Click Test and release > Testing > Internal release > Create new release.
  3. Enter a release name and upload the signed app bundle file.
  4. 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:

  1. In the Google Play Console, from the left navigation menu, go to Monetize with Play > Products > One-time products.
  2. Click Create one-time product.
  3. 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.
    Note: For the purpose of this codelab, you can skip configuring the Tax, compliance, and programs section.
  4. Click Next.
  5. 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.
  6. In the Availability and pricing section, click Set prices > Bulk edit pricing.
  7. Select the Country / region option. This selects all the regions.
  8. Click Continue. This opens a dialog to enter a price. Enter 10 USD and then click Apply.
  9. 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:

  1. 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")
    }
    
  2. 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();
    }
    
  3. 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.");
          }
        });
    }
    
  4. 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.
    private 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);
          }
        });
    }
    
    Fetching 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": []
            }
        ]
    }
    
  5. 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);
    }
    
  6. Detect and process purchases. As part of this step, you need to:
    1. Verify the purchase
    2. Grant entitlement to the user
    3. Notify the user
    4. Notify Google of the purchase process
    Out of these, steps a, b, and c should be done in your backend, and hence are out of scope for this codelab.The following snippet shows how to notify Google for a consumable one-time product:
    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?

  • Could indicate low intent users who are sensitive to prices.
  • The purchase is pending or the payment is declined.

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?

  • The Play Store app on the user's device is out of date.
  • The user is in a country where Play isn't supported.
  • The user is an enterprise user, and their enterprise administrator has disabled users from making purchases.
  • Google Play is unable to charge the user's payment method. For example, the user's credit card might have expired.
  • Monitor the trends for system issues and in specific regions
  • Consider migrating to PBL 8 as it supports the more granular PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS sub-response code. If you get this response code, consider notifying users of the failure or suggest alternate payment methods.
  • This response code is designed for retries, allowing you to implement suitable retry strategies.
    Automatic retries are unlikely to help in this case. However, a manual retry can help if the user addresses the condition that caused the issue. For example, if the user updates their Play Store version to a supported version, then a manual retry of the initial operation could work.

    If you get this response code when the user is not in session, retrying might not make sense. When you receive a `BILLING_UNAVAILABLE` response as a result of the purchase flow, it's very likely the user received feedback from Google Play during the purchase process and might be aware of what went wrong. In this case, you could show an error message specifying something went wrong and offer a `Try again` button to give the user the option of a manual retry after they address the issue.

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

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.