Vende suscripciones en la app con la Biblioteca de Facturación Play 5

1. Introducción

El sistema de facturación de Google Play es un servicio que te permite vender productos y contenido digital en tu app para Android. Es la manera más directa de vender productos integrados en la aplicación para monetizar tu app. En este codelab, se muestra cómo usar la Biblioteca de Facturación Google Play para vender suscripciones en tu proyecto de una manera que encapsule los detalles esenciales cuando integres las compras con el resto de tu app.

También se presentan conceptos relacionados con las suscripciones, como los planes básicos, las ofertas, las etiquetas y los planes prepagados. Para obtener más información sobre las suscripciones de Facturación Google Play, consulta nuestro Centro de ayuda.

Qué compilarás

En este codelab, agregarás la biblioteca de facturación más reciente (versión 5.0.0) a una app de perfil basada en suscripciones. Como la app ya está compilada para ti, solo agregarás la parte de facturación. Como se muestra en la figura 1, en esta app, el usuario se registra en cualquiera de los planes básicos o las ofertas que se ofrecen a través de dos productos de suscripción renovables (básico y premium) o una suscripción prepagada no renovable. Eso es todo. Los planes básicos son suscripciones mensuales y anuales respectivamente. El usuario puede actualizar una suscripción prepagada, cambiarla a una versión inferior o convertirla en una renovable.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Para incorporar la Biblioteca de Facturación Google Play a tu app, crearás lo siguiente:

  • BillingClientWrapper: Es un wrapper para la biblioteca BillingClient. Tiene como objetivo encapsular las interacciones con BillingClient de la Biblioteca de Facturación Play, pero no es necesario en tu propia integración.
  • SubscriptionDataRepository: Es un repositorio de facturación para tu app que contiene una lista del inventario de productos de suscripción de la app (es decir, qué está a la venta) y una lista de variables de ShareFlow que ayuda a recopilar el estado de las compras y los detalles del producto.
  • MainViewModel: Es un ViewModel a través del cual el resto de tu app se comunica con el repositorio de facturación. Ayuda a iniciar el flujo de facturación en la IU con varios métodos de compra.

Cuando termines, la arquitectura de tu app debería ser similar a la que se muestra a continuación:

c83bc759f32b0a63.png

Qué aprenderás

  • Cómo integrar la Biblioteca de Facturación Play
  • Cómo crear productos de suscripción, planes básicos, ofertas y etiquetas a través de Play Console
  • Cómo recuperar los planes básicos y las ofertas disponibles de la app
  • Cómo iniciar el flujo de facturación con los parámetros adecuados
  • Cómo ofrecer productos de suscripción prepagada

Este codelab se enfoca en la Facturación Google Play. Los conceptos y los bloques de código no relevantes se pasan por alto y se proporcionan para que simplemente los copies y pegues.

Requisitos

  • Una versión reciente de Android Studio (>= Arctic Fox | 2020.3.1)
  • Un dispositivo con Android 8.0 o una versión posterior
  • El código de muestra que se te proporciona en GitHub (consulta las instrucciones en secciones posteriores)
  • Conocimientos moderados del desarrollo de Android en Android Studio
  • Conocimientos sobre cómo publicar una app en Google Play Store
  • Experiencia moderada en la escritura de código Kotlin
  • Versión 5.0.0 de la Biblioteca de Facturación Google Play

2. Cómo prepararte

Obtén el código de GitHub

Todo lo que necesitas con relación a este proyecto se encuentra en un repositorio de Git. Para comenzar, deberás tomar el código y abrirlo en tu entorno de desarrollo favorito. Para este codelab, recomendamos usar Android Studio.

El código para comenzar está almacenado en un repositorio de GitHub. Puedes clonar el repositorio con el siguiente comando:

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

3. La estructura básica

¿Cuál es nuestro punto de partida?

Nuestro punto de partida es una app básica de perfil de usuario diseñada para este codelab. El código se simplificó para mostrar los conceptos que queremos ilustrar y no está diseñado para su uso en la producción. Si decides reutilizar cualquier parte de este código en una app de producción, asegúrate de seguir las prácticas recomendadas y probar todo el código por completo.

