Sell subscriptions in app with the Play Billing Library 5

1. Introduction

Google Play's billing system is a service that allows you to sell digital products and content in your Android app. It's the most direct way for you to sell in-app products to monetize your app. This codelab shows you how to use the Google Play Billing Library to sell subscriptions in your project in a way that encapsulates the nitty-gritty details when integrating purchases with the rest of your app.

It also introduces subscription related concepts such as base plans, offers, tags, and prepaid plans. To learn more about subscriptions on Google Play Billing, you can check out our Help Center.

What you'll build

In this codelab, you're going to add the latest billing library (version 5.0.0) to a simple subscription based profile app. The app is already built for you, so you'll just be adding the billing portion. As shown in figure 1, in this app the user signs up for any of the base plans and/or offers offered through two renewable subscription products (basic and premium) or for a non-renewable prepaid. That's all. The base plans are respectively monthly and yearly subscriptions. The user can upgrade, downgrade, or convert a prepaid subscription to a renewable one.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

To incorporate the Google Play Billing Library in your app, you'll create the following:

  • BillingClientWrapper- a wrapper for the BillingClient library. It intends to encapsulate the interactions with the Play Billing Library's BillingClient but it isn't required in your own integration.
  • SubscriptionDataRepository - a billing repository for your app that contains a list of the app's subscription products inventory (i.e. what's for sale), and a list of ShareFlow variables that helps collect the state of purchases and product details
  • MainViewModel - a ViewModel through which the rest of your app communicates with the billing repository. It helps to launch the billing flow in the UI using various purchase methods.

When finished, your app's architecture should look similar to the below figure:

c83bc759f32b0a63.png

What you'll learn

  • How to integrate the Play billing library
  • How to create subscription products, base plans, offers and tags via the Play Console
  • How to retrieve available base plans and offers from the app
  • How to launch the billing flow with the appropriate parameters
  • How to offer prepaid subscription products

This codelab is focused on Google Play Billing. Non-relevant concepts and code blocks are glossed over and are provided for you to simply copy and paste.

What you'll need

  • A recent version of Android Studio (>= Arctic Fox | 2020.3.1)
  • An Android device with Android 8.0 or greater
  • The sample code, provided for you on GitHub (instructions in later sections)
  • Moderate knowledge of Android development on Android Studio
  • Knowledge of how to publish an app to the Google Play Store
  • Moderate experience writing Kotlin code
  • Google Play Billing library version 5.0.0

2. Getting set up

Get the code from Github

We've put everything you need for this project into a Git repo. To get started, you'll need to grab the code and open it in your favorite dev environment. For this codelab, we recommend using Android Studio.

The code to get started is stored in a GitHub repository. You can clone the repository via the following command:

git clone https://github.com/android/play-billing-samples.git
cd PlayBillingCodelab

3. The Groundwork

What's our starting point?

Our starting point is a basic user profile app designed for this codelab. The code has been simplified to show the concepts we want to illustrate and it hasn't been designed for production use. If you choose to reuse any part of this code in a production app, make sure to follow best practices and fully test all your code.

Import the project into Android Studio

PlayBillingCodelab is the base app that doesn't contain a Google Play Billing implementation. Start Android Studio and import billing-codelab by choosing Open > billing/PlayBillingCodelab

The project has two modules::

  • start has the skeleton app but is lacking the required dependencies and all the methods you need to implement.
  • finished has the completed project and can serve as a guide when you are stuck.

The app consists of eight class files: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory, and MainActivity.

  • BillingClientWrapper is a wrapper that isolates the Google Play Billing's [BillingClient] methods needed to have a simple implementation and emits responses to the data repository for processing.
  • SubscriptionDataRepository is used to abstract the Google Play Billing data source (.i.e. the Billing Client library) and converts the StateFlow data emitted in BillingClientWrapper into Flows.
  • ButtonModel is a data class used to build buttons in the UI.
  • Composables extracts all the UI's composable methods into one class.
  • MainState is a data class for state management.
  • MainViewModel is used to hold billing related data and states used in the UI. It combines all the flows in SubscriptionDataRepository into one state object.
  • MainActivity is the main activity class that loads the Composables for the user interface.
  • Constants is the object that has the constants used by multiple classes.

Gradle

You need to add a Gradle dependency in order to add Google Play Billing to your app. Open the build.gradle file of the app module, and add the following:

dependencies {
    val billing_version = "5.0.0"

    implementation("com.android.billingclient:billing:$billing_version")
}

Google Play Console

For the purposes of this codelab, you need to create the following two subscription product offerings in the Google Play Console's subscriptions section:

  • 1 basic subscription with the product id up_basic_sub

