Vender assinaturas no app com a Biblioteca Play Faturamento 5

1. Introdução

O sistema de faturamento do Google Play é um serviço que permite vender produtos e conteúdos digitais em apps Android. É a maneira mais direta de vender produtos no app para gerar receita. Este codelab mostra como usar a Biblioteca Google Play Faturamento para vender assinaturas no projeto de uma maneira que encapsule os detalhes ao integrar compras com o restante do app.

Ele também apresenta conceitos relacionados a assinaturas, como planos básicos, ofertas, tags e planos pré-pagos. Para saber mais sobre assinaturas no Google Play Faturamento, acesse a Central de Ajuda.

O que você vai criar

Neste codelab, você adicionará a biblioteca de faturamento mais recente (versão 5.0.0) a um app simples de perfil com base em assinatura. Como o app já foi criado para você, basta adicionar a parte de faturamento. Como mostrado na figura 1, neste app, o usuário se inscreve em qualquer um dos planos básicos e/ou nas ofertas oferecidas por dois produtos por assinatura renováveis (básico e premium) ou por um pré-pagamento não renovável. Isso é tudo. Os planos básicos são, respectivamente, assinaturas mensais e anuais. O usuário pode fazer upgrade, downgrade ou converter uma assinatura pré-paga em uma assinatura renovável.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Para incorporar a Biblioteca Google Play Faturamento ao seu app, é necessário criar o seguinte:

  • BillingClientWrapper: um wrapper para a biblioteca BillingClient. Ela pretende encapsular as interações com o BillingClient da biblioteca Play Faturamento, mas isso não é necessário na sua própria integração.
  • SubscriptionDataRepository: um repositório de faturamento do seu app que contém uma lista do inventário de produtos por assinatura (ou seja, que está à venda) e uma lista de variáveis do ShareFlow que ajudam a coletar o estado de compras e detalhes do produto
  • MainViewModel: um ViewModel em que o restante do app se comunica com o repositório de faturamento. É útil iniciar o fluxo de faturamento na IU usando vários métodos de compra.

Quando terminar, a arquitetura do seu app será semelhante à figura abaixo:

c83bc759f32b0a63.png

O que você aprenderá

  • Como integrar a Biblioteca Play Faturamento
  • Como criar produtos por assinatura, planos básicos, ofertas e tags no Play Console
  • Como recuperar planos básicos e ofertas disponíveis no app
  • Como iniciar o fluxo de faturamento com os parâmetros adequados
  • Como oferecer produtos por assinatura pré-pagos

Este codelab é focado no Google Play Faturamento. Conceitos não-relevantes e blocos de código são citados rapidamente e fornecidos para que você simplesmente os copie e cole.

O que será necessário

  • Uma versão recente do Android Studio (>= Arctic Fox | 2020.3.1)
  • Dispositivo com o Android 8.0 ou superior
  • O exemplo de código, fornecido no GitHub (instruções nas seções posteriores)
  • Conhecimento moderado do desenvolvimento para Android no Android Studio
  • Saber como publicar um app na Google Play Store
  • Experiência moderada com programação de código Kotlin.

2. Etapas da configuração

Buscar o código do GitHub

Colocamos tudo o que você precisa para este projeto em um repositório Git. Para começar, será necessário pegar o código e abri-lo no seu ambiente de desenvolvimento favorito. Para este codelab, recomendamos o uso do Android Studio.

O código para começar é armazenado em um repositório do GitHub. Você pode clonar o repositório usando este comando:

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

3. O alicerce

Qual é nosso ponto de partida?

Nosso ponto de partida é um app básico de perfil de usuário criado para este codelab. O código foi simplificado para mostrar os conceitos que queremos ilustrar e não foi projetado para uso em produção. Se você reutilizar qualquer parte desse código em um app de produção, siga as práticas recomendadas e teste todo o código.

Importar o projeto para o Android Studio

O Billing-codelab é o app base que não contém uma implementação do Google Play Faturamento. Inicie o Android Studio e importe o code-billing escolhendo **Open > billing-codelab/build.gradle**.

