Back to the RevenueCat homepage
Google Play Billing

Appendix D: Migrating from PBL 7 to PBL 8

Google Play Billing Library 8.0 is a major release that removes deprecated APIs, renames confusing terminology, and introduces new features that simplify billing integration. If you have a working PBL 7 integration, you cannot simply bump the version number and expect everything to compile. Several APIs that were deprecated in PBL 6 and 7 are now gone entirely, and a few method signatures have changed in ways that require code updates.

This appendix walks you through every breaking change, shows you the before and after code for each migration step, and gives you a checklist to follow so nothing falls through the cracks. If you are starting a new project from scratch, you can skip this appendix. But if you have an existing PBL 7 codebase, read this carefully before upgrading.

Breaking Changes Summary

The following table summarizes every breaking change between PBL 7 and PBL 8. Use it as a quick reference, then read the detailed sections below for migration instructions.

Area

PBL 7 (Removed/Changed)

PBL 8 (Replacement)

Impact

Purchase history

queryPurchaseHistoryAsync()

queryPurchasesAsync()

Compile error. No direct replacement for history queries.

Product queries

querySkuDetailsAsync()

queryProductDetailsAsync()

Compile error. Different return types.

Pending purchases

enablePendingPurchases() (no args)

enablePendingPurchases(PendingPurchasesParams)

Compile error. Must pass params object.

Product details result

ProductDetailsResponseListener returns List<ProductDetails>

Returns QueryProductDetailsResult

Compile error. Must unwrap result object.

Alternative billing

enableAlternativeBilling()

enableUserChoiceBilling()

Compile error. Method and listener renamed.

Alternative billing listener

AlternativeBillingListener

UserChoiceBillingListener

Compile error. Interface renamed.

Alternative choice details

AlternativeChoiceDetails

UserChoiceDetails

Compile error. Class renamed.

Proration modes

ProrationMode constants

ReplacementMode constants

Compile error. Enum class replaced.

Proration setter

setReplaceProrationMode()

setSubscriptionReplacementMode()

Compile error. Method renamed.

Subscription updates

SubscriptionUpdateParams

SubscriptionProductReplacementParams (8.1)

Deprecation warning in 8.0, migration recommended for 8.1.

Auto reconnection

Manual reconnection required

enableAutoServiceReconnection()

New opt in feature.

Removed APIs and Their Replacements

queryPurchaseHistoryAsync() Removal

queryPurchaseHistoryAsync() returned the most recent purchase for each product the user had ever bought, including expired and consumed purchases. Google removed this API in PBL 8 because it encouraged patterns that led to incorrect entitlement logic. Developers often used it to "restore purchases" on new devices, but the results included purchases that were no longer valid, leading to users getting access they should not have.

The replacement is queryPurchasesAsync(), which returns only active, unconsumed purchases. This is what you should use for entitlement checks and purchase restoration.

Before (PBL 7):

kotlin
billingClient.queryPurchaseHistoryAsync(
    QueryPurchaseHistoryParams.newBuilder()
        .setProductType(ProductType.SUBS)
        .build()
) { billingResult, historyRecords ->
    if (billingResult.responseCode ==
        BillingResponseCode.OK
    ) {
        historyRecords?.forEach { record ->
            // Process historical purchase
        }
    }
}

After (PBL 8):

kotlin
val result = billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder()
        .setProductType(ProductType.SUBS)
        .build()
)
if (result.billingResult.responseCode ==
    BillingResponseCode.OK
) {
    result.purchasesList.forEach { purchase ->
        // Process active purchase
    }
}

If you genuinely need historical purchase data (for analytics or auditing), query the Google Play Developer API from your backend using the Purchases.subscriptionsv2 or Purchases.products endpoints. The client library is no longer the right place for historical queries.

querySkuDetailsAsync() to queryProductDetailsAsync()

querySkuDetailsAsync() was the original API for fetching product information. It was deprecated in PBL 5 when Google introduced the new product model with base plans and offers. In PBL 8, it is gone entirely.

