1. Introducción
Para agregar compras directas desde la aplicación a una app de Flutter, debes configurar correctamente App Store y Play Store, verificar la compra y otorgar los permisos necesarios, como los beneficios de suscripción.
En este codelab, agregarás tres tipos de compras directas desde la aplicación a una app (que se te proporcionará) y verificarás estas compras con un backend de Dart con Firebase. La app proporcionada, Dash Clicker, contiene un juego que usa la mascota de Dash como moneda. Agregarás las siguientes opciones de compra:
- Es una opción de compra repetible de 2,000 Dashes a la vez.
- Es una compra de actualización única para convertir el Dash de estilo antiguo en un Dash de estilo moderno.
- Es una suscripción que duplica los clics generados automáticamente.
La primera opción de compra le otorga al usuario un beneficio directo de 2,000 Dashes. Están disponibles directamente para el usuario y se pueden comprar muchas veces. Se denomina consumible porque se consume directamente y se puede consumir varias veces.
La segunda opción actualiza el tablero a uno más atractivo. Solo se debe comprar una vez y está disponible para siempre. Este tipo de compra se denomina no consumible porque la app no la puede consumir, pero es válida para siempre.
La tercera y última opción de compra es una suscripción. Mientras la suscripción esté activa, el usuario obtendrá Dashes más rápido, pero cuando deje de pagar la suscripción, los beneficios también desaparecerán.
El servicio de backend (que también se te proporciona) se ejecuta como una app de Dart, verifica que se realicen las compras y las almacena con Firestore. Firestore se usa para facilitar el proceso, pero en tu app de producción, puedes usar cualquier tipo de servicio de backend.
Qué compilarás
- Extenderás una app para admitir compras y suscripciones de elementos consumibles.
- También extenderás una app de backend de Dart para verificar y almacenar los elementos comprados.
Qué aprenderás
- Cómo configurar App Store y Play Store con productos que se pueden comprar
- Cómo comunicarse con las tiendas para verificar las compras y almacenarlas en Firestore
- Cómo administrar las compras en tu app
Requisitos
- Android Studio
- Xcode (para el desarrollo de iOS)
- SDK de Flutter
2. Cómo configurar el entorno de desarrollo
Para comenzar este codelab, descarga el código y cambia el identificador del paquete para iOS y el nombre del paquete para Android.
Descarga el código
Para clonar el repositorio de GitHub desde la línea de comandos, usa el siguiente comando:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
O bien, si tienes instalada la herramienta de CLI de GitHub, usa el siguiente comando:
gh repo clone flutter/codelabs flutter-codelabs
El código de muestra se clona en un directorio flutter-codelabs
que contiene el código de una colección de codelabs. El código de este codelab se encuentra en flutter-codelabs/in_app_purchases
.
La estructura de directorios en flutter-codelabs/in_app_purchases
contiene una serie de instantáneas de dónde deberías estar al final de cada paso con nombre. El código de partida se encuentra en el paso 0, por lo que debes navegar a él de la siguiente manera:
cd flutter-codelabs/in_app_purchases/step_00
Si quieres adelantar o ver cómo debería verse algo después de un paso, busca en el directorio que lleva el nombre del paso que te interesa. El código del último paso se encuentra en la carpeta complete
.
Configura el proyecto inicial
Abre el proyecto inicial desde step_00/app
en tu IDE favorito. Usamos Android Studio para las capturas de pantalla, pero Visual Studio Code también es una excelente opción. Con cualquiera de los editores, asegúrate de que estén instalados los complementos más recientes de Dart y Flutter.
Las apps que crearás deben comunicarse con App Store y Play Store para saber qué productos están disponibles y a qué precio. Cada app se identifica con un ID único. En la App Store de iOS, se llama identificador de paquete, y en Play Store de Android, ID de aplicación. Por lo general, estos identificadores se crean con una notación de nombre de dominio inverso. Por ejemplo, cuando creas una app de compra directa desde la aplicación para flutter.dev, usarías dev.flutter.inapppurchase
. Piensa en un identificador para tu app. Ahora lo establecerás en la configuración del proyecto.
Primero, configura el identificador de paquete para iOS. Para ello, abre el archivo Runner.xcworkspace
en la app de Xcode.
En la estructura de carpetas de Xcode, el proyecto Runner se encuentra en la parte superior, y los destinos Flutter, Runner y Products se encuentran debajo del proyecto Runner. Haz doble clic en Runner para editar la configuración del proyecto y, luego, haz clic en Signing & Capabilities. Ingresa el identificador del paquete que acabas de elegir en el campo Equipo para establecer tu equipo.
Ahora puedes cerrar Xcode y volver a Android Studio para terminar la configuración de Android. Para ello, abre el archivo build.gradle.kts
en android/app,
y cambia tu applicationId
(en la línea 24 de la captura de pantalla que se muestra a continuación) al ID de la aplicación, que es el mismo que el identificador del paquete de iOS. Ten en cuenta que los IDs de las tiendas de iOS y Android no tienen que ser idénticos, pero mantenerlos idénticos es menos propenso a errores, por lo que en este codelab también usaremos identificadores idénticos.
3. Instala el complemento
En esta parte del codelab, instalarás el complemento in_app_purchase.
Agrega la dependencia en pubspec
Agrega in_app_purchase
al archivo pubspec agregando in_app_purchase
a las dependencias de tu proyecto:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) flutter_lints 5.0.0 (6.0.0 available) + in_app_purchase 3.2.3 + in_app_purchase_android 0.4.0+3 + in_app_purchase_platform_interface 1.4.0 + in_app_purchase_storekit 0.4.4 + json_annotation 4.9.0 lints 5.1.1 (6.0.0 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) provider 6.1.5 (6.1.5+1 available) test_api 0.7.6 (0.7.7 available) Changed 5 dependencies! 7 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Abre tu pubspec.yaml
y confirma que ahora tienes in_app_purchase
como una entrada en dependencies
y in_app_purchase_platform_interface
en dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^6.0.0
cupertino_icons: ^1.0.8
firebase_auth: ^6.0.1
firebase_core: ^4.0.0
google_sign_in: ^7.1.1
http: ^1.5.0
intl: ^0.20.2
provider: ^6.1.5
logging: ^1.3.0
in_app_purchase: ^3.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
4. Configura la App Store
Para configurar las compras directas desde la aplicación y probarlas en iOS, debes crear una app nueva en App Store y crear productos que se puedan comprar allí. No es necesario que publiques nada ni que envíes la app a Apple para su revisión. Para ello, necesitas una cuenta de desarrollador. Si no tienes uno, inscríbete en el programa para desarrolladores de Apple.
Acuerdos de Aplicaciones Pagadas
Para usar las compras directas desde la app, también debes tener un acuerdo activo para las apps pagadas en App Store Connect. Ve a https://appstoreconnect.apple.com/ y haz clic en Acuerdos, impuestos y banca.
Aquí verás los acuerdos de las apps gratuitas y pagadas. El estado de las apps gratuitas debe ser activo, y el de las apps pagadas debe ser nuevo. Asegúrate de ver las condiciones, aceptarlas y completar toda la información obligatoria.
Cuando todo esté configurado correctamente, el estado de las apps pagadas estará activo. Esto es muy importante porque no podrás probar las compras integradas en la app sin un acuerdo activo.
Registrar ID de la app
Crea un identificador nuevo en Apple Developer Portal. Visita developer.apple.com/account/resources/identifiers/list y haz clic en el ícono de signo más junto al encabezado Identifiers.
Elige los IDs de la app
Elegir app
Proporciona una descripción y establece el ID del paquete para que coincida con el mismo valor que se estableció anteriormente en Xcode.
Para obtener más orientación sobre cómo crear un ID de app nuevo, consulta la Ayuda para cuentas de desarrollador.
Cómo crear una app nueva
Crea una app nueva en App Store Connect con tu identificador de paquete único.
Para obtener más orientación sobre cómo crear una app nueva y administrar acuerdos, consulta la Ayuda de App Store Connect.
Para probar las compras directas desde la aplicación, necesitas un usuario de prueba de zona de pruebas. Este usuario de prueba no debe estar conectado a iTunes, ya que solo se usa para probar las compras integradas en la app. No puedes usar una dirección de correo electrónico que ya se use para una cuenta de Apple. En Usuarios y acceso, ve a Sandbox para crear una nueva cuenta de Sandbox o administrar los IDs de Apple de Sandbox existentes.
Ahora puedes configurar tu usuario de espacio aislado en tu iPhone. Para ello, ve a Configuración > Desarrollador > Cuenta de Apple de espacio aislado.
Cómo configurar tus compras directas desde la aplicación
Ahora configurarás los tres elementos comprables:
dash_consumable_2k
: Es una compra consumible que se puede realizar muchas veces y que otorga al usuario 2, 000 Dashes (la moneda integrada en la app) por compra.dash_upgrade_3d
: Es una compra de "actualización" no consumible que solo se puede comprar una vez y le brinda al usuario un Dash cosméticamente diferente para hacer clic.dash_subscription_doubler
: Es una suscripción que otorga al usuario el doble de guiones por clic durante el período de la suscripción.
Ve a Compras directas desde la aplicación.
Crea tus compras directas desde la aplicación con los IDs especificados:
- Configura
dash_consumable_2k
como Consumible. Usadash_consumable_2k
como el ID del producto. El nombre de referencia solo se usa en App Store Connect, así que configúralo comodash consumable 2k
.Configura la disponibilidad. El producto debe estar disponible en el país del usuario de la zona de pruebas.
Agrega precios y establece el precio en
$1.99
o el equivalente en otra moneda.Agrega tus localizaciones para la compra. Llama a la compra
Spring is in the air
con2000 dashes fly out
como descripción.Agrega una captura de pantalla de la opinión. El contenido no importa, a menos que el producto se envíe para su revisión, pero es necesario para que el producto esté en el estado "Listo para enviar", lo que es necesario cuando la app recupera productos de App Store.
- Configura
dash_upgrade_3d
como no consumible. Usadash_upgrade_3d
como el ID del producto. Establece el nombre de referencia comodash upgrade 3d
. Llama a la compra3D Dash
conBrings your dash back to the future
como descripción. Establece el precio en$0.99
. Configura la disponibilidad y sube la captura de pantalla de la revisión de la misma manera que lo hiciste para el productodash_consumable_2k
. - Configura
dash_subscription_doubler
como una suscripción con renovación automática. El flujo de las suscripciones es un poco diferente. Primero, debes crear un grupo de suscripciones. Cuando varias suscripciones forman parte del mismo grupo, el usuario solo puede suscribirse a una de ellas a la vez, pero puede cambiar a una versión superior o inferior entre estas suscripciones. Solo llama a este gruposubscriptions
.Y agrega la localización para el grupo de suscripciones.
A continuación, crearás la suscripción. Establece el nombre de referencia en
dash subscription doubler
y el ID del producto endash_subscription_doubler
.A continuación, selecciona la duración de la suscripción de 1 semana y las localizaciones. Asigna el nombre
Jet Engine
a esta suscripción con la descripciónDoubles your clicks
. Establece el precio en$0.49
. Configura la disponibilidad y sube la captura de pantalla de la revisión de la misma manera que lo hiciste para el productodash_consumable_2k
.
Ahora deberías ver los productos en las listas:
5. Configura Play Store
Al igual que con App Store, también necesitarás una cuenta de desarrollador para Play Store. Si aún no tienes una, regístrate para obtener una cuenta.
Crea una app nueva
Crea una app nueva en Google Play Console:
- Abre Play Console.
- Selecciona Todas las apps > Crear app.
- Selecciona un idioma predeterminado y agrega un título para la app. Escribe el nombre como quieras que se muestre en Google Play. Puedes cambiar el nombre más adelante.
- Especifica que tu aplicación es un juego. Puedes cambiarlo más adelante.
- Especifica si se trata de una aplicación pagada o gratuita.
- Completa las declaraciones de Lineamientos de contenido y Leyes de exportación de EE.UU.
- Selecciona Crear app.
Después de crear la app, ve al panel y completa todas las tareas de la sección Configura tu app. Aquí, proporcionarás información sobre tu app, como las clasificaciones del contenido y las capturas de pantalla.
Firma la aplicación
Para poder probar las compras directas desde la aplicación, debes subir al menos una compilación a Google Play.
Para ello, debes firmar la compilación de lanzamiento con algo que no sean las claves de depuración.
Crea un almacén de claves
Si ya tienes un almacén de claves, avanza al siguiente paso. De lo contrario, crea uno ejecutando lo siguiente en la línea de comandos.
En Mac o Linux, usa el siguiente comando:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
En Windows, usa el siguiente comando:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Este comando almacena el archivo key.jks
en tu directorio principal. Si quieres almacenar el archivo en otro lugar, cambia el argumento que pasas al parámetro -keystore
. Conserva el
keystore
El archivo es privado; no lo registres en el control de código fuente público.
Cómo hacer referencia al almacén de claves desde la app
Crea un archivo llamado <your app dir>/android/key.properties
que contenga una referencia a tu almacén de claves:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
Cómo configurar la firma en Gradle
Edita el archivo <your app dir>/android/app/build.gradle.kts
para configurar la firma de tu app.
Agrega la información del almacén de claves de tu archivo de propiedades antes del bloque android
:
import java.util.Properties
import java.io.FileInputStream
plugins {
// omitted
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Carga el archivo key.properties
en el objeto keystoreProperties
.
Actualiza el bloque buildTypes
de la siguiente manera:
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Configura el bloque signingConfigs
en el archivo build.gradle.kts
de tu módulo con la información de configuración de la firma:
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Ahora se firmarán automáticamente las compilaciones de lanzamiento de tu app.
Para obtener más información sobre cómo firmar tu app, consulta Cómo firmar tu app en developer.android.com.
Sube tu primera compilación
Después de configurar tu app para la firma, deberías poder compilarla ejecutando el siguiente comando:
flutter build appbundle
De forma predeterminada, este comando genera una compilación de lanzamiento, y el resultado se puede encontrar en <your app dir>/build/app/outputs/bundle/release/
.
En el panel de Play Console, ve a Pruebas y lanzamiento > Pruebas > Pruebas cerradas y crea un nuevo lanzamiento de prueba cerrada.
A continuación, sube el paquete de aplicación app-release.aab
que generó el comando de compilación.
Haz clic en Guardar y, luego, en Revisar la versión.
Por último, haz clic en Iniciar lanzamiento en la prueba cerrada para activar la versión de prueba cerrada.
Configura usuarios de prueba
Para poder probar las compras directas desde la aplicación, las Cuentas de Google de tus verificadores deben agregarse en dos ubicaciones de Google Play Console:
- Al segmento de pruebas específico (pruebas internas)
- Como verificador de licencias
Primero, agrega al verificador al segmento de pruebas internas. Regresa a Prueba y lanza > Pruebas > Pruebas internas y haz clic en la pestaña Verificadores.
Haz clic en Crear lista de direcciones de correo electrónico para crear una nueva. Asigna un nombre a la lista y agrega las direcciones de correo electrónico de las Cuentas de Google que necesitan acceso a las pruebas de compras integradas en la aplicación.
A continuación, selecciona la casilla de verificación de la lista y haz clic en Guardar cambios.
Luego, agrega los verificadores de licencias:
- Regresa a la vista Todas las apps de Google Play Console.
- Ve a Configuración > Prueba de licencia.
- Agrega las mismas direcciones de correo electrónico de los verificadores que deben poder probar las compras integradas en la app.
- Establece Respuesta de licencia en
RESPOND_NORMALLY
. - Haga clic en Guardar cambios.
Cómo configurar tus compras directas desde la aplicación
Ahora configurarás los elementos que se pueden comprar en la app.
Al igual que en App Store, debes definir tres compras diferentes:
dash_consumable_2k
: Es una compra consumible que se puede realizar muchas veces y que otorga al usuario 2, 000 Dashes (la moneda integrada en la app) por compra.dash_upgrade_3d
: Es una compra de "actualización" no consumible que solo se puede comprar una vez y que le da al usuario un Dash cosméticamente diferente para hacer clic.dash_subscription_doubler
: Es una suscripción que otorga al usuario el doble de guiones por clic durante el período de la suscripción.
Primero, agrega el producto consumible y el no consumible.
- Ve a Google Play Console y selecciona tu aplicación.
- Ve a Monetización > Productos > Productos integrados en la aplicación.
- Haz clic en Crear producto.
- Ingresa toda la información requerida sobre tu producto. Asegúrate de que el ID de producto coincida exactamente con el ID que deseas usar.
- Haga clic en Guardar.
- Haz clic en Activar.
- Repite el proceso para la compra de "actualización" no consumible.
A continuación, agrega la suscripción:
- Ve a Google Play Console y selecciona tu aplicación.
- Ve a Monetización > Productos > Suscripciones.
- Haz clic en Crear suscripción.
- Ingresa toda la información requerida para tu suscripción. Asegúrate de que el ID de producto coincida exactamente con el ID que deseas usar.
- Haga clic en Guardar.
Tus compras ya deberían estar configuradas en Play Console.
6. Configura Firebase
En este codelab, usarás un servicio de backend para verificar y hacer un seguimiento de las compras de los usuarios.
Usar un servicio de backend tiene varios beneficios:
- Puedes verificar transacciones de forma segura.
- Puedes reaccionar a los eventos de facturación de las tiendas de aplicaciones.
- Puedes hacer un seguimiento de las compras en una base de datos.
- Los usuarios no podrán engañar a tu app para que proporcione funciones premium adelantando el reloj del sistema.
Si bien existen muchas formas de configurar un servicio de backend, lo harás con Cloud Functions y Firestore, usando Firebase de Google.
Escribir el backend se considera fuera del alcance de este codelab, por lo que el código de inicio ya incluye un proyecto de Firebase que controla las compras básicas para que puedas comenzar.
Los complementos de Firebase también se incluyen con la app de inicio.
Lo que te queda por hacer es crear tu propio proyecto de Firebase, configurar la app y el backend para Firebase y, por último, implementar el backend.
Crea un proyecto de Firebase
Ve a Firebase console y crea un proyecto de Firebase nuevo. Para este ejemplo, llama al proyecto Dash Clicker.
En la app de backend, vinculas las compras a un usuario específico, por lo que necesitas autenticación. Para ello, usa el módulo de autenticación de Firebase con el acceso con Google.
- En el panel de Firebase, ve a Authentication y habilítalo, si es necesario.
- Ve a la pestaña Método de acceso y habilita el proveedor de acceso de Google.
Como también usarás la base de datos de Firestore de Firebase, habilita esta opción.
Establece las reglas de Cloud Firestore de la siguiente manera:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
Configura Firebase para Flutter
La forma recomendada de instalar Firebase en la app de Flutter es usar la CLI de FlutterFire. Sigue las instrucciones que se explican en la página de configuración.
Cuando ejecutes flutterfire configure, selecciona el proyecto que acabas de crear en el paso anterior.
$ flutterfire configure i Found 5 Firebase projects. ? Select a Firebase project to configure your Flutter application with › ❯ in-app-purchases-1234 (in-app-purchases-1234) other-flutter-codelab-1 (other-flutter-codelab-1) other-flutter-codelab-2 (other-flutter-codelab-2) other-flutter-codelab-3 (other-flutter-codelab-3) other-flutter-codelab-4 (other-flutter-codelab-4) <create a new project>
A continuación, selecciona las dos plataformas para habilitar iOS y Android.
? Which platforms should your configuration support (use arrow keys & space to select)? › ✔ android ✔ ios macos web
Cuando se te solicite que anules firebase_options.dart, selecciona sí.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Configura Firebase para Android: Pasos adicionales
En el panel de Firebase, ve a Project Overview,elige Settings y selecciona la pestaña General.
Desplázate hacia abajo hasta Tus apps y selecciona la app dashclicker (android).
Para permitir el Acceso con Google en el modo de depuración, debes proporcionar la huella digital del hash SHA-1 de tu certificado de depuración.
Obtén el hash de tu certificado de firma de depuración
En la raíz del proyecto de tu app de Flutter, cambia el directorio a la carpeta android/
y, luego, genera un informe de firma.
cd android ./gradlew :app:signingReport
Verás una lista extensa de claves de firma. Como buscas el hash del certificado de depuración, busca el certificado con las propiedades Variant
y Config
establecidas en debug
. Es probable que el almacén de claves se encuentre en tu carpeta principal en .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
Copia el hash SHA-1 y completa el último campo del diálogo modal de envío de la app.
Por último, vuelve a ejecutar el comando flutterfire configure
para actualizar la app y que incluya la configuración de firma.
$ flutterfire configure ? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes ✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
Configura Firebase para iOS: Pasos adicionales
Abre el archivo ios/Runner.xcworkspace
con Xcode
. También puedes usar el IDE que prefieras.
En VS Code, haz clic con el botón derecho en la carpeta ios/
y, luego, en open in xcode
.
En Android Studio, haz clic con el botón derecho en la carpeta ios/
y, luego, en flutter
seguido de la opción open iOS module in Xcode
.
Para permitir el acceso con Google en iOS, agrega la opción de configuración CFBundleURLTypes
a tus archivos de compilación plist
. (Consulta la documentación del paquete google_sign_in
para obtener más información). En este caso, el archivo es ios/Runner/Info.plist
.
El par clave-valor ya se agregó, pero sus valores deben reemplazarse:
- Obtén el valor de
REVERSED_CLIENT_ID
del archivoGoogleService-Info.plist
, sin el elemento<string>..</string>
que lo rodea. - Reemplaza el valor en tu archivo
ios/Runner/Info.plist
en la claveCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
Ya terminaste con la configuración de Firebase.
7. Cómo escuchar actualizaciones de compras
En esta parte del codelab, prepararás la app para comprar los productos. Este proceso incluye escuchar las actualizaciones y los errores de compra después de que se inicia la app.
Escucha las actualizaciones de compras
En main.dart,
, busca el widget MyHomePage
que tiene un Scaffold
con un BottomNavigationBar
que contiene dos páginas. En esta página, también se crean tres objetos Provider
para DashCounter
, DashUpgrades,
y DashPurchases
. DashCounter
hace un seguimiento del recuento actual de guiones y los incrementa automáticamente. DashUpgrades
administra las actualizaciones que puedes comprar con Dashes. Este codelab se enfoca en DashPurchases
.
De forma predeterminada, el objeto de un proveedor se define cuando se solicita por primera vez. Este objeto escucha las actualizaciones de compras directamente cuando se inicia la app, por lo que debes inhabilitar la carga diferida en este objeto con lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
También necesitas una instancia de InAppPurchaseConnection
. Sin embargo, para que la app se pueda probar, necesitas alguna forma de simular la conexión. Para ello, crea un método de instancia que se pueda anular en la prueba y agrégalo a main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
Actualiza la prueba de la siguiente manera:
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable({
required PurchaseParam purchaseParam,
bool autoConsume = true,
}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(
ProductDetailsResponse(productDetails: [], notFoundIDs: []),
);
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
En lib/logic/dash_purchases.dart
, ve al código de DashPurchasesChangeNotifier
. En este punto, solo hay un DashCounter
que puedes agregar a los Dashes que compraste.
Agrega una propiedad de suscripción a la transmisión, _subscription
(de tipo StreamSubscription<List<PurchaseDetails>> _subscription;
), el IAPConnection.instance,
y las importaciones. El código resultante debería verse de la siguiente manera:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
La palabra clave late
se agrega a _subscription
porque _subscription
se inicializa en el constructor. Este proyecto está configurado para que no acepte valores nulos de forma predeterminada (NNBD), lo que significa que las propiedades que no se declaran como anulables deben tener un valor no nulo. El calificador late
te permite retrasar la definición de este valor.
En el constructor, obtén la transmisión de purchaseUpdated
y comienza a escucharla. En el método dispose()
, cancela la suscripción a la transmisión.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
Ahora, la app recibe las actualizaciones de compra, por lo que, en la siguiente sección, realizarás una compra.
Antes de continuar, ejecuta las pruebas con "flutter test"
para verificar que todo esté configurado correctamente.
$ flutter test 00:01 +1: All tests passed!
8. Realizar compras
En esta parte del codelab, reemplazarás los productos simulados existentes por productos reales que se pueden comprar. Estos productos se cargan desde las tiendas, se muestran en una lista y se compran cuando se presiona el producto.
Adapt PurchasableProduct
PurchasableProduct
muestra un producto simulado. Actualízalo para que muestre contenido real. Para ello, reemplaza la clase PurchasableProduct
en purchasable_product.dart
con el siguiente código:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus { purchasable, purchased, pending }
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
En dash_purchases.dart,
, quita las compras simuladas y reemplázalas por una lista vacía, List<PurchasableProduct> products = [];
.
Carga las compras disponibles
Para permitir que un usuario realice una compra, carga las compras desde la tienda. Primero, verifica si la tienda está disponible. Cuando la tienda no está disponible, configurar storeState
como notAvailable
muestra un mensaje de error al usuario.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Cuando la tienda esté disponible, carga las compras disponibles. Con la configuración anterior de Google Play y App Store, deberías ver storeKeyConsumable
, storeKeySubscription,
y storeKeyUpgrade
. Cuando no esté disponible una compra esperada, imprime esta información en la consola. También puedes enviar esta información al servicio de backend.
El método await iapConnection.queryProductDetails(ids)
devuelve tanto los IDs que no se encontraron como los productos que se pueden comprar que sí se encontraron. Usa el productDetails
de la respuesta para actualizar la IU y establece StoreState
en available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
// ...
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails
.map((e) => PurchasableProduct(e))
.toList();
storeState = StoreState.available;
notifyListeners();
}
Llama a la función loadPurchases()
en el constructor:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases(); // Add this line
}
Por último, cambia el valor del campo storeState
de StoreState.available
a StoreState.loading:
.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Mostrar los productos que se pueden comprar
Considera el archivo purchase_page.dart
. El widget PurchasePage
muestra _PurchasesLoading
, _PurchaseList,
o _PurchasesNotAvailable,
según el valor de StoreState
. El widget también muestra las compras anteriores del usuario, que se usan en el siguiente paso.
El widget _PurchaseList
muestra la lista de productos que se pueden comprar y envía una solicitud de compra al objeto DashPurchases
.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map(
(product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
},
),
)
.toList(),
);
}
}
Si están configurados correctamente, deberías poder ver los productos disponibles en las tiendas de iOS y Android. Ten en cuenta que las compras pueden tardar un tiempo en estar disponibles cuando se ingresan en las consolas respectivas.
Vuelve a dash_purchases.dart
y, luego, implementa la función para comprar un producto. Solo debes separar los consumibles de los no consumibles. La actualización y los productos de suscripción son no consumibles.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails,
'${product.id} is not a known product',
);
}
}
Antes de continuar, crea la variable _beautifiedDashUpgrade
y actualiza el getter beautifiedDash
para que haga referencia a ella.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
El método _onPurchaseUpdate
recibe las actualizaciones de compra, actualiza el estado del producto que se muestra en la página de compra y aplica la compra a la lógica del contador. Es importante llamar a completePurchase
después de procesar la compra para que la tienda sepa que se procesó correctamente.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. Configura el backend
Antes de continuar con el seguimiento y la verificación de las compras, configura un backend de Dart para admitir esta acción.
En esta sección, trabajarás desde la carpeta dart-backend/
como raíz.
Asegúrate de tener instaladas las siguientes herramientas:
- Dart
- Firebase CLI
Descripción general del proyecto base
Dado que algunas partes de este proyecto se consideran fuera del alcance de este codelab, se incluyen en el código de partida. Es una buena idea revisar lo que ya está en el código de partida antes de comenzar, para tener una idea de cómo vas a estructurar las cosas.
Este código de backend se puede ejecutar de forma local en tu máquina, por lo que no es necesario que lo implementes para usarlo. Sin embargo, debes poder conectarte desde tu dispositivo de desarrollo (Android o iPhone) a la máquina en la que se ejecutará el servidor. Para ello, deben estar en la misma red y debes conocer la dirección IP de tu máquina.
Intenta ejecutar el servidor con el siguiente comando:
$ dart ./bin/server.dart Serving at http://0.0.0.0:8080
El backend de Dart usa shelf
y shelf_router
para entregar extremos de API. De forma predeterminada, el servidor no proporciona ninguna ruta. Más adelante, crearás una ruta para controlar el proceso de verificación de la compra.
Una parte que ya se incluye en el código de partida es el IapRepository
en lib/iap_repository.dart
. Dado que aprender a interactuar con Firestore, o con bases de datos en general, no se considera relevante para este codelab, el código de partida contiene funciones para que crees o actualices compras en Firestore, así como todas las clases para esas compras.
Configura el acceso a Firebase
Para acceder a Firebase Firestore, necesitas una clave de acceso a la cuenta de servicio. Para generar una, abre la configuración del proyecto de Firebase, navega a la sección Cuentas de servicio y, luego, selecciona Generar nueva clave privada.
Copia el archivo JSON descargado en la carpeta assets/
y cámbiale el nombre a service-account-firebase.json
.
Configura el acceso a Google Play
Para acceder a Play Store y verificar las compras, debes generar una cuenta de servicio con estos permisos y descargar sus credenciales JSON.
- Visita la página de la API de Google Play Android Developer en la consola de Google Cloud.
En caso de que Google Play Console te solicite que crees un proyecto o que lo vincules a uno existente, hazlo primero y, luego, vuelve a esta página.
- A continuación, ve a la página Cuentas de servicio y haz clic en + Crear cuenta de servicio.
- Ingresa el Nombre de la cuenta de servicio y haz clic en Crear y continuar.
- Selecciona el rol Suscriptor de Pub/Sub y haz clic en Listo.
- Una vez que se cree la cuenta, ve a Administrar claves.
- Selecciona Agregar clave > Crear clave nueva.
- Crea y descarga una clave JSON.
- Cambia el nombre del archivo descargado a
service-account-google-play.json,
y muévelo al directorioassets/
. - A continuación, ve a la página Usuarios y permisos en Play Console.
- Haz clic en Invitar a usuarios nuevos y, luego, ingresa la dirección de correo electrónico de la cuenta de servicio que creaste antes. Puedes encontrar el correo electrónico en la tabla de la página Cuentas de servicio.
- Otorga los permisos Ver datos financieros y Administrar pedidos y suscripciones para la aplicación.
- Haz clic en Invitar a un usuario.
Una cosa más que debemos hacer es abrir lib/constants.dart,
y reemplazar el valor de androidPackageId
por el ID de paquete que elegiste para tu app para Android.
Configura el acceso a la App Store de Apple
Para acceder a App Store y verificar las compras, debes configurar un secreto compartido:
- Abre App Store Connect.
- Ve a Mis apps y selecciona tu app.
- En la navegación de la barra lateral, ve a General > Información de la app.
- Haz clic en Administrar en el encabezado Secreto compartido específico de la app.
- Genera un secreto nuevo y cópialo.
- Abre
lib/constants.dart,
y reemplaza el valor deappStoreSharedSecret
por el secreto compartido que acabas de generar.
Archivo de configuración de constantes
Antes de continuar, asegúrate de que las siguientes constantes estén configuradas en el archivo lib/constants.dart
:
androidPackageId
: Es el ID del paquete que se usa en Android, comocom.example.dashclicker
.appStoreSharedSecret
: Es el secreto compartido para acceder a App Store Connect y realizar la verificación de compras.bundleId
: ID del paquete que se usa en iOS, comocom.example.dashclicker
Por el momento, puedes ignorar el resto de las constantes.
10. Verificar compras
El flujo general para verificar las compras es similar en iOS y Android.
En ambas tiendas, tu aplicación recibe un token cuando se realiza una compra.
La app envía este token a tu servicio de backend, que, a su vez, verifica la compra con los servidores de la tienda correspondiente usando el token proporcionado.
Luego, el servicio de backend puede optar por almacenar la compra y responder a la aplicación si la compra fue válida o no.
Si el servicio de backend realiza la validación con las tiendas en lugar de la aplicación que se ejecuta en el dispositivo del usuario, puedes evitar que el usuario obtenga acceso a las funciones premium, por ejemplo, rebobinando el reloj del sistema.
Configura el lado de Flutter
Configura la autenticación
Como enviarás las compras a tu servicio de backend, debes asegurarte de que el usuario esté autenticado cuando realice una compra. La mayor parte de la lógica de autenticación ya se agregó en el proyecto de inicio. Solo debes asegurarte de que PurchasePage
muestre el botón de acceso cuando el usuario aún no haya accedido. Agrega el siguiente código al principio del método de compilación de PurchasePage
:
lib/pages/purchase_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart'; // Add this import
import '../model/firebase_state.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart'; // And this one as well
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) { // Update from here
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
} // To here.
// ...
Llama al extremo de verificación desde la app
En la app, crea la función _verifyPurchase(PurchaseDetails purchaseDetails)
que llama al extremo /verifypurchase
en tu backend de Dart con una llamada POST de HTTP.
Envía la tienda seleccionada (google_play
para Play Store o app_store
para App Store), el serverVerificationData
y el productID
. El servidor devuelve un código de estado que indica si se verificó la compra.
En las constantes de la app, configura la IP del servidor con la dirección IP de tu máquina local.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'dart:convert'; // Add this import
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // And this import
import 'package:in_app_purchase/in_app_purchase.dart';
import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart'; // And this one
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier; // Add this line
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter, this.firebaseNotifier) { // Update this line
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Agrega firebaseNotifier
con la creación de DashPurchases
en main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Agrega un getter para el usuario en FirebaseNotifier, de modo que puedas pasar el ID del usuario a la función de verificación de compra.
lib/logic/firebase_notifier.dart
Future<FirebaseFirestore> get firestore async {
var isInitialized = await _isInitialized.future;
if (!isInitialized) {
throw Exception('Firebase is not initialized');
}
return FirebaseFirestore.instance;
}
User? get user => FirebaseAuth.instance.currentUser; // Add this line
Future<void> load() async {
// ...
Agrega la función _verifyPurchase
a la clase DashPurchases
. Esta función async
devuelve un valor booleano que indica si se validó la compra.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
Llama a la función _verifyPurchase
en _handlePurchase
justo antes de aplicar la compra. Solo debes aplicar la compra cuando se verifique. En una app de producción, puedes especificar esto aún más, por ejemplo, para aplicar una suscripción de prueba cuando la tienda no esté disponible temporalmente. Sin embargo, para este ejemplo, aplica la compra cuando se verifique correctamente.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
En la app, todo está listo para validar las compras.
Configura el servicio de backend
A continuación, configura el backend para verificar las compras en el backend.
Compila controladores de compra
Dado que el flujo de verificación de ambas tiendas es casi idéntico, configura una clase abstracta PurchaseHandler
con implementaciones separadas para cada tienda.
Comienza por agregar un archivo purchase_handler.dart
a la carpeta lib/
, en el que definirás una clase PurchaseHandler
abstracta con dos métodos abstractos para verificar dos tipos diferentes de compras: suscripciones y compras que no son suscripciones.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
Como puedes ver, cada método requiere tres parámetros:
userId:
Es el ID del usuario que accedió, para que puedas vincular las compras al usuario.productData:
Datos sobre el producto. Definirás esto en un minuto.token:
Es el token que la tienda le proporciona al usuario.
Además, para que estos controladores de compra sean más fáciles de usar, agrega un método verifyPurchase()
que se pueda usar tanto para suscripciones como para productos que no sean suscripciones:
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
Ahora, puedes llamar a verifyPurchase
para ambos casos, pero seguir teniendo implementaciones separadas.
La clase ProductData
contiene información básica sobre los diferentes productos que se pueden comprar, lo que incluye el ID del producto (a veces también denominado SKU) y el ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
El objeto ProductType
puede ser una suscripción o no.
lib/products.dart
enum ProductType { subscription, nonSubscription }
Por último, la lista de productos se define como un mapa en el mismo archivo.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
A continuación, define algunas implementaciones de marcadores de posición para Google Play Store y Apple App Store. Comienza con Google Play:
Crea lib/google_play_purchase_handler.dart
y agrega una clase que extienda el PurchaseHandler
que acabas de escribir:
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(this.androidPublisher, this.iapRepository);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Por ahora, devuelve true
para los métodos del controlador. Los verás más adelante.
Como habrás notado, el constructor toma una instancia de IapRepository
. El controlador de compras usa esta instancia para almacenar información sobre las compras en Firestore más adelante. Para comunicarte con Google Play, usa el AndroidPublisherApi
proporcionado.
A continuación, haz lo mismo con el controlador de la tienda de aplicaciones. Crea lib/app_store_purchase_handler.dart
y agrega una clase que extienda PurchaseHandler
nuevamente:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(this.iapRepository);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
¡Genial! Ahora tienes dos controladores de compra. A continuación, crea el extremo de la API de verificación de compras.
Usa controladores de compra
Abre bin/server.dart
y crea un extremo de API con shelf_route
:
bin/server.dart
import 'dart:convert';
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
if (payload case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
El código hace lo siguiente:
- Define un extremo POST al que se llamará desde la app que creaste anteriormente.
- Decodifica la carga útil de JSON y extrae la siguiente información:
userId
: ID del usuario que accediósource
: Es la tienda que se usó, ya seaapp_store
ogoogle_play
.productData
: Se obtiene del objetoproductDataMap
que creaste anteriormente.token
: Contiene los datos de verificación que se enviarán a las tiendas.
- Llamada al método
verifyPurchase
, ya sea paraGooglePlayPurchaseHandler
oAppStorePurchaseHandler
, según la fuente. - Si la verificación se realizó correctamente, el método devuelve un
Response.ok
al cliente. - Si la verificación falla, el método devuelve un
Response.internalServerError
al cliente.
Después de crear el extremo de la API, debes configurar los dos controladores de compra. Para ello, debes cargar las claves de la cuenta de servicio que obtuviste en el paso anterior y configurar el acceso a los diferentes servicios, incluidas la API de Android Publisher y la API de Firebase Firestore. Luego, crea los dos controladores de compra con las diferentes dependencias:
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Verifica las compras en Android: Implementa el controlador de compras
A continuación, sigue implementando el controlador de compras de Google Play.
Google ya proporciona paquetes de Dart para interactuar con las APIs que necesitas para verificar compras. Los inicializaste en el archivo server.dart
y ahora los usas en la clase GooglePlayPurchaseHandler
.
Implementa el controlador para las compras que no son de tipo suscripción:
lib/google_play_purchase_handler.dart
/// Handle non-subscription purchases (one time purchases).
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
Puedes actualizar el controlador de compra de suscripciones de manera similar:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
Agrega el siguiente método para facilitar el análisis de los IDs de pedidos, así como dos métodos para analizar el estado de la compra.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
Ahora se deberían verificar tus compras de Google Play y almacenarse en la base de datos.
A continuación, pasa a las compras en App Store para iOS.
Verifica las compras en iOS: Implementa el controlador de compras
Para verificar las compras con App Store, existe un paquete de Dart de terceros llamado app_store_server_sdk
que facilita el proceso.
Comienza por crear la instancia ITunesApi
. Usa la configuración de zona de pruebas y habilita el registro para facilitar la depuración de errores.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(ITunesEnvironment.sandbox(), loggingEnabled: true),
);
Ahora, a diferencia de las APIs de Google Play, App Store usa los mismos extremos de API para las suscripciones y los productos que no son de suscripción. Esto significa que puedes usar la misma lógica para ambos controladores. Combínalos para que llamen a la misma implementación:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
// See next step
}
Ahora, implementa handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(
NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0'),
),
type: product.type,
status: NonSubscriptionStatus.completed,
),
);
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(
SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0'),
),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0'),
),
status: SubscriptionStatus.active,
),
);
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
Tus compras en App Store ahora deberían estar verificadas y almacenadas en la base de datos.
Ejecuta el backend
En este punto, puedes ejecutar dart bin/server.dart
para entregar el extremo /verifypurchase
.
$ dart bin/server.dart Serving at http://0.0.0.0:8080
11. Haz un seguimiento de las compras
La forma recomendada de hacer un seguimiento de las compras de los usuarios es en el servicio de backend. Esto se debe a que tu backend puede responder a los eventos de la tienda y, por lo tanto, es menos propenso a tener información desactualizada debido al almacenamiento en caché, además de ser menos susceptible a la manipulación.
Primero, configura el procesamiento de eventos de la tienda en el backend con el backend de Dart que has estado compilando.
Procesa eventos de la tienda en el backend
Las tiendas pueden informar a tu backend sobre cualquier evento de facturación que ocurra, como cuando se renuevan las suscripciones. Puedes procesar estos eventos en tu backend para mantener actualizadas las compras en tu base de datos. En esta sección, configura esta opción para Google Play Store y App Store de Apple.
Procesa eventos de facturación de Google Play
Google Play proporciona eventos de facturación a través de lo que se denomina un tema de Cloud Pub/Sub. Básicamente, son colas de mensajes en las que se pueden publicar mensajes y desde las que se pueden consumir.
Como esta funcionalidad es específica de Google Play, la incluyes en GooglePlayPurchaseHandler
.
Para comenzar, abre lib/google_play_purchase_handler.dart
y agrega la importación de PubsubApi
:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Luego, pasa el PubsubApi
al GooglePlayPurchaseHandler
y modifica el constructor de la clase para crear un Timer
de la siguiente manera:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
El Timer
está configurado para llamar al método _pullMessageFromPubSub
cada diez segundos. Puedes ajustar la duración según tus preferencias.
Luego, crea el _pullMessageFromPubSub
.
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(maxMessages: 1000);
final topicName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(ackIds: [id]);
final subscriptionName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
El código que acabas de agregar se comunica con el tema de Pub/Sub de Google Cloud cada diez segundos y solicita mensajes nuevos. Luego, procesa cada mensaje en el método _processMessage
.
Este método decodifica los mensajes entrantes y obtiene la información actualizada sobre cada compra, tanto de suscripciones como de compras que no son de suscripciones, y llama a los métodos handleSubscription
o handleNonSubscription
existentes si es necesario.
Cada mensaje debe confirmarse con el método _askMessage
.
A continuación, agrega las dependencias necesarias al archivo server.dart
. Agrega PubsubApi.cloudPlatformScope a la configuración de credenciales:
bin/server.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub; // Add this import
final clientGooglePlay = await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // Add this line
]);
Luego, crea la instancia de PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Por último, pásalo al constructor GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // Add this line
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Configuración de Google Play
Escribiste el código para consumir eventos de facturación del tema de Pub/Sub, pero no creaste el tema de Pub/Sub ni publicaste ningún evento de facturación. Es hora de configurarlo.
Primero, crea un tema de Pub/Sub:
- Establece el valor de
googleCloudProjectId
enconstants.dart
como el ID de tu proyecto de Google Cloud. - Visita la página de Cloud Pub/Sub en la consola de Google Cloud.
- Asegúrate de estar en tu proyecto de Firebase y haz clic en + Create Topic.
- Asigna un nombre al tema nuevo, que sea idéntico al valor establecido para
googlePlayPubsubBillingTopic
enconstants.dart
. En este caso, asígnale el nombreplay_billing
. Si eliges otra opción, asegúrate de actualizarconstants.dart
. Crea el tema. - En la lista de tus temas de Pub/Sub, haz clic en los tres puntos verticales del tema que acabas de crear y, luego, en Ver permisos.
- En la barra lateral de la derecha, elige Agregar principal.
- Aquí, agrega
google-play-developer-notifications@system.gserviceaccount.com
y otórgale el rol de Publicador de Pub/Sub. - Guarda los cambios en los permisos.
- Copia el Nombre del tema del tema que acabas de crear.
- Vuelve a abrir Play Console y elige tu app en la lista Todas las apps.
- Desplázate hacia abajo y ve a Monetizar > Configuración de la monetización.
- Completa el tema completo y guarda los cambios.
Todos los eventos de facturación de Google Play ahora se publicarán en el tema.
Procesa eventos de facturación de App Store
A continuación, haz lo mismo con los eventos de facturación de App Store. Existen dos formas eficaces de implementar el control de actualizaciones en las compras de App Store. Una es implementar un webhook que le proporcionas a Apple y que este usa para comunicarse con tu servidor. La segunda forma, que es la que encontrarás en este codelab, es conectándote a la API de App Store Server y obteniendo la información de la suscripción de forma manual.
El motivo por el que este codelab se enfoca en la segunda solución es que tendrías que exponer tu servidor a Internet para implementar el webhook.
En un entorno de producción, lo ideal sería tener ambos. El webhook para obtener eventos de App Store y la API del servidor en caso de que te hayas perdido un evento o necesites verificar el estado de una suscripción.
Para comenzar, abre lib/app_store_purchase_handler.dart
y agrega la dependencia AppStoreServerAPI
:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI; // Add this member
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // And this parameter
);
Modifica el constructor para agregar un temporizador que llame al método _pullStatus
. Este temporizador llamará al método _pullStatus
cada 10 segundos. Puedes ajustar la duración de este temporizador según tus necesidades.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Luego, crea el método _pullStatus
de la siguiente manera:
lib/app_store_purchase_handler.dart
/// Request the App Store for the latest subscription status.
/// Updates all App Store subscriptions in the database.
/// NOTE: This code only handles when a subscription expires as example.
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where(
(element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore,
);
for (final purchase in appStoreSubscriptions) {
final status = await appStoreServerAPI.getAllSubscriptionStatuses(
purchase.orderId,
);
// Obtain all subscriptions for the order ID.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0,
);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(
SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate,
),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
),
);
}
}
}
}
Este método funciona de la siguiente manera:
- Obtiene la lista de suscripciones activas de Firestore con IapRepository.
- Para cada pedido, solicita el estado de la suscripción a la API del servidor de App Store.
- Obtiene la última transacción de la compra de esa suscripción.
- Verifica la fecha de vencimiento.
- Actualiza el estado de la suscripción en Firestore. Si venció, se marcará como tal.
Por último, agrega todo el código necesario para configurar el acceso a la API de App Store Server:
bin/server.dart
import 'package:app_store_server_sdk/app_store_server_sdk.dart'; // Add this import
import 'package:firebase_backend_dart/constants.dart'; // And this one.
// add from here
final subscriptionKeyAppStore = File(
'assets/SubscriptionKey.p8',
).readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // Add this argument
),
};
Configuración de App Store
A continuación, configura App Store:
- Accede a App Store Connect y selecciona Usuarios y acceso.
- Ve a Integraciones > Claves > Compra integrada en la aplicación.
- Presiona el ícono de signo más para agregar uno nuevo.
- Asigna un nombre, como "Clave de codelab".
- Descarga el archivo .p8 que contiene la clave.
- Cópialo en la carpeta de recursos con el nombre
SubscriptionKey.p8
. - Copia el ID de la clave recién creada y configúralo como la constante
appStoreKeyId
en el archivolib/constants.dart
. - Copia el ID del emisor que se encuentra en la parte superior de la lista de claves y configúralo como la constante
appStoreIssuerId
en el archivolib/constants.dart
.
Realiza un seguimiento de las compras en el dispositivo
La forma más segura de hacer un seguimiento de tus compras es del lado del servidor, ya que el cliente es difícil de proteger, pero necesitas alguna forma de devolver la información al cliente para que la app pueda actuar según la información del estado de la suscripción. Si almacenas las compras en Firestore, puedes sincronizar los datos con el cliente y mantenerlos actualizados automáticamente.
Ya incluiste IAPRepo en la app, que es el repositorio de Firestore que contiene todos los datos de compra del usuario en List<PastPurchase> purchases
. El repositorio también contiene hasActiveSubscription,
, que es verdadero cuando hay una compra con productId storeKeySubscription
con un estado que no venció. Cuando el usuario no accedió, la lista está vacía.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any(
(element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired,
);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
Toda la lógica de compra se encuentra en la clase DashPurchases
, que es donde se deben aplicar o quitar las suscripciones. Por lo tanto, agrega iapRepo
como una propiedad en la clase y asigna iapRepo
en el constructor. A continuación, agrega un objeto de escucha directamente en el constructor y quita el objeto de escucha en el método dispose()
. Al principio, el objeto de escucha puede ser solo una función vacía. Como IAPRepo
es un ChangeNotifier
y llamas a notifyListeners()
cada vez que cambian las compras en Firestore, siempre se llama al método purchasesUpdate()
cuando cambian los productos comprados.
lib/logic/dash_purchases.dart
import '../repo/iap_repo.dart'; // Add this import
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier;
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
IAPRepo iapRepo; // Add this line
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
// Add this.iapRepo as a parameter
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
Future<void> loadPurchases() async {
// Elided.
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate); // Add this line
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
A continuación, proporciona el IAPRepo
al constructor en main.dart.
. Puedes obtener el repositorio con context.read
porque ya se creó en un Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(), // Add this line
),
lazy: false,
),
A continuación, escribe el código para la función purchaseUpdate()
. En dash_counter.dart,
, los métodos applyPaidMultiplier
y removePaidMultiplier
establecen el multiplicador en 10 o 1, respectivamente, por lo que no tienes que verificar si ya se aplicó la suscripción. Cuando cambia el estado de la suscripción, también actualizas el estado del producto que se puede comprar para mostrar en la página de compra que ya está activo. Establece la propiedad _beautifiedDashUpgrade
según si se compra la actualización.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable,
);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
Ahora te aseguraste de que el estado de la suscripción y la actualización esté siempre actualizado en el servicio de backend y sincronizado con la app. La app actúa en consecuencia y aplica las funciones de suscripción y actualización a tu juego de clics de Dash.
12. Todo listo
¡Felicitaciones! Completaste el codelab. Puedes encontrar el código completo de este codelab en la carpeta complete.
Para obtener más información, prueba los otros codelabs de Flutter.