O projeto tem dois pacotes:

  • O codelab tem o app de estrutura, mas não tem as dependências necessárias e todos os métodos que você precisa implementar.
  • solution apresenta o projeto concluído e pode servir de guia quando você não consegue avançar

O app consiste em sete arquivos de classe: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory e MainActivity.

  • O BillingClientWrapper é um wrapper que isola os métodos [BillingClient] do Google Play Faturamento necessários para ter uma implementação simples e emite respostas ao repositório de dados para processamento.
  • O subscriptionDataRepository é usado para abstrair a fonte de dados do Google Play Faturamento (ou seja, a biblioteca de cliente de faturamento) e converter os dados de StateFlow emitidos em BillingClientWrapper em fluxos.
  • ButtonModel é uma classe de dados usada para criar botões na IU.
  • Os elementos que podem ser compostos extrai todos os métodos de IU que podem ser compostos em uma classe.
  • MainState é uma classe de dados para gerenciamento de estado.
  • MainViewModel é usado para armazenar dados relacionados ao faturamento e estados usados na IU. Ela combina todos os fluxos de SubscriptionDataRepository em um objeto de estado.
  • MainViewModelFactory é uma implementação da interface ViewModelProviders.Factory usada para instanciar o MainViewModel.
  • Constantes são o objeto que tem as constantes usadas por várias classes.
  • MainActivity é a classe de atividade principal que carrega os elementos que podem ser compostos para a interface do usuário.

Gradle

É necessário adicionar uma dependência do Gradle para adicionar o Google Play Faturamento ao seu app. Abra o arquivo build.gradle do módulo do app e adicione o seguinte:

dependencies {
    val billing_version = "5.0.0"

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

Console

Para os fins deste codelab, você precisa criar as duas ofertas de produtos por assinatura no Google Play Console:

  • 1 assinatura básica com o código do produto up_basic_sub

O produto precisa ter três planos básicos (dois com renovação automática e um pré-pago) com tags associadas : uma assinatura básica mensal com a tag monthlybasic, uma assinatura básica anual com a tag yearlybasic e uma assinatura pré-paga com a tag prepaidbasic

Você pode adicionar ofertas aos planos básicos, que herdarão as tags dos planos básicos associados.

  • Uma assinatura premium com o ID do produto up_premium_sub

O produto precisa ter três planos básicos(dois de renovação automática e um pré-pago) com tags associadas: uma assinatura básica mensal com a tag monthlypremium, uma assinatura básica anual com a tag yearlypremium e uma assinatura pré-paga com a tag prepaidpremium

Você pode adicionar ofertas aos planos básicos, que herdarão as tags dos planos básicos associados.

Para informações mais detalhadas sobre como criar produtos por assinatura, planos básicos, ofertas e tags, consulte a Central de Ajuda do Google Play aqui.

4. A configuração do cliente de faturamento

Nesta seção, você trabalhará na classe BillingClientWrapper.

No final, você terá tudo o que é necessário para o cliente de faturamento ser instanciado, além de todos os métodos relacionados.

  1. Inicializar um BillingClient

Depois de adicionar uma dependência à Biblioteca Play Faturamento, é necessário inicializar uma instância de BillingClient.

BillingClientWrapper.kt.

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Estabelecer uma conexão com o Google Play

Após a criação de um BillingClient, é preciso estabelecer uma conexão com o Google Play.

Para se conectar ao Google Play, chamamos startConnection(). O processo de conexão é assíncrono, e precisamos implementar um BillingClientStateListener para receber um callback assim que a configuração do cliente for concluída e estiver pronta para fazer mais solicitações.

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. Consultar o Google Play Faturamento para compras existentes

Depois de estabelecer uma conexão com o Google Play, estamos prontos para consultar as compras que o usuário fez anteriormente chamando 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. Mostrar produtos disponíveis para compra

Agora, podemos consultar os produtos disponíveis e exibi-los aos usuários. Para consultar o Google Play sobre os detalhes do produto por assinatura, chamaremos queryProductDetailsAsync(). Consultar detalhes do produto é uma etapa importante antes de exibir os produtos para os usuários, já que retorna informações localizadas do produto.

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. Definir o listener da consulta ProductDetails

Observação: esse método emite o resultado da consulta em um mapa para _productWithProductDetails.

Observe que a consulta precisa retornar ProductDetails. Se isso não acontecer, é provável que o produto configurado no Play Console não tenha sido ativado ou você não tenha publicado uma versão com a dependência do cliente de faturamento em uma das faixas de lançamento.

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. Iniciar o fluxo de compra

launchBillingFlow é o método chamado quando o usuário clica para comprar um item. Ele solicita que o Google Play inicie o fluxo de compra com o ProductDetails do produto.

BillingClientWrapper.kt.

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

}
  1. Definir o listener do resultado da operação de compra