The migration is straightforward, but the return types are different. SkuDetails was a flat object with a single price. ProductDetails has a richer structure with subscriptionOfferDetails containing multiple base plans and offers, each with their own pricing phases.

Before (PBL 7):

kotlin
val params = SkuDetailsParams.newBuilder()
    .setSkusList(listOf("premium_monthly"))
    .setType(BillingClient.SkuType.SUBS)
    .build()

billingClient.querySkuDetailsAsync(params) {
    billingResult, skuDetailsList ->
    skuDetailsList?.forEach { skuDetails ->
        val price = skuDetails.price
        val title = skuDetails.title
    }
}

After (PBL 8):

kotlin
val params = QueryProductDetailsParams.newBuilder()
    .setProductList(listOf(
        QueryProductDetailsParams.Product
            .newBuilder()
            .setProductId("premium_monthly")
            .setProductType(ProductType.SUBS)
            .build()
    )).build()

val result = billingClient
    .queryProductDetailsAsync(params)

Note that queryProductDetailsAsync in PBL 8 returns a QueryProductDetailsResult rather than delivering results through a callback with a raw list. You access the product details through the result object. See the next section for details on this change.

enablePendingPurchases() Signature Change

In PBL 7, you called enablePendingPurchases() with no arguments to opt in to pending purchase support. In PBL 8, Google requires you to pass a PendingPurchasesParams object. This gives you explicit control over which types of pending purchases you support.

Before (PBL 7):

kotlin
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases()
    .build()

After (PBL 8):

kotlin
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder()
            .enableOneTimeProducts()
            .enablePrepaidPlans()
            .build()
    )
    .build()

The PendingPurchasesParams builder lets you specify enableOneTimeProducts() and enablePrepaidPlans() separately. You must call at least one of these methods. If your app supports both one time products and prepaid subscriptions, call both. If you only sell auto renewing subscriptions (no prepaid, no one time products), you still need to pass the params object, but you configure it for the product types you actually sell.

ProductDetailsResponseListener Signature Change

In PBL 7, queryProductDetailsAsync() accepted a ProductDetailsResponseListener that received a BillingResult and a nullable List<ProductDetails>. In PBL 8, the method returns a QueryProductDetailsResult object instead. This result object wraps both the billing result and the product details list, and also introduces handling for unfetched products (covered in the new features section below).

Before (PBL 7):

kotlin
billingClient.queryProductDetailsAsync(
    params,
    ProductDetailsResponseListener {
        billingResult, productDetailsList ->
        if (billingResult.responseCode ==
            BillingResponseCode.OK
        ) {
            productDetailsList?.forEach { details ->
                displayProduct(details)
            }
        }
    }
)

After (PBL 8):

kotlin
val result = billingClient
    .queryProductDetailsAsync(params)

if (result.billingResult.responseCode ==
    BillingResponseCode.OK
) {
    result.productDetailsList.forEach { details ->
        displayProduct(details)
    }
    // Check for products that could not be fetched
    result.unfetchedProductList.forEach { unfetched ->
        logUnfetchedProduct(unfetched)
    }
}

The QueryProductDetailsResult object gives you three things: the billingResult with the response code, the productDetailsList containing the products that were successfully fetched, and the unfetchedProductList containing products that could not be retrieved. This is a cleaner pattern than the nullable list from PBL 7.

Alternative Billing API Renaming

Google renamed the entire alternative billing API surface to use "user choice" terminology instead of "alternative." This is not just cosmetic. The rename reflects Google's compliance framework and the legal terminology used in various markets. The functionality is identical, but every class, method, and interface name has changed.

PBL 7 Name

PBL 8 Name

enableAlternativeBilling()

enableUserChoiceBilling()

AlternativeBillingListener

UserChoiceBillingListener

AlternativeChoiceDetails

UserChoiceDetails

AlternativeChoiceDetails.Product

UserChoiceDetails.Product

Before (PBL 7):

kotlin
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases()
    .enableAlternativeBilling(
        AlternativeBillingListener { details ->
            val products = details.products
            val token = details.externalTransactionToken
            handleAlternativeBilling(products, token)
        }
    )
    .build()