The product should have 3 base plans (2 auto-renewing and 1 prepaid) with associated tags : 1 monthly basic subscription with the tag monthlybasic, 1 yearly basic subscription with the tag yearlybasic , and 1 prepaid subscription with the tag prepaidbasic

You can add offers to the base plans. Offers will inherit the tags from their associated base plans.

  • 1 premium subscription with the product id up_premium_sub

The product should have 3 base plans(2 auto-renewing and 1 prepaid) with associated tags: 1 monthly basic subscription with the tag monthlypremium, 1 yearly basic subscription with the tag yearlypremium, and 1 prepaid subscription with the tag prepaidpremium

a9f6fd6e70e69fed.png

You can add offers to the base plans. Offers will inherit the tags from their associated base plans.

For more detailed information about how to create subscription products, base plans, offers, and tags, please see the Google Play Help Center.

4. The Billing Client set up

For this section, you'll be working in the BillingClientWrapper class.

By the end, you'll have everything needed for the Billing Client to be instantiated, and all the related methods.

  1. Initialize a BillingClient

Once we've added a dependency on the Google Play Billing Library, we need to initialize a BillingClient instance.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Establish a connection to Google Play

After you have created a BillingClient, we need to establish a connection to Google Play.

To connect to Google Play, we call startConnection(). The connection process is asynchronous, and we need to implement a BillingClientStateListener to receive a callback once the setup of the client is complete and it's ready to make further requests.

BillingClientWrapper.kt

fun startBillingConnection(billingConnectionState: MutableLiveData<Boolean>) {

   billingClient.startConnection(object : BillingClientStateListener {
       override fun onBillingSetupFinished(billingResult: BillingResult) {
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               Log.d(TAG, "Billing response OK")
               // The BillingClient is ready. You can query purchases and product details here
               queryPurchases()
               queryProductDetails()
               billingConnectionState.postValue(true)
           } else {
               Log.e(TAG, billingResult.debugMessage)
           }
       }

       override fun onBillingServiceDisconnected() {
           Log.i(TAG, "Billing connection disconnected")
           startBillingConnection(billingConnectionState)
       }
   })
}
  1. Query Google Play Billing for existing purchases

After we have established a connection to Google Play, we are ready to query for purchases that the user has previously made by calling queryPurchasesAsync().

BillingClientWrapper.kt

fun queryPurchases() {
   if (!billingClient.isReady) {
       Log.e(TAG, "queryPurchases: BillingClient is not ready")
   }
   // Query for existing subscription products that have been purchased.
   billingClient.queryPurchasesAsync(
       QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
   ) { billingResult, purchaseList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           if (!purchaseList.isNullOrEmpty()) {
               _purchases.value = purchaseList
           } else {
               _purchases.value = emptyList()
           }

       } else {
           Log.e(TAG, billingResult.debugMessage)
       }
   }
}
  1. Show products available to buy

We can now query for available products and display them to the users. To query Google Play for subscription product details, we will call queryProductDetailsAsync(). Querying for Product details is an important step before displaying the products to the users, as it returns localized product information.

BillingClientWrapper.kt

fun queryProductDetails() {
   val params = QueryProductDetailsParams.newBuilder()
   val productList = mutableListOf<QueryProductDetailsParams.Product>()
   for (product in LIST_OF_PRODUCTS) {

       productList.add(
           QueryProductDetailsParams.Product.newBuilder()
               .setProductId(product)
               .setProductType(BillingClient.ProductType.SUBS)
               .build()
       )

       params.setProductList(productList).let { productDetailsParams ->
           Log.i(TAG, "queryProductDetailsAsync")
           billingClient.queryProductDetailsAsync(productDetailsParams.build(), this)
       }
   }
}
  1. Set the Listener for the ProductDetails query

Note: This method emits the result of the query in a Map to _productWithProductDetails.

Also note that the query is expected to return ProductDetails. If this does not happen, the issue is most likely that products set up in the Play console have not been activated or you have not published a build with the billing client dependency in any of the release tracks.

BillingClientWrapper.kt

