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 contenido y productos digitales en tu app para Android. Es la forma más directa para que vendas productos integrados en la aplicación a fin de 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 un modo que encapsule los detalles fundamentales cuando integras compras en el resto de la app.

También se incluyen conceptos relacionados con las suscripciones, como planes básicos, ofertas, etiquetas y planes prepagados. Para obtener más información sobre las suscripciones en la 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 simple basada en suscripciones. La app ya se creó para ti, así que solo deberás agregar 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 mediante dos productos de suscripción renovables (básica y Premium) o con un plan prepago no renovable. Eso es todo. Los planes básicos son las suscripciones mensuales y anuales. El usuario puede pasar a una versión anterior, pasar a una versión anterior o convertir una suscripción prepagada en una renovable.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

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

  • BillingClientWrapper: Es un wrapper para la biblioteca BillingClient. Su propósito es encapsular las interacciones con BillingClient de la Biblioteca de Facturación Play, pero no es necesario en su propia integración.
  • SubscriptionDataRepository: Un repositorio de facturación para tu app que contiene una lista del inventario de productos de suscripción (es decir, lo que está a la venta) y una lista de variables de ShareFlow que ayudan a recopilar el estado de las compras y los detalles del producto
  • MainViewModel: Es un ViewModel mediante el 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 de la siguiente imagen:

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 mediante 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 copie y pegue.

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 (instrucciones en las secciones posteriores)
  • Conocimientos moderados sobre el desarrollo de Android en Android Studio
  • Conocimientos sobre cómo publicar una app en Google Play Store
  • Experiencia moderada a la escritura de código Kotlin

2. Cómo prepararte

Obtén el código en GitHub

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

El código para comenzar se almacena en un repositorio de GitHub. Puedes clonar el repositorio con el siguiente comando:

git clone https://github.com/android/play-billing-codelabs

3. La base

¿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 usarse en producción. Si decides reutilizar alguna 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.

Cómo importar el proyecto a Android Studio

Billing-codelab es la app de base que no incluye una implementación de la Facturación Google Play. Para iniciar Android Studio y, luego, importar el codelab de facturación, elige **Open > billing-codelab/build.gradle**.

El proyecto tiene dos paquetes:

  • codelab tiene la app de esqueleto, pero no tiene las dependencias necesarias ni todos los métodos que debes implementar.
  • solution tiene el proyecto completo y puede servir como guía cuando tienes problemas

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

  • BillingClientWrapper es un wrapper que aísla los métodos de [BillingClient] de Facturación Google Play necesarios para una implementación simple y emite respuestas para el procesamiento del repositorio de datos.
  • SubscriptionDataRepository se usa para abstraer la fuente de datos de 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.
  • Elementos que admiten composición extrae todos los métodos de la IU que admiten composición en una clase.
  • MainState es una clase de datos para la administración de estados.
  • MainViewModel se usa para conservar datos y estados relacionados con la facturación que se usan en la IU. Combina todos los flujos en subscriptionDataRepository en un solo objeto de estado.
  • MainViewModelFactory es una implementación de la interfaz ViewModelProviders.Factory que se usa para crear una instancia de MainViewModel.
  • Las constantes son el objeto que tiene las constantes que usan varias clases.
  • MainActivity es la clase de actividad principal que carga los elementos componibles para la interfaz de usuario.

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")
}

Consola

A los fines de este codelab, deberás crear las siguientes dos ofertas de productos de suscripción en Google Play Console:

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

El producto debe tener 3 planes básicos (2 con renovación automática y 1 prepago) con etiquetas asociadas : 1 suscripción básica por mes con la etiqueta monthlybasic, 1 suscripción básica anual 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 de producto up_premium_sub

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

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

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

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

Al final, tendrás todo lo necesario para que se creen instancias del cliente de facturación y todos los métodos relacionados.

  1. Inicializa un BillingClient

Una vez que agregamos una dependencia a 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. Establece una conexión con Google Play

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

Para conectarnos a Google Play, llamamos a startConnection(). El proceso de conexión es asíncrono, y debemos 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. Consulta las compras existentes en la Facturación Google Play

Después de establecer una conexión con Google Play, estamos listos para consultar las compras que el usuario realizó 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. Muestra los productos disponibles para comprar