Importa el proyecto a Android Studio

PlayBillingCodelab es la app base que no contiene una implementación de la Facturación Google Play. Inicia Android Studio e importa el codelab de facturación eligiendo Open > billing/PlayBillingCodelab

El proyecto tiene dos módulos:

  • start tiene la app base, pero carece de las dependencias necesarias y de todos los métodos que debes implementar.
  • finished tiene el proyecto completo y puede servir como guía cuando no puedas avanzar.

La app consta de ocho archivos de clases: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory y MainActivity.

  • BillingClientWrapper es un wrapper que aísla los métodos [BillingClient] de Facturación Google Play necesarios para realizar una implementación simple y emite respuestas al repositorio de datos para su procesamiento.
  • SubscriptionDataRepository se usa para abstraer la fuente de datos de la Facturación Google Play (es decir, la biblioteca cliente de Facturación) y convierte los datos StateFlow emitidos en BillingClientWrapper en flujos.
  • ButtonModel es una clase de datos que se usa para compilar botones en la IU.
  • Los elementos componibles extraen todos los métodos de la IU componibles en una clase.
  • MainState es una clase de datos para la administración de estados.
  • MainViewModel se usa para conservar los datos relacionados con la facturación y los estados que se usan en la IU. Combina todos los flujos de SubscriptionDataRepository en un objeto de estado.
  • MainActivity es la clase principal de actividad que carga los elementos componibles para la interfaz de usuario.
  • Constants es el objeto que tiene las constantes que usan varias clases.

Gradle

Debes agregar una dependencia de Gradle para agregar Facturación Google Play a tu app. Abre el archivo build.gradle del módulo de la app y agrega lo siguiente:

dependencies {
    val billing_version = "5.0.0"

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

Google Play Console

Para los fines de este codelab, debes crear las siguientes dos ofertas de productos de suscripción en la sección de suscripciones de Google Play Console:

  • 1 suscripción básica con el ID del producto up_basic_sub

El producto debe tener 3 planes básicos (2 con renovación automática y 1 prepagado) con etiquetas asociadas : 1 suscripción básica mensual con la etiqueta monthlybasic, 1 suscripción anual básica con la etiqueta yearlybasic y 1 suscripción prepagada con la etiqueta prepaidbasic

Puedes agregar ofertas a los planes básicos. Las ofertas heredarán las etiquetas de sus planes básicos asociados.

  • 1 suscripción premium con el ID del producto up_premium_sub

El producto debe tener 3 planes básicos(2 con renovación automática y 1 prepagado) con etiquetas asociadas: 1 suscripción básica mensual con la etiqueta monthlypremium, 1 suscripción anual básica con la etiqueta yearlypremium y 1 suscripción prepagada con la etiqueta prepaidpremium

a9f6fd6e70e69fed.png

Puedes agregar ofertas a los planes básicos. Las ofertas heredarán las etiquetas de sus planes básicos asociados.

Para obtener información más detallada sobre cómo crear productos de suscripción, planes básicos, ofertas y etiquetas, consulta el Centro de ayuda de Google Play.

4. La configuración del cliente de facturación

En esta sección, trabajarás en la clase BillingClientWrapper.

Al final, tendrás todo lo necesario para que se cree una instancia del cliente de facturación, además de todos los métodos relacionados.

  1. Inicializa un BillingClient

Una vez que agregamos una dependencia en la Biblioteca de Facturación Google Play, debemos inicializar una instancia de BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Establecer una conexión con Google Play

Después de crear un BillingClient, debemos establecer una conexión con Google Play.

Para conectarnos a Google Play, llamamos a startConnection(). El proceso de conexión es asíncrono. Necesitamos implementar un BillingClientStateListener para recibir una devolución de llamada una vez que se complete la configuración del cliente y esté lista para realizar más solicitudes.

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. Cómo buscar compras existentes con la Facturación Google Play

Después de establecer una conexión con Google Play, podremos buscar compras que el usuario haya realizado anteriormente llamando a 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. Cómo mostrar productos disponibles para comprar

Ahora podemos consultar los productos disponibles y mostrarlos a los usuarios. Para consultar en Google Play los detalles del producto de suscripción, llamaremos a queryProductDetailsAsync(). La consulta de detalles de productos es un paso importante antes de mostrar los productos a los usuarios, ya que devuelve información localizada sobre los productos.

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. Cómo configurar el objeto de escucha para la consulta ProductDetails

Nota: Este método emite el resultado de la consulta en un Map a _productWithProductDetails.

Además, ten en cuenta que se espera que la consulta muestre ProductDetails. Si esto no sucede, lo más probable es que el problema sea que no se hayan activado los productos configurados en Play Console o que no hayas publicado una compilación con la dependencia de cliente de facturación en ninguno de los segmentos.

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. Inicia el flujo de compra

launchBillingFlow es el método al que se llama cuando el usuario hace clic para comprar un artículo. Le indica a Google Play que inicie el flujo de compra con el ProductDetails del producto.

BillingClientWrapper.kt

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

}
  1. Configura el objeto de escucha del resultado de la operación de compra