After (PBL 8):

kotlin
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder()
            .enableOneTimeProducts()
            .build()
    )
    .enableUserChoiceBilling(
        UserChoiceBillingListener { details ->
            val products = details.products
            val token = details.externalTransactionToken
            handleUserChoiceBilling(products, token)
        }
    )
    .build()

If you use alternative billing in your app, this is a find and replace migration. The callback behavior, the data you receive, and the flow you implement are all the same. Just update the names.

Replacement Mode Migration from ProrationMode

PBL 7 used ProrationMode constants to specify how plan changes should handle billing transitions. PBL 8 replaces this with ReplacementMode, which uses clearer names and adds a new mode (KEEP_EXISTING in PBL 8.1). The setter method on SubscriptionUpdateParams also changed from setReplaceProrationMode() to setSubscriptionReplacementMode().

Constant Mapping

ProrationMode (PBL 7)

ReplacementMode (PBL 8)

IMMEDIATE_WITH_TIME_PRORATION

WITH_TIME_PRORATION

IMMEDIATE_AND_CHARGE_PRORATED_PRICE

CHARGE_PRORATED_PRICE

IMMEDIATE_AND_CHARGE_FULL_PRICE

CHARGE_FULL_PRICE

IMMEDIATE_WITHOUT_PRORATION

WITHOUT_PRORATION

DEFERRED

DEFERRED

(N/A)

KEEP_EXISTING (PBL 8.1+)

Notice that Google dropped the IMMEDIATE_ prefix from most modes. The old names were verbose and somewhat misleading (for example, DEFERRED was never prefixed with IMMEDIATE_ even in PBL 7, creating an inconsistency). The new names are shorter and more consistent.

Before (PBL 7):

kotlin
val updateParams = BillingFlowParams
    .SubscriptionUpdateParams.newBuilder()
    .setOldPurchaseToken(oldToken)
    .setReplaceProrationMode(
        ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE
    )
    .build()

After (PBL 8.0):

kotlin
val updateParams = BillingFlowParams
    .SubscriptionUpdateParams.newBuilder()
    .setOldPurchaseToken(oldToken)
    .setSubscriptionReplacementMode(
        ReplacementMode.CHARGE_PRORATED_PRICE
    )
    .build()

SubscriptionUpdateParams Deprecation in PBL 8.1

In PBL 8.0, SubscriptionUpdateParams still works with the new ReplacementMode constants and the renamed setter. But starting in PBL 8.1, Google deprecated SubscriptionUpdateParams entirely in favor of SubscriptionProductReplacementParams. The new API attaches replacement configuration to the product parameters rather than to the billing flow parameters.

After (PBL 8.1, recommended):

kotlin
val replacementParams = BillingFlowParams
    .SubscriptionProductReplacementParams
    .newBuilder()
    .setOldPurchaseToken(oldToken)
    .setReplacementMode(
        ReplacementMode.CHARGE_PRORATED_PRICE
    )
    .build()

val productParams = BillingFlowParams
    .ProductDetailsParams.newBuilder()
    .setProductDetails(newProductDetails)
    .setOfferToken(selectedOfferToken)
    .setSubscriptionReplacementParams(
        replacementParams
    )
    .build()

This two step migration (ProrationMode to ReplacementMode in 8.0, then SubscriptionUpdateParams to SubscriptionProductReplacementParams in 8.1) can be done all at once if you are jumping straight from PBL 7 to PBL 8.1 or later. There is no reason to stop at the intermediate step.

New Features to Adopt

PBL 8 is not just about breaking changes. It introduces several new features that you should adopt during your migration to get the most out of the upgrade.

enableAutoServiceReconnection()