override fun onProductDetailsResponse(
   billingResult: BillingResult,
   productDetailsList: MutableList<ProductDetails>
) {
   val responseCode = billingResult.responseCode
   val debugMessage = billingResult.debugMessage
   when (responseCode) {
       BillingClient.BillingResponseCode.OK -> {
           var newMap = emptyMap<String, ProductDetails>()
           if (productDetailsList.isNullOrEmpty()) {
               Log.e(
                   TAG,
                   "onProductDetailsResponse: " +
                           "Found null or empty ProductDetails. " +
                           "Check to see if the Products you requested are correctly " +
                           "published in the Google Play Console."
               )
           } else {
               newMap = productDetailsList.associateBy {
                   it.productId
               }
           }
           _productWithProductDetails.value = newMap
       }
       else -> {
           Log.i(TAG, "onProductDetailsResponse: $responseCode $debugMessage")
       }
   }
}
  1. Launch the purchase flow

launchBillingFlow is the method that's called when the user clicks to purchase an item. It prompts Google Play to start the buying flow with the product's ProductDetails.

BillingClientWrapper.kt

fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
   if (!billingClient.isReady) {
       Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
   }
   billingClient.launchBillingFlow(activity, params)

}
  1. Set the listener for the result of the purchase operation

When the user exits the Google Play purchase screen (either by tapping the ‘Buy' button to complete the purchase, or by tapping the Back button to cancel the purchase), the onPurchaseUpdated() callback sends the result of the purchase flow back to your app. Based on the BillingResult.responseCode, you can then determine whether the user successfully purchased the product. If responseCode == OK, that means the purchase was successfully completed.

onPurchaseUpdated() passes back a list of Purchase objects that includes all the purchases the user has made through the app. Among many other fields, each Purchase object contains the product id, purchaseToken, and isAcknowledged attributes. Using these fields, for each Purchase object you can then determine whether it is a new purchase that needs to be processed or an existing purchase that needs no further processing.

For subscription purchases, processing is akin to acknowledging the new purchase.

BillingClientWrapper.kt

override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchases: List<Purchase>?
) {
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
       && !purchases.isNullOrEmpty()
   ) {
       // Post new purchase List to _purchases
       _purchases.value = purchases

       // Then, handle the purchases
       for (purchase in purchases) {
           acknowledgePurchases(purchase)
       }
   } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
       // Handle an error caused by a user cancelling the purchase flow.
       Log.e(TAG, "User has cancelled")
   } else {
       // Handle any other error codes.
   }
}
  1. Processing purchases (Verify and Acknowledge purchases)

Once a user completes a purchase, the app then needs to process that purchase by acknowledging it.

Additionally _isNewPurchaseAcknowledged's value is set to true when the acknowledgement is successfully processed.

BillingClientWrapper.kt

private fun acknowledgePurchases(purchase: Purchase?) {
   purchase?.let {
       if (!it.isAcknowledged) {
           val params = AcknowledgePurchaseParams.newBuilder()
               .setPurchaseToken(it.purchaseToken)
               .build()

           billingClient.acknowledgePurchase(
               params
           ) { billingResult ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
                   it.purchaseState == Purchase.PurchaseState.PURCHASED
               ) {
                   _isNewPurchaseAcknowledged.value = true
               }
           }
       }
   }
}
  1. Terminate billing connection

Finally, when an activity is destroyed, you want to terminate the connection to Google Play, so endConnection() is called to do so.

BillingClientWrapper.kt

fun terminateBillingConnection() {
   Log.i(TAG, "Terminating connection")
   billingClient.endConnection()
}

5. The SubscriptionDataRepository

In the BillingClientWrapper, responses from QueryPurchasesAsync and QueryProductDetails are posted respectively to MutableStateFlow _purchases and _productWithProductDetails which are exposed outside of the class with purchases and productWithProductDetails.

In SubscriptionDataRepository, purchases are processed into three Flows based on the returned purchase's product: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium, and hasPremiumPrepaid.

Additionally, productWithProductDetails is processed into respective basicProductDetails, and premiumProductDetails Flows.

6. The MainViewModel

The hard part is done. Now you are going to define the MainViewModel, which is just a public interface for your clients so they don't have to know the internals of BillingClientWrapper and SubscriptionDataRepository.