Cuando el usuario sale de la pantalla de compra de Google Play (ya sea presionando el botón "Comprar" para completar la compra o el botón Atrás para cancelar la compra), la devolución de llamada onPurchaseUpdated() envía el resultado del flujo de compra a tu app. Según el BillingResult.responseCode, puedes determinar si el usuario compró correctamente el producto. Si el valor es responseCode == OK, significa que la compra se completó correctamente.

onPurchaseUpdated() devuelve una lista de objetos Purchase que incluye todas las compras que el usuario realizó a través de la app. Entre muchos otros campos, cada objeto Purchase contiene los atributos ID de producto, purchaseToken y isAcknowledged. Con estos campos, para cada objeto Purchase, puedes determinar si es una compra nueva que debe procesarse o una existente que no necesita procesamiento adicional.

En el caso de las compras de suscripciones, el procesamiento es similar a confirmar la compra nueva.

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. Procesamiento de compras (verificación y confirmación de compras)

Una vez que un usuario completa una compra, la app debe confirmarla para procesarla.

Además, el valor de _isNewPurchaseAcknowledged se establece como verdadero cuando la confirmación se procesa correctamente.

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. Cómo finalizar la conexión de facturación

Por último, cuando se destruye una actividad, quieres finalizar la conexión con Google Play, por lo que se llama a endConnection() para hacerlo.

BillingClientWrapper.kt

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

5. SubscriptionDataRepository

En BillingClientWrapper, las respuestas de QueryPurchasesAsync y QueryProductDetails se publican respectivamente en MutableStateFlow, _purchases y _productWithProductDetails, que se exponen fuera de la clase con compras y productWithProductDetails.

En SubscriptionDataRepository, las compras se procesan en tres flujos según el producto de la compra que se devuelve: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium y hasPremiumPrepaid.

Además, productWithProductDetails se procesa en los flujos basicProductDetails y premiumProductDetails correspondientes.

6. El MainViewModel

Terminó la parte difícil. Ahora, definirás el MainViewModel, que es solo una interfaz pública para tus clientes, de modo que no deban conocer los componentes internos de BillingClientWrapper y SubscriptionDataRepository.

Primero, en MainViewModel, iniciamos la conexión de facturación cuando se inicializa viewModel.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Luego, los flujos del repositorio se combinan respectivamente en productsForSaleFlows (para productos disponibles) y userCurrentSubscriptionFlow (para la suscripción actual y activa del usuario), según se procesan en la clase del repositorio.

La lista de compras actuales también está disponible en la IU con 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

El userCurrentSubscriptionFlow combinado se recopila en un bloque init y el valor se publica en un objeto MutableLiveData llamado _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 también agrega algunos métodos muy útiles:

  1. Planes básicos y recuperación de tokens de oferta

A partir de la versión 5.0.0 de la Biblioteca de Facturación Play, todos los productos de suscripción pueden tener varios planes básicos y ofertas, excepto los planes básicos prepagados que no pueden tener ofertas.

Este método ayuda a recuperar todas las ofertas y los planes básicos para los que un usuario es apto utilizando el nuevo concepto de etiquetas que se utilizan para agrupar las ofertas relacionadas.