Ahora podemos buscar los productos disponibles y mostrarlos a los usuarios. Para consultar los detalles del producto de suscripción en Google Play, llamaremos al queryProductDetailsAsync(). La consulta de los detalles de productos es un paso importante antes de mostrar los productos a los usuarios, ya que muestra 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. Configura el objeto de escucha para la consulta ProductDetails

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

Además, ten en cuenta que se espera que la búsqueda muestre ProductDetails. Si esto no sucede, es muy probable que los productos configurados en Play Console no estén activados o que no hayas publicado una compilación con la dependencia del 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. Cómo iniciar el flujo de compra

launchBillingFlow es el método al que se llama cuando el usuario hace clic para comprar un artículo. Le pide 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 para el 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 presionando el botón Atrás para cancelar la compra), la devolución de llamada de onPurchaseUpdated() envía el resultado del flujo de compra a la app. Según el BillingResult.responseCode, puedes determinar si el usuario compró el producto correctamente. Si es responseCode == OK, eso 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. Mediante estos campos, para cada objeto Purchase, puedes determinar si se trata de una compra nueva que se debe procesar o una compra existente que no necesita procesamiento adicional.

En el caso de las compras de suscripciones, el procesamiento es similar al de la confirmación de la nueva compra.

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 (verificar y confirmar compras)

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

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. Finaliza 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().

BillingClientWrapper.kt.

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

5. Suscripción de datos de suscripción

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 compra devuelto: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium y hasPremiumPrepaid.

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

6. MainViewModel

Ya terminamos con todo. Ahora, definirás la MainViewModel, que es solo una interfaz pública para tus clientes, de modo que no tengan que conocer los componentes internos de BillingClientWrapper y SubscriptionDataRepository.

Primero, en MainViewModel, iniciamos la conexión de facturación cuando se inicializa el 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), como se procesa 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. Recuperación de tokens de ofertas y planes básicos

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

Este método ayuda a recuperar todas las ofertas y los planes básicos a los que puede acceder un usuario mediante el concepto recién introducido de las etiquetas que se usan para agrupar las ofertas relacionadas.

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

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

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

Esta implementación simplemente muestra las ofertas con los precios más bajos en función del conjunto pricingPhases y no tiene en cuenta los promedios.

Otra implementación podría ser considerar la oferta de precio promedio más baja.

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 específico, se deben configurar el ProductDetails del producto y el token de oferta seleccionado a fin de compilar un BilingFlowParams.

Hay dos métodos que pueden ser de ayuda:

upDowngradeBillingFlowParamsBuilder() compila los parámetros para cambiar a una versión inferior o inferior.

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 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 utiliza BillingClientWrapper& launchBillingFlow() y BillingFlowParams para lanzar 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. Finaliza la conexión de facturación

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

Esto es para finalizar la conexión de facturación actual cuando se destruye la actividad asociada.

7. La IU

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

Elementos que admiten composición.kt

Se proporcionó por completo la clase que admite composición y define todas las funciones de Compose utilizadas 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 métodos de Compose nuevos que muestran los tres planes básicos respectivos: mensual, anual y prepago.

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

  • Cuando un usuario tiene una suscripción básica, el perfil básico le permite actualizarse a una suscripción premium mensual, anual o prepaga.
  • En cambio, cuando un usuario tiene una suscripción premium, puede cambiarse a una suscripción básica mensual, anual o prepaga.
  • Cuando un usuario tiene una suscripción prepagada, puede agregar su botón de recarga o agregar su suscripción prepagada al plan básico correspondiente con renovación automática.

Por último, hay una función de pantalla de carga que se usa cuando se establece la conexión con Google Play y 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 redacción llamada MainNavHost.
  2. MainNavHost comienza con una variable isBillingConnected creada a partir de los datos en vivo billingConnectionSate de modelmodel y se observa para detectar cambios porque, cuando se crea una instancia de vieModel, pasa billingConnectionSate al método startBillingConnection de BillingClientWrapper.

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

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

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

Se crean instancias 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 destinationScreen de LiveData de viewModel.

Según la condición de suscripción actual del usuario, se procesa 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 puede encontrar en el módulo de la solución.

9. Felicitaciones

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

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

Lecturas adicionales