First in the MainViewModel, we start the billing connection when the viewModel is initialized.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Then, the Flows from the repository are respectively combined into productsForSaleFlows (for available products) and userCurrentSubscriptionFlow (for the user's current and active subscription) as processed in the repo class.

The list of current purchases is also made available to the UI with currentPurchasesFlow.

MainViewModel.kt

val productsForSaleFlows = combine(
   repo.basicProductDetails,
   repo.premiumProductDetails
) { basicProductDetails,
   premiumProductDetails
   ->
   MainState(
       basicProductDetails = basicProductDetails,
       premiumProductDetails = premiumProductDetails
   )
}

// The userCurrentSubscriptionFlow object combines all the possible subscription flows into one
// for emission.
private val userCurrentSubscriptionFlow = combine(
   repo.hasRenewableBasic,
   repo.hasPrepaidBasic,
   repo.hasRenewablePremium,
   repo.hasPrepaidPremium
) { hasRenewableBasic,
   hasPrepaidBasic,
   hasRenewablePremium,
   hasPrepaidPremium
   ->
   MainState(
       hasRenewableBasic = hasRenewableBasic,
       hasPrepaidBasic = hasPrepaidBasic,
       hasRenewablePremium = hasRenewablePremium,
       hasPrepaidPremium = hasPrepaidPremium
   )
}

// Current purchases.
val currentPurchasesFlow = repo.purchases

The combined userCurrentSubscriptionFlow is collected in an init block and the value is posted to a MutableLiveData object called _destinationScreen.

init {
   viewModelScope.launch {
       userCurrentSubscriptionFlow.collectLatest { collectedSubscriptions ->
           when {
               collectedSubscriptions.hasRenewableBasic == true &&
                       collectedSubscriptions.hasRenewablePremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasRenewablePremium == true &&
                       collectedSubscriptions.hasRenewableBasic == false -> {
                   _destinationScreen.postValue(DestinationScreen.PREMIUM_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasPrepaidBasic == true &&
                       collectedSubscriptions.hasPrepaidPremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN)
               }
               collectedSubscriptions.hasPrepaidPremium == true &&
                       collectedSubscriptions.hasPrepaidBasic == false -> {
                   _destinationScreen.postValue(
                       DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN
                   )
               }
               else -> {
                   _destinationScreen.postValue(DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN)
               }
           }
       }

   }
}

MainViewModel also adds some very helpful methods:

  1. Base Plans and Offer tokens retrieval

Starting with Play Billing Library version 5.0.0, all subscription products can have multiple base plans and offers except for prepaid base plans which cannot have offers.

This method helps retrieve all offers and base plans a user is eligible for by using the newly introduced concept of tags that are used to group related offers.

For example, when a user attempts to buy a monthly basic subscription, all base plans and offers associated with the monthly basic subscription product are tagged with the string monthlyBasic.

MainViewModel.kt

private fun retrieveEligibleOffers(
   offerDetails: MutableList<ProductDetails.SubscriptionOfferDetails>,
   tag: String
): List<ProductDetails.SubscriptionOfferDetails> {
   val eligibleOffers = emptyList<ProductDetails.SubscriptionOfferDetails>().toMutableList()
   offerDetails.forEach { offerDetail ->
       if (offerDetail.offerTags.contains(tag)) {
           eligibleOffers.add(offerDetail)
       }
   }

   return eligibleOffers
}
  1. Lowest priced offer calculation

When a user is eligible for multiple offers, the leastPricedOfferToken() method is used to calculate the lowest offer amongst the ones returned by retrieveEligibleOffers().

The method returns the offer id token of the selected offer.

This implementation simply returns the lowest priced offers in terms of the pricingPhases set and does not account for averages.

Another implementation could be looking at the lowest average priced offer instead.

MainViewModel.kt

private fun leastPricedOfferToken(
   offerDetails: List<ProductDetails.SubscriptionOfferDetails>
): String {
   var offerToken = String()
   var leastPricedOffer: ProductDetails.SubscriptionOfferDetails
   var lowestPrice = Int.MAX_VALUE

   if (!offerDetails.isNullOrEmpty()) {
       for (offer in offerDetails) {
           for (price in offer.pricingPhases.pricingPhaseList) {
               if (price.priceAmountMicros < lowestPrice) {
                   lowestPrice = price.priceAmountMicros.toInt()
                   leastPricedOffer = offer
                   offerToken = leastPricedOffer.offerToken
               }
           }
       }
   }
   return offerToken
}
  1. BillingFlowParams builders

To launch the purchase flow for a particular product, the product' s ProductDetails and selected offer's token need to be set and used to build a BilingFlowParams.

There are two methods to help with this:

upDowngradeBillingFlowParamsBuilder() builds the params for upgrades and downgrades.

MainViewModel.kt

private fun upDowngradeBillingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String,
   oldToken: String
): BillingFlowParams {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   ).setSubscriptionUpdateParams(
       BillingFlowParams.SubscriptionUpdateParams.newBuilder()
           .setOldPurchaseToken(oldToken)
           .setReplaceProrationMode(
               BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
           )
           .build()
   ).build()
}

billingFlowParamsBuilder() builds the params for normal purchases.

MainViewModel.kt

private fun billingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String
): BillingFlowParams.Builder {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   )
}
  1. Buy method