Por ejemplo, cuando un usuario intenta comprar una suscripción básica mensual, todas las ofertas y los planes básicos asociados con ese producto se etiquetan con la cadena 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. Cálculo de la oferta con el precio más bajo

Cuando un usuario es apto para varias ofertas, se usa el método leastPricedOfferToken() para calcular la oferta más baja entre las que devuelve retrieveEligibleOffers().

El método muestra el token de ID de la oferta seleccionada.

Esta implementación simplemente muestra las ofertas con el precio más bajo en términos del conjunto pricingPhases y no tiene en cuenta los promedios.

Otra implementación podría ser mirar la oferta con el precio promedio más bajo en su lugar.

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. Compiladores de BillingFlowParams

Para iniciar el flujo de compra de un producto en particular, se puede Se deben configurar s ProductDetails y el token de la oferta seleccionada, y usarse para crear un BilingFlowParams.

Existen dos métodos para ayudar con esto:

upDowngradeBillingFlowParamsBuilder() compila los parámetros para actualizaciones y cambios a versiones inferiores.

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() compila los parámetros para las compras normales.

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. Método de compra

El método de compra usa el launchBillingFlow() de BillingClientWrapper y el BillingFlowParams para iniciar las compras.

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. Cómo finalizar la conexión de facturación

Por último, se llama al método terminateBillingConnection de BillingClientWrapper en un onCleared() de ViewModel.

De esta manera, se finaliza la conexión de facturación actual cuando se destruye la actividad asociada.

7. La IU

Ahora, es el momento de usar todo lo que compilaste en la IU. Para ayudarte con eso, trabajarás con las clases Composables y MainActivity.

Composables.kt

La clase de elementos componibles se proporciona por completo y define todas las funciones de Compose que se usan para renderizar la IU y el mecanismo de navegación entre ellas.

La función Subscriptions muestra dos botones: Basic Subscription y Premium Subscription.

Basic Subscription y Premium Subscription cargan nuevos métodos de Compose que muestran los tres planes básicos correspondientes: mensual, anual y prepagado.

Luego, existen tres funciones posibles de composición de perfiles, cada una para una suscripción específica que puede tener un usuario: un básico renovable, un Premium renovable y un perfil prepagado básico o premium prepagado.

  • Cuando un usuario tiene una suscripción Básica, el perfil básico le permite actualizar a una suscripción premium mensual, anual o prepaga.
  • Por el contrario, cuando un usuario tiene una suscripción premium, puede cambiar a una suscripción básica mensual, anual o prepaga.
  • Cuando un usuario tiene una suscripción prepagada, puede agregar dinero con el botón para agregar dinero o convertir la suscripción prepagada en un plan básico con renovación automática correspondiente.

Por último, hay una función de la pantalla de carga que se usa cuando se establece una conexión con Google Play y cuando se renderiza un perfil de usuario.

MainActivity.kt

  1. Cuando se crea MainActivity, se crea una instancia de viewModel y se carga una función de Compose llamada MainNavHost.
  2. MainNavHost comienza con una variable isBillingConnected creada a partir de los billingConnectionSateLivedata de viewModel y observadas en busca de cambios, ya que cuando se crea una instancia de vieModel, pasa billingConnectionSate al método startBillingConnection de BillingClientWrapper.

isBillingConnected se configura como verdadero cuando se establece la conexión y como falso cuando no se establece.

Cuando es falso, se carga la función de Compose LoadingScreen() y, cuando es verdadero, se cargan las funciones Subscription o de perfil.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Cuando se establece una conexión de facturación:

Se crea una instancia de navController de Compose

val navController = rememberNavController()

Luego, se recopilan los flujos en MainViewModel.

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

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

Por último, se observa la variable LiveData destinationScreen de viewModel.

Según el estado actual de la suscripción del usuario, se renderiza la función de redacción correspondiente.

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. Código de solución

El código completo de la solución se encuentra en el módulo de la solución.

9. Felicitaciones

¡Felicitaciones! Integraste correctamente los productos de suscripción a la Biblioteca de Facturación Google Play 5.0.0 en una app muy simple.

Para ver la versión documentada de una app más sofisticada con una configuración de servidor segura, consulta el ejemplo oficial.

Lecturas adicionales