In PBL 7, when the connection to Google Play Services dropped (which happens when the system kills the Play Store process or the user's device switches networks), you had to detect the disconnection in onBillingServiceDisconnected() and manually call startConnection() again. This was error prone and led to a lot of boilerplate retry logic.

PBL 8 introduces enableAutoServiceReconnection() on the BillingClient.Builder. When enabled, the library automatically re establishes the connection when it drops, with built in exponential backoff. You no longer need to manage reconnection yourself.

kotlin
val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder()
            .enableOneTimeProducts()
            .build()
    )
    .enableAutoServiceReconnection()
    .build()

After enabling this, your onBillingServiceDisconnected() callback still fires, but you do not need to call startConnection() inside it. The library handles that for you. You can use the callback purely for logging or UI updates (such as showing a temporary "reconnecting" indicator).

UnfetchedProduct Handling

When you call queryProductDetailsAsync() in PBL 8, the result may include an unfetchedProductList. This list contains products that you requested but that could not be retrieved. Reasons include a product being deactivated in the Play Console, a product ID typo, or a temporary server issue for a specific product.

In PBL 7, if one product out of ten failed to load, you got no information about which one failed. You just received a shorter list than expected. In PBL 8, the UnfetchedProduct object tells you exactly which product ID failed and why.

kotlin
val result = billingClient
    .queryProductDetailsAsync(params)

result.unfetchedProductList.forEach { unfetched ->
    Log.w("Billing",
        "Could not fetch: ${unfetched.productId}, " +
        "reason: ${unfetched.reason}"
    )
}

Use this information to build better diagnostics. If a product consistently appears in the unfetched list, it might be misconfigured in the Play Console or the product ID in your code might have a typo.

Sub Response Codes

PBL 8 introduces sub response codes that provide more granular information about why a purchase failed. The main BillingResponseCode tells you what happened (for example, the purchase failed). The sub response code tells you why (for example, the user had insufficient funds).

You access sub response codes through BillingResult.getOnPurchasesUpdatedSubResponseCode() inside your PurchasesUpdatedListener. See Appendix B for the full list of sub response codes and recommended handling strategies.

includeSuspendedSubscriptions (PBL 8.1)

PBL 8.1 adds the includeSuspendedSubscriptions option to QueryPurchasesParams. When enabled, queryPurchasesAsync() returns subscriptions that are in a suspended state (such as account hold or grace period) in addition to fully active subscriptions.

kotlin
val params = QueryPurchasesParams.newBuilder()
    .setProductType(ProductType.SUBS)
    .includeSuspendedSubscriptions(true)
    .build()
val result = billingClient
    .queryPurchasesAsync(params)

This is useful when you want to show users that their subscription has a payment issue and prompt them to update their payment method. Without this flag, suspended subscriptions are invisible to queryPurchasesAsync(), which means you cannot detect the problem on the client side and must rely entirely on your backend and push notifications to alert the user.

Step by Step Migration Checklist