The buy method uses BillingClientWrapper's launchBillingFlow() and the BillingFlowParams to launch purchases.

MainViewModel.kt

fun buy(
   productDetails: ProductDetails,
   currentPurchases: List<Purchase>?,
   activity: Activity,
   tag: String
) {
   val offers =
       productDetails.subscriptionOfferDetails?.let {
           retrieveEligibleOffers(
               offerDetails = it,
               tag = tag.lowercase()
           )
       }
   val offerToken = offers?.let { leastPricedOfferToken(it) }
   val oldPurchaseToken: String

   // Get current purchase. In this app, a user can only have one current purchase at
   // any given time.
   if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size == MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // This either an upgrade, downgrade, or conversion purchase.
       val currentPurchase = currentPurchases.first()

       // Get the token from current purchase.
       oldPurchaseToken = currentPurchase.purchaseToken

       val billingParams = offerToken?.let {
           upDowngradeBillingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it,
               oldToken = oldPurchaseToken
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams
           )
       }
   } else if (currentPurchases == null) {
       // This is a normal purchase.
       val billingParams = offerToken?.let {
           billingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams.build()
           )
       }
   } else if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size > MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // The developer has allowed users  to have more than 1 purchase, so they need to
       /// implement a logic to find which one to use.
       Log.d(TAG, "User has more than 1 current purchase.")
   }
}

d726a27add092140.png

  1. Terminate the billing connection

Finally, the BillingClientWrapper's terminateBillingConnection method is called on a ViewModel's onCleared().

This is to terminate the current billing connection when the associated activity is destroyed.

7. The UI

Now, it is time to use everything you've built in the UI. To help with that you'll be working with the Composables and MainActivity classes.

Composables.kt

The Composables class is fully supplied and defines all the Compose functions used to render the UI and the navigation mechanism between them.

The Subscriptions function shows two buttons: Basic Subscription, and Premium Subscription.

Basic Subscription and Premium Subscription each loads new Compose methods that show the three respective base plans: monthly, yearly, and prepaid.

Then, there are three possible profile compose functions each for a specific subscription a user may have: a renewable Basic, a renewable Premium, and either a Prepaid Basic or Prepaid Premium profile.

  • When a user has Basic subscription, the basic profile allows them to upgrade to either a monthly, yearly, or prepaid premium subscription.
  • Conversely, when a user has a premium subscription, they can downgrade to either a monthly, a yearly, or a prepaid basic subscription.
  • When a user has a prepaid subscription, they can top up their subscription with the top up button or convert their prepaid subscription into a corresponding auto-renewing base plan.

Finally, there is a Loading screen function that is used when connection is being made to Google Play and when a user profile is being rendered.

MainActivity.kt

  1. When MainActivity is created, the viewModel is instantiated and a compose function called MainNavHost is loaded.
  2. MainNavHost starts with a variable isBillingConnected created from the viewModel's billingConnectionSate Livedata and observed for changes because when the vieModel is instantiated, it passes billingConnectionSate into the BillingClientWrapper's startBillingConnection method.

isBillingConnected is set to true when the connection is established and false when not.

When false, the LoadingScreen() compose function is loaded, and when true the Subscription or profile functions are loaded.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. When a billing connection is established:

The Compose navController is instantiated

val navController = rememberNavController()

Then, the flows in MainViewModel are collected.

val productsForSale by viewModel.productsForSaleFlows.collectAsState(
   initial = MainState()
)

val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
   initial = listOf()
)

Finally, the viewModel's destinationScreen LiveData variable is observed.

Based on the user's current subscription status, the corresponding compose function is rendered.

val screen by viewModel.destinationScreen.observeAsState()
when (screen) {
   // User has a Basic Prepaid subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_monthly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_yearly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_BASIC_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable basic subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_RENEWABLE_PROFILE -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.monthly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.basic_sub_message
       )
   }
   // User has a prepaid Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_monthly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_yearly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_PREMIUM_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_RENEWABLE_PROFILE -> {
       UserProfile(
           listOf(
               ButtonModel(R.string.monthly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.premium_sub_message
       )
   }
   // User has no current subscription - the subscription composable
   // is loaded.
   MainViewModel.DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN -> {
       SubscriptionNavigationComponent(
           productsForSale = productsForSale,
           navController = navController,
           viewModel = viewModel
       )
   }
}

8. Solution Code

The complete solution code can be found in the solution module.

9. Congratulations

Congratulations, you've successfully integrated Google Play Billing Library 5.0.0 subscription products into a very simple app!

For a documented version of a more sophisticated app with a secure server setup, see the official sample.

Further reading