Las compras dentro de la aplicación de Android, parte 1: configuración y adición al proyecto
Updated: marzo 20, 2023
Las compras dentro de la aplicación (In-app purchase (IAP)), especialmente las suscripciones (subscriptions), son los métodos más populares para monetizar una aplicación. Por un lado, la suscripción permite al desarrollador invertir en el desarrollo del contenido y del producto, y por otro lado, ayuda a los usuarios a obtener una aplicación (app) de mayor calidad en general. Las compras dentro de la aplicación están sujetas a una comisión del 30%, pero si un usuario ha estado suscrito durante más de un año o una aplicación gana menos de 1$ al año, la comisión es del 15%.
En este artículo, te explicaremos cómo:
- Crear un producto en Google Play Console;
- Configurar las suscripciones: cómo especificar la duración, el precio y las pruebas;
- Añadir una lista de productos a una aplicación.
Crear una suscripción
Antes de empezar, asegúrate de:
- Tener una cuenta de desarrollador en Google Play.
- Haber firmado todos los acuerdos y estar preparados para empezar a trabajar.
Ahora, pongamos manos a la obra y creemos nuestro primer producto.
Cambia a tu cuenta de desarrollador y elige la aplicación.
A continuación, en el menú de la izquierda, busca la sección Productos, selecciona Subscriptions y haz clic en Create a Subscription.
A continuación, veremos el configurador de la suscripción. Estos son algunos puntos importantes.
- Crea el ID que se utilizará en la aplicación. Es una buena idea añadir un periodo de suscripción o alguna otra información útil al ID, así podrás crear productos de un solo estilo y facilitar el análisis de las estadísticas de ventas.
- El nombre de la suscripción que el usuario verá en la tienda.
- Descripción de la suscripción. El usuario también la verá.
Desplázate hacia abajo y elige el periodo de suscripción. En nuestro caso, es una semana. Establece el precio.
Normalmente, estableces el precio en la moneda básica de la cuenta, y el sistema convierte el precio automáticamente. Pero también puedes editar manualmente el precio para un país determinado.
Por favor, ten en cuenta que Google muestra el impuesto para cada país. Es genial, en cambio App Store Connect no lo hace.
Desplázate hacia abajo y elige (si es necesario):
- El periodo de prueba gratis (Free trial).
- El precio de introducción, que es una oferta para los primeros periodos de pago.
- El periodo de gracia (Grace Period). Si un usuario tiene problemas de pago, puedes seguir proporcionándole acceso premium durante cierto número de días.
- Una oportunidad para volver a suscribirse desde Play Store, no desde la aplicación, después de la cancelación.
Comparación del proceso de compra en Play Console y App Store Connect
Independientemente de que las suscripciones se monetizan más eficazmente en iOS, el tablero de administración de Play Console es más cómodo, está mejor organizado y localizado, y funciona más rápido.
El proceso de creación de productos es lo más sencillo posible. Aquí te explicamos cómo crear productos en iOS.
Obtener una lista de productos en una aplicación
Una vez creados los productos, vamos a trabajar en la arquitectura para aceptar y procesar las compras. En general, el proceso tiene el siguiente aspecto:
- Añade una biblioteca de facturación.
- Desarrolla una clase para interactuar con los productos de Google Play.
- Implementa todos los métodos para procesar las compras.
- Añade la validación de una compra (purchase validation) por el servidor.
- Recopila la analítica.
En esta parte, vamos a examinar más detalladamente los dos primeros puntos.
Añadir la Biblioteca de Facturación a un proyecto:
implementation "com.android.billingclient:billing:4.0.0"
En el momento de escribir esto, la última versión es la 4.0.0. Podrás sustituirla por cualquier otra en cualquier momento.
Vamos a crear una clase contenedora que cubra la lógica de interacción con Google Play e inicialice BillingClient desde la Biblioteca de Facturación en la misma. Llamemos a esta clase BillingClientWrapper.
Esta clase implementará la interfaz PurchasesUpdatedListener. Ahora anularemos un método para ello: onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?): es necesario justo después de que se realice una compra, pero describiremos el proceso de implementación en el próximo artículo.
import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
private val billingClient = BillingClient
.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build()
override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
// here come callbacks about new purchases
}
}
Google recomienda evitar tener más de una conexión activa entre BillingClient y Google Play a fin de evitar que una devolución de llamada sobre una compra realizada se ejecute varias veces. Así, deberías tener un único BillingClient en una clase singleton. La clase del ejemplo no es un singleton, pero podemos utilizar la inyección de dependencia (por ejemplo, con la ayuda de Dagger o Koin) de esta forma, permitiendo que sólo exista una instancia en un momento dado.
Para realizar cualquier solicitud con la Biblioteca de Facturación, BillingClient debe tener una conexión activa con Google Play en el momento en que se realiza la solicitud, pero la conexión puede perderse en algún momento. Por comodidad, vamos a escribir una envoltura que nos permita realizar cualquier solicitud sólo cuando la conexión esté activa.
Para obtener los productos, necesitamos sus ID que establecimos en el mercado. Pero no basta con una solicitud, también necesitamos el tipo de producto (suscripciones o compras únicas), por eso podemos obtener una lista general de productos «combinando» los resultados de dos solicitudes.
La solicitud de productos es asíncrona, por lo que necesitamos una devolución de llamada que nos proporcione una lista de productos o devuelva un modelo de error. Al ocurrir un error, la Biblioteca de Facturación devuelve uno de sus BillingResponseCodes, así como el debugMessage. Vamos a crear una interfaz de devolución de llamada y un modelo para un error:
interface OnQueryProductsListener {
fun onSuccess(products: List < SkuDetails > )
fun onFailure(error: Error)
}
class Error(val responseCode: Int, val debugMessage: String)
Aquí está el código de un método privado para obtener datos sobre un tipo específico de productos y un método público que «combinará» los resultados de dos solicitudes y proporcionará al usuario la lista final de productos o mostrará un mensaje de error.
fun queryProducts(listener: OnQueryProductsListener) {
val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
queryProductsForType(
skusList,
BillingClient.SkuType.SUBS
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val products = skuDetailsList ?: mutableListOf()
queryProductsForType(
skusList,
BillingClient.SkuType.INAPP
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
products.addAll(skuDetailsList ?: listOf())
listener.onSuccess(products)
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
}
private fun queryProductsForType(
skusList: List<String>,
@BillingClient.SkuType type: String,
listener: SkuDetailsResponseListener
) {
onConnected {
billingClient.querySkuDetailsAsync(
SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
listener
)
}
}
Así, obtendremos información valiosa sobre los productos (SkuDetails), donde podremos ver los nombres localizados, los precios, el tipo de producto, así como el periodo de facturación y la información sobre el precio de lanzamiento y el periodo de prueba (si está disponible para este usuario) para las suscripciones. Este es el aspecto de la clase final:
import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
interface OnQueryProductsListener {
fun onSuccess(products: List<SkuDetails>)
fun onFailure(error: Error)
}
class Error(val responseCode: Int, val debugMessage: String)
private val billingClient = BillingClient
.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build()
fun queryProducts(listener: OnQueryProductsListener) {
val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
queryProductsForType(
skusList,
BillingClient.SkuType.SUBS
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val products = skuDetailsList ?: mutableListOf()
queryProductsForType(
skusList,
BillingClient.SkuType.INAPP
) { billingResult, skuDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
products.addAll(skuDetailsList ?: listOf())
listener.onSuccess(products)
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
} else {
listener.onFailure(
Error(billingResult.responseCode, billingResult.debugMessage)
)
}
}
}
private fun queryProductsForType(
skusList: List<String>,
@BillingClient.SkuType type: String,
listener: SkuDetailsResponseListener
) {
onConnected {
billingClient.querySkuDetailsAsync(
SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
listener
)
}
}
private fun onConnected(block: () -> Unit) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
block()
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
// here come callbacks about new purchases
}
}
Eso es todo por hoy. En los próximos artículos, te hablaremos de la implementación de la compra, las pruebas y el manejo de errores.