Quando o usuário sair da tela de compra do Google Play (tocando no botão"Comprar e concluir"ou tocando no botão"Voltar"para cancelar a compra), o callback onPurchaseUpdated() enviará o resultado do fluxo de compra de volta ao seu app. Com base no BillingResult.responseCode, será possível determinar se o usuário comprou o produto. Se responseCode == OK, isso significa que a compra foi concluída.

O onPurchaseUpdated() retorna uma lista de objetos Purchase que inclui todas as compras que o usuário fez no app. Entre muitos outros campos, cada objeto de compra contém os atributos ID do produto, purchaseToken e isAcknowledged. Usando esses campos, para cada objeto Purchase, você pode determinar se é uma nova compra que precisa ser processada ou uma compra existente que não precisa de processamento adicional.

Para compras de assinatura, o processamento é semelhante ao reconhecimento da nova 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. Processamento de compras (verificação e confirmação de compras)

Depois que um usuário conclui uma compra, o app precisa processá-la para confirmar a compra.

Além disso, o valor de _isNewPurchaseAcknowledged é definido como verdadeiro quando a confirmação é processada.

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. Encerrar conexão de faturamento

Por fim, quando uma atividade é destruída, é recomendável encerrar a conexão com o Google Play, para que o endConnection() seja chamado.

BillingClientWrapper.kt.

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

5. O SubscriptionDataRepository

Na BillingClientWrapper, as respostas de QueryPurchasesAsync e QueryProductDetails são postadas, respectivamente, em MutableStateFlow _purchases e _productWithProductDetails, que são expostas fora da classe com compras e productWithProductDetails.

Em SubscriptionDataRepository, as compras são processadas em três fluxos com base no produto de compra retornado: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium e hasPremiumPrepaid.

Além disso, o productWithProductDetails é processado nos respectivos fluxos basicProductDetails e premiumProductDetails.

6. O MainViewModel

A parte mais difícil foi concluída. Agora você definirá a MainViewModel, que é apenas uma interface pública para seus clientes. Assim, eles não precisam conhecer os componentes internos de BillingClientWrapper e SubscriptionDataRepository.

No MainViewModel, iniciamos a conexão de faturamento quando o viewModel é inicializado.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Em seguida, os fluxos do repositório são combinados em productsForSaleFlows (para produtos disponíveis) e userCurrentSubscriptionFlow (para a assinatura atual e ativa do usuário), conforme processado na classe de repositório.

A lista de compras atuais também é disponibilizada para a IU com 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

O userCurrentSubscriptionFlow combinado é coletado em um bloco init, e o valor é postado em um objeto MutableLiveData chamado _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 também adiciona alguns métodos muito úteis:

  1. Recuperação de tokens básicos da oferta e dos planos básicos

A partir da versão 5.0.0 da Biblioteca Play Faturamento, todos os produtos por assinatura podem ter vários planos básicos e ofertas, exceto planos básicos pré-pagos que não podem ter ofertas.

Esse método ajuda a recuperar todas as ofertas e planos básicos em que um usuário se qualifica usando o novo conceito de tags usadas para agrupar ofertas relacionadas.