Follow these steps in order when migrating from PBL 7 to PBL 8.

  1. Update your dependency. Change your build.gradle file to reference PBL 8.x. Set the version to the latest stable release (8.1 or later is recommended, since it includes SubscriptionProductReplacementParams and includeSuspendedSubscriptions).
  2. Fix the BillingClient builder. Replace enablePendingPurchases() with enablePendingPurchases(PendingPurchasesParams). Add enableAutoServiceReconnection(). If you use alternative billing, rename enableAlternativeBilling() to enableUserChoiceBilling() and update the listener type.
  3. Remove queryPurchaseHistoryAsync() calls. Replace them with queryPurchasesAsync(). If you need historical data, move that logic to your backend.
  4. Remove querySkuDetailsAsync() calls. Replace them with queryProductDetailsAsync(). Update your product display code to handle the ProductDetails model with its base plans and offers structure.
  5. Update queryProductDetailsAsync() callbacks. Switch from the ProductDetailsResponseListener callback pattern to the QueryProductDetailsResult return type. Add handling for unfetchedProductList.
  6. Replace ProrationMode with ReplacementMode. Find every usage of ProrationMode constants and replace them with the corresponding ReplacementMode constants. Replace setReplaceProrationMode() with setSubscriptionReplacementMode().
  7. Migrate SubscriptionUpdateParams (if targeting PBL 8.1+). Replace SubscriptionUpdateParams with SubscriptionProductReplacementParams. Move the replacement configuration from BillingFlowParams to ProductDetailsParams.
  8. Rename alternative billing classes. Replace AlternativeBillingListener with UserChoiceBillingListener, AlternativeChoiceDetails with UserChoiceDetails, and update all references.
  9. Adopt new PBL 8 features. Add enableAutoServiceReconnection() to your builder. Add UnfetchedProduct handling to your product query flow. Consider adopting sub response codes in your purchase error handling. Add includeSuspendedSubscriptions if you want to detect payment issues on the client.
  10. Simplify your reconnection logic. If you have manual reconnection code in onBillingServiceDisconnected(), you can remove it (or reduce it to logging) now that auto reconnection is enabled.
  11. Run your test suite. Verify that all billing flows work: new purchases, subscription renewals, plan changes (upgrades and downgrades), consumable purchases, and pending transactions. Test on a device with the latest Play Store version.
  12. Test with the Google Play Billing Library test tool. Use the BillingClient test methods and license testing accounts to verify each flow without spending real money. Pay special attention to plan change flows if you migrated from ProrationMode to ReplacementMode.
  13. Update your ProGuard/R8 rules if needed. PBL 8 may have different class names that need to be kept. Check the official migration guide for any updated ProGuard rules.
  14. Deploy to internal testing first. Roll out to your internal test track before pushing to production. Monitor crash reports and billing success rates for at least one full billing cycle.

Version Deprecation Timeline

Understanding Google's deprecation timeline helps you plan your migration schedule.

Library Version

Status

Key Date

PBL 5 and earlier

Deprecated, unsupported

Already past end of life. Google Play may reject new app submissions using these versions.

PBL 6

Deprecated

Support ended. Migrate as soon as possible.

PBL 7

Active, approaching deprecation

Support ends August 31, 2026. After this date, Google may stop accepting app updates that target PBL 7.

PBL 8.0

Stable

Current major version.

PBL 8.1+

Stable, recommended

Latest release with all new features.

What "support ends" means in practice: After the deprecation date, Google does not immediately break your existing app. Users who already have your app installed will still be able to make purchases. However, Google may reject new APK/AAB uploads to the Play Console if they reference the deprecated library version. Google may also stop fixing bugs in deprecated versions, so any issues you encounter will not be patched.

The practical deadline: If you are currently on PBL 7, you have until August 31, 2026 to complete your migration. That might sound like plenty of time, but billing migrations deserve thorough testing across multiple billing cycles. Start your migration early enough that you have at least two to three months of production testing before the deadline. A good target is to have your PBL 8 migration in production by June 2026 at the latest.

If you are still on PBL 5 or 6: You are already past the recommended migration window. Prioritize upgrading to PBL 8 immediately. Skipping PBL 7 entirely is fine. You can migrate directly from PBL 5 or 6 to PBL 8 by following the same steps in this appendix, though you may have additional changes to make if you are still using the original SkuDetails based API throughout your codebase.

The migration from PBL 7 to PBL 8 is mechanical, not architectural. Your billing flow structure stays the same. Your backend integration stays the same. The changes are at the API surface level: renamed classes, updated method signatures, and a few removed methods. Budget a few days for the code changes and a few weeks for thorough testing, and you will be in good shape well before the deadline.

Want a simpler approach?

The RevenueCat SDK Handbook covers the same topics — with less code and a managed backend.

Related chapters

  • Chapter 5: Integrating the Play Billing Library

    BillingClient setup, connection lifecycle, product queries, and the four builder methods you must call correctly.

    Learn more
  • Chapter 7: Subscription Upgrades, Downgrades, and Plan Changes

    Six replacement modes, proration math, linkedPurchaseToken chains, and choosing the right mode for each scenario.

    Learn more
  • Chapter 8: Error Handling and Retry Strategies

    Every BillingResponseCode explained. Simple retry, exponential backoff, and the complete error decision tree.

    Learn more
Migrating from PBL 7 to PBL 8 | RevenueCat