Por exemplo, quando um usuário tenta comprar uma assinatura básica mensal, todos os planos básicos e ofertas associados ao produto são assinados com a 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 oferta mais barato

Quando um usuário está qualificado para várias ofertas, o método leastPricedOfferToken() é usado para calcular a oferta mais baixa entre as que foram retornadas por retrieveEligibleOffers().

O método retorna o token de ID da oferta selecionada.

Essa implementação retorna as ofertas mais baratas em termos do conjunto de pricingPhases e não considera as médias.

Outra implementação pode ser a oferta com preço médio mais baixo.

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

Para iniciar o fluxo de compra de um produto específico, o ProductDetails do produto e o token da oferta selecionada precisam ser definidos e usados para criar um BilingFlowParams.

Há dois métodos para fazer isso:

upDowngradeBillingFlowParamsBuilder() cria os parâmetros para upgrades e 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() cria os parâmetros para compras normais.

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

O método de compra usa os launchBillingFlow() da BillingClientWrapper e do BillingFlowParams para iniciar 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. Encerrar a conexão de faturamento

Por fim, o método terminateBillingConnection do BillingClientWrapper é chamado no onCleared() do ViewModel.

Isso encerra a conexão de faturamento atual quando a atividade associada é destruída.

7. A IU

Agora, é hora de usar tudo o que você criou na IU. Para ajudar nisso, você trabalhará com as classes que podem ser compostas e a MainActivity.

Composables.kt (link em inglês)

A classe que pode ser composta é totalmente fornecida e define todas as funções do Compose usadas para renderizar a IU e o mecanismo de navegação entre elas.

A função Subscriptions mostra dois botões: Basic Subscription e Premium Subscription.

Basic Subscription e Premium Subscription carregam novos métodos do Compose que mostram os três planos básicos respectivos: mensal, anual e pré-pago.

Depois, há três funções de composição de perfil possíveis para uma assinatura específica que o usuário pode ter: um plano básico renovável, um Premium renovável e um perfil pré-pago básico ou pré-pago premium.

  • Quando um usuário tem a assinatura Basic, o perfil básico permite o upgrade para uma assinatura premium mensal, anual ou pré-paga.
  • Por outro lado, quando um usuário tem uma assinatura premium, ele pode fazer downgrade para uma assinatura básica mensal, anual ou pré-paga.
  • Quando um usuário tiver uma assinatura pré-paga, poderá fazer uma recarga com o botão correspondente ou convertê-la em um plano básico correspondente com renovação automática.

Por fim, há uma função de tela de carregamento que é usada quando a conexão está sendo feita com o Google Play e quando um perfil de usuário está sendo renderizado.

MainActivity.kt

  1. Quando MainActivity é criado, o viewModel é instanciado e uma função de composição chamada MainNavHost é carregada.
  2. O MainNavHost começa com uma variável isBillingConnected criada a partir dos dados em tempo real billingConnectionSate do viewModel& e observado para alterações porque, quando o vieModel é instanciado, ele transmite billingConnectionSate para o método startBillingConnection do BillingClientWrapper.

isBillingConnected é definido como verdadeiro quando a conexão é estabelecida e falso quando não é.

Quando definido como falso, a função de composição LoadingScreen() é carregada. Quando verdadeiro, as funções Subscription ou de perfil são carregadas.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Quando uma conexão de faturamento é estabelecida:

O navController do Compose foi instanciado

val navController = rememberNavController()

Em seguida, os fluxos em MainViewModel são coletados.

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

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

Por fim, a variável LiveData destinationScreen de viewModel's é observada.

Com base no status atual da assinatura do usuário, a função de escrita correspondente é renderizada.

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 da solução

O código completo da solução pode ser encontrado no módulo de soluções.

9. Parabéns

Parabéns! Você integrou os produtos por assinatura da Biblioteca Google Play Faturamento 5.0.0 em um app muito simples.

Para ver a versão documentada de um app mais sofisticado com uma configuração de servidor segura, consulte o exemplo oficial.

Leia mais