1. Introducción
Última actualización: 11/07/2023
Para agregar compras directas desde la aplicación a una app creada con Flutter, debes configurar correctamente las App Store y Play Store, verificar la compra y otorgar los permisos necesarios, como los beneficios de la suscripción.
En este codelab, agregarás tres tipos de compras directas desde la aplicación (proporcionadas para ti) y verificarás estas compras mediante 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 guiones a la vez.
- Una compra de mejora única para convertir el estilo antiguo de Dash en un Dash de estilo moderno
- 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 guiones. Están disponibles directamente para el usuario y se pueden comprar muchas veces. Se denomina producto consumible, ya que se consume directamente y se puede utilizar varias veces.
La segunda opción mejora a Dash para que sea más atractivo. Solo se debe comprar una vez y está disponible para siempre. Este tipo de compra se denomina no consumible porque la aplicación no puede consumirlo, 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 proporcionó) 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 consumibles.
- También extenderás una app de backend de Dart para verificar y almacenar los artículos comprados.
Qué aprenderás
- Cómo configurar la 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 4.1 o versiones posteriores
- Xcode 12 o versiones posteriores (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 de 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 está en flutter-codelabs/in_app_purchases
.
La estructura de directorios en flutter-codelabs/in_app_purchases
contiene una serie de instantáneas de la ubicación que deberías encontrar al final de cada paso con nombre. El código de partida se encuentra en el paso 0, por lo que ubicar los archivos coincidentes es tan fácil como se muestra a continuación:
cd flutter-codelabs/in_app_purchases/step_00
Si deseas 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 de step_00
en tu IDE favorito. Usamos Android Studio para las capturas de pantalla, pero Visual Studio Code también es una gran opción. Con cualquiera de los dos editores, asegúrate de que estén instalados los complementos más recientes de Dart y Flutter.
Las apps que vas a crear 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. Para la App Store de iOS, esto se denomina identificador de paquete y para Play Store de Android es el ID de aplicación. Por lo general, estos identificadores se realizan con una notación de nombre de dominio inverso. Por ejemplo, cuando se crea una app de compra directa desde la aplicación para flutter.dev, usamos dev.flutter.inapppurchase
. Piensa en un identificador para tu aplicación; ahora lo vas a establecer en la configuración del proyecto.
Primero, configura el identificador de paquete para iOS.
Con el proyecto abierto en Android Studio, haz clic con el botón derecho en la carpeta de iOS, selecciona Flutter y abre el módulo en la app de Xcode.
En la estructura de carpetas de Xcode, Runner project está en la parte superior y los destinos Flutter, Runner y Products están debajo del proyecto de Runner. Haz doble clic en Runner para editar la configuración del proyecto y, luego, haz clic en Signing & Funciones Ingresa el identificador de paquete que acabas de elegir en el campo Equipo para configurar tu equipo.
Ahora puedes cerrar Xcode y volver a Android Studio para finalizar la configuración para Android. Para ello, abre el archivo build.gradle
en android/app,
y cambia tu applicationId
(en la línea 37 de la captura de pantalla a continuación) al ID de aplicación, al igual que el identificador de paquete de iOS. Ten en cuenta que no es necesario que los IDs de las tiendas de iOS y Android sean idénticos. Sin embargo, mantenerlos iguales es menos propenso a errores y, por lo tanto, 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.
Cómo agregar una dependencia en pubspec
Para agregar in_app_purchase
a pubspec, agrega in_app_purchase
a las dependencias de tu pubspec:
$ cd app $ flutter pub add in_app_purchase
pubspec.yaml
dependencies:
..
cloud_firestore: ^4.0.3
firebase_auth: ^4.2.2
firebase_core: ^2.5.0
google_sign_in: ^6.0.1
http: ^0.13.4
in_app_purchase: ^3.0.1
intl: ^0.18.0
provider: ^6.0.2
..
Haz clic en pub get para descargar el paquete o ejecutar flutter pub get
en la línea de comandos.
4. Configura la App Store
Para configurar compras directas desde la aplicación y probarlas en iOS, debes crear una nueva app en la App Store y crear productos que se puedan comprar desde allí. No tienes que publicar nada ni enviar la app a Apple para su revisión. Necesitas una cuenta de desarrollador para hacerlo. Si no tienes uno, inscríbete en el programa para desarrolladores de Apple.
Acuerdos de Aplicaciones Pagadas
Para usar compras directas desde la aplicación, también debes tener un acuerdo activo para aplicaciones pagadas en App Store Connect. Ve a https://appstoreconnect.apple.com/ y haz clic en Acuerdos, Impuestos y Banca.
Aquí verás los acuerdos para las aplicaciones gratuitas y pagadas. El estado de las aplicaciones gratuitas debe ser activo y el de las aplicaciones pagadas debe ser nuevo. Asegúrate de leerlas, aceptarlas y de ingresar toda la información necesaria.
Cuando todo esté configurado correctamente, el estado de las aplicaciones pagadas estará activo. Esto es muy importante porque no podrás probar compras directas desde la aplicación sin un acuerdo activo.
Registrar ID de la app
Crea un identificador nuevo en el portal para desarrolladores de Apple.
Elegir los IDs de la app
Elegir app
Proporciona alguna descripción y configura el ID del paquete para que coincida con el valor establecido anteriormente en Xcode.
Para obtener más orientación sobre cómo crear un nuevo ID de app, consulta la Ayuda de la cuenta de desarrollador .
Crea una app nueva
Crea una app nueva en App Store Connect con tu identificador único de paquete.
Para obtener más orientación sobre cómo crear una app nueva y administrar los acuerdos, consulta la ayuda de App Store Connect.
Para probar las compras directas desde la aplicación, necesitas un usuario de prueba de la zona de pruebas. Este usuario de prueba no debe estar conectado a iTunes; solo se usa para probar compras directas desde la aplicación. 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 Verificadores en Zona de pruebas para crear una nueva cuenta de zona de pruebas o administrar los IDs de Apple existentes de la zona de pruebas.
Ahora puedes configurar el usuario de la zona de pruebas en tu iPhone desde Configuración > App Store > Cuenta de zona de pruebas.
Cómo configurar las compras directas desde la aplicación
Ahora configurarás los tres elementos que se pueden comprar:
dash_consumable_2k
: Es una compra de un producto consumible que se puede comprar muchas veces, lo que le otorga al usuario 2,000 guiones (la moneda integrada en la app) por compra.dash_upgrade_3d
: Una "actualización" no consumible compra que solo puede adquirirse una vez y le da al usuario un Dash diferente en cuanto a la estética para que haga clic.dash_subscription_doubler
: Es una suscripción que le otorga al usuario el doble de guiones por clic por el tiempo que dure la suscripción.
Ve a Compras directas desde la aplicación > Administrar.
Crea compras directas desde la aplicación con los ID especificados:
- Configura
dash_consumable_2k
como Consumable.
Usa dash_consumable_2k
como el ID del producto. El nombre de referencia solo se usa en App Store Connect. Solo debes configurarlo como dash consumable 2k
y agregar las localizaciones para la compra. Llama a la compra Spring is in the air
con 2000 dashes fly out
como descripción.
- Configura
dash_upgrade_3d
como No consumible.
Usa dash_upgrade_3d
como el ID del producto. Establece el nombre de referencia en dash upgrade 3d
y agrega las localizaciones para la compra. Llama a la compra 3D Dash
con Brings your dash back to the future
como descripción.
- Configura
dash_subscription_doubler
como una suscripción con renovación automática.
El flujo para las suscripciones es un poco diferente. Primero, debes establecer el nombre de referencia y el ID del producto:
Luego, debes crear un grupo de suscripciones. Cuando varias suscripciones forman parte del mismo grupo, un usuario solo puede suscribirse a una de ellas a la vez, pero puede actualizar las suscripciones o cambiar a una versión inferior con facilidad. Simplemente llama a este grupo subscriptions
.
A continuación, ingresa la duración de la suscripción y las localizaciones. Asigna el nombre Jet Engine
a esta suscripción con la descripción Doubles your clicks
. Haz clic en Guardar (Save).
Después de hacer clic en el botón Guardar, agrega un precio para la suscripción. Elige el precio que desees.
Ahora deberías ver las tres compras en la lista de compras:
5. Configura Play Store
Al igual que con la App Store, también necesitarás una cuenta de desarrollador para Play Store. Si aún no tienes una, regístrala.
Cómo crear una app nueva
Sigue estos pasos para crear una app nueva en Google Play Console:
- Abre Play Console.
- Selecciona Todas las aplicaciones > Crear app
- Selecciona un idioma predeterminado y agrega un título para tu app. Escribe el nombre de la app como quieras que aparezca 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 la aplicación es gratuita o pagada.
- Agrega una dirección de correo electrónico para que los usuarios de Play Store puedan comunicarse contigo acerca de esta aplicación.
- Completa los lineamientos de contenido y las declaraciones de leyes de exportación de EE.UU.
- Selecciona Crear app.
Después de crear tu app, ve al panel y completa todas las tareas de la sección Configura tu app. Aquí, debes proporcionar información sobre la app, como clasificaciones del contenido y 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, tu compilación de lanzamiento debe estar firmada con un elemento que no sea las claves de depuración.
Crea un almacén de claves
Si ya tienes un almacén de claves, continúa con el siguiente paso. De lo contrario, ejecuta el siguiente comando en la línea de comandos para crear uno:
En Mac/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 deseas almacenar el archivo en otro lugar, cambia el argumento que pasas al parámetro -keystore
. Mantén la
keystore
archivo privado; no la registres en el control de código público.
Haz referencia al almacén de claves desde la app
Crea un archivo llamado <your app dir>/android/key.properties
que contenga una referencia al 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 el acceso en Gradle
Para configurar la firma de tu app, edita el archivo <your app dir>/android/app/build.gradle
.
Agrega la información del almacén de claves de tu archivo de propiedades antes del bloque android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Carga el archivo key.properties
en el objeto keystoreProperties
.
Agrega el siguiente código antes del bloque buildTypes
:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
Configura el bloque signingConfigs
en el archivo build.gradle
de tu módulo con la información de configuración de firma:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Las compilaciones de lanzamiento de tu app ahora se firmarán automáticamente.
Para obtener más información sobre cómo firmar tu aplicación, consulta Cómo firmar tu aplicación en developer.android.com.
Cómo subir tu primera compilación
Una vez que tu app esté configurada para firmar, deberías poder compilarla mediante la ejecución del siguiente comando:
flutter build appbundle
Este comando genera una compilación de lanzamiento de forma predeterminada. El resultado se puede encontrar en <your app dir>/build/app/outputs/bundle/release/
.
En el panel de Google Play Console, ve a Versión > Prueba > Pruebas cerradas, y crea una nueva versión de prueba cerrada.
En este codelab, deberás mantener la firma de la app por parte de Google, por lo que debes presionar Continuar en Firma de apps de Play para habilitarla.
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 Start rollout to Internal testing para activar la versión de prueba interna.
Configura usuarios de prueba
Para poder probar las compras directas desde la aplicación, debes agregar las Cuentas de Google de los verificadores a Google Play Console en dos ubicaciones:
- En el segmento de pruebas específico (prueba interna)
- Como verificador de licencias
Primero, agrega el verificador al segmento de pruebas internas. Regresa a Versión > Prueba > 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 lista de direcciones de correo electrónico. Asigna un nombre a la lista y agrega las direcciones de correo electrónico de las Cuentas de Google que necesitan acceso para probar compras directas desde la aplicación.
Luego, selecciona la casilla de verificación de la lista y haz clic en Guardar cambios.
Luego, agrega los verificadores de licencias:
- Vuelve a la vista Todas las apps de Google Play Console.
- Ve a Configuración > Prueba de licencias.
- Agrega las mismas direcciones de correo electrónico de los verificadores que podrán realizar pruebas de las compras directas desde la aplicación.
- Establece License response en
RESPOND_NORMALLY
. - Haga clic en Guardar cambios.
Cómo configurar las compras directas desde la aplicación
Ahora configurarás los elementos que se pueden comprar dentro de la app.
Al igual que en la App Store, debes definir tres compras diferentes:
dash_consumable_2k
: Es una compra de un producto consumible que se puede comprar muchas veces, lo que le otorga al usuario 2,000 guiones (la moneda integrada en la app) por compra.dash_upgrade_3d
: Una "actualización" no consumible compra que solo se puede adquirir una vez, lo que le da al usuario un Dash diferente en cuanto a la estética para hacer clic.dash_subscription_doubler
: Es una suscripción que le otorga al usuario el doble de guiones por clic por el tiempo que dure la suscripción.
Primero, agrega los consumibles y los no consumibles.
- Ve a Google Play Console y selecciona tu aplicación.
- Ve a Monetizar > Productos > Productos integrados en la aplicación
- Haz clic en Crear producto.
- Ingresa toda la información requerida de tu producto. Asegúrate de que el ID del producto coincida exactamente con el que quieres usar.
- Haz clic en Guardar.
- Haz clic en Activar.
- Repite el proceso para la “actualización” de los productos no consumibles compra.
A continuación, agrega la suscripción:
- Ve a Google Play Console y selecciona tu aplicación.
- Ve a Monetizar > Productos > Suscripciones.
- Haz clic en Crear suscripción.
- Ingresa toda la información necesaria para tu suscripción. Asegúrate de que el ID del producto coincida exactamente con el que quieres 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 contraseñas compras.
Usar un servicio de backend tiene varios beneficios:
- Puedes verificar las transacciones de forma segura.
- Puedes reaccionar a los eventos de facturación de las tiendas de aplicaciones.
- Puedes realizar 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 si retroceden el reloj del sistema.
Si bien existen muchas formas de configurar un servicio de backend, podrás hacerlo con Cloud Functions y Firestore, con Firebase de Google.
La escritura del backend se considera fuera del alcance de este codelab, por lo que el código de partida 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 partida.
Lo que 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, las compras se vinculan a un usuario específico; por lo tanto, necesitas autenticación. Para ello, aprovecha 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 también.
Configura 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
}
}
}
Cómo configurar Firebase para Flutter
Para instalar Firebase en la app de Flutter, se recomienda usar la CLI de FlutterFire. Sigue las instrucciones que se explican en la página de configuración.
Cuando ejecutes la configuración de Flutterfire, 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 anular firebase_options.dart, selecciona Sí.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Cómo configurar Firebase para Android: Más pasos
En el panel de Firebase, ve a Descripción general del proyecto,elige Configuración y selecciona la pestaña General.
Desplázate hacia abajo hasta Tus apps y selecciona la app de dashclicker (Android).
Para permitir el Acceso con Google en modo de depuración, debes proporcionar la huella digital del hash SHA-1 de tu certificado de depuración.
Obtén el hash del 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 gran lista de claves de firma. Como buscas el hash del certificado de depuración, busca el certificado con las propiedades Variant
y Config
configuradas como debug
. Es probable que el almacén de claves se encuentre en la 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.
Cómo configurar Firebase para iOS: Más pasos
Abre ios/Runnder.xcworkspace
con Xcode
. o con el IDE que prefieras.
En VSCode, 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/
. Luego, haz clic en flutter
y, luego, en 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 plist
de compilación. (consulta los documentos del paquete google_sign_in
para obtener más información). En este caso, los archivos son ios/Runner/Info-Debug.plist
y ios/Runner/Info-Release.plist
.
Ya se agregó el par clave-valor, pero se deben reemplazar sus valores:
- Obtén el valor de
REVERSED_CLIENT_ID
del archivoGoogleService-Info.plist
, sin el elemento<string>..</string>
que lo rodea. - Reemplaza el valor en los archivos
ios/Runner/Info-Debug.plist
yios/Runner/Info-Release.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 la configuración de Firebase.
7. Escucha actualizaciones sobre 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.
Cómo escuchar actualizaciones sobre 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 Provider
para DashCounter
, DashUpgrades,
y DashPurchases
. DashCounter
hace un seguimiento del recuento actual de guiones y los aumenta automáticamente. DashUpgrades
administra las mejoras que puedes comprar con Dashes. Este codelab se enfoca en DashPurchases
.
De forma predeterminada, el objeto de un proveedor se define cuando ese objeto se solicita por primera vez. Este objeto escucha las actualizaciones de compra 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,
),
También necesitas una instancia de InAppPurchaseConnection
. Sin embargo, para que la app se pueda probar, necesitas alguna manera 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!;
}
}
Debes actualizar un poco la prueba si quieres que siga funcionando. Consulta widget_test.dart en GitHub para obtener el código completo de TestIAPConnection
.
test/widget_test.dart
void main() {
testWidgets('App starts', (WidgetTester tester) async {
IAPConnection.instance = TestIAPConnection();
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
En lib/logic/dash_purchases.dart
, ve al código de DashPurchases ChangeNotifier
. Por el momento, solo hay un DashCounter
que puedes agregar a los Dash que compraste.
Agrega una propiedad de suscripción de 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 'package:in_app_purchase/in_app_purchase.dart';
class DashPurchases extends ChangeNotifier {
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter);
}
La palabra clave late
se agrega a _subscription
porque _subscription
se inicializa en el constructor. Este proyecto está configurado para ser no anulable de forma predeterminada (NNBD), lo que significa que las propiedades que no se declaren anulables deben tener un valor que no sea nulo. El calificador late
te permite retrasar la definición de este valor.
En el constructor, obtén el purchaseUpdatedStream
y comienza a escuchar la transmisión. En el método dispose()
, cancela la suscripción de transmisión.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
late StreamSubscription<List<PurchaseDetails>> _subscription;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
Future<void> buy(PurchasableProduct product) async {
// omitted
}
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
}
}
Ahora, la app recibe las actualizaciones de compra, de modo que, en la próxima 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 ficticios existentes actualmente 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.
Adapta PurchasableProduct
PurchasableProduct
muestra un producto simulado. Actualízala para que muestre contenido real reemplazando 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 ficticias y reemplázalas por una lista vacía, List<PurchasableProduct> products = [];
.
Cómo cargar compras disponibles
Para que un usuario pueda realizar una compra, carga las compras realizadas en la tienda. Primero, verifica si la tienda está disponible. Cuando la tienda no está disponible, se muestra un mensaje de error al usuario si se establece storeState
en notAvailable
.
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. Dada la configuración anterior de Firebase, se espera que se muestren storeKeyConsumable
, storeKeySubscription,
y storeKeyUpgrade
. Cuando una compra esperada no esté disponible, imprime esta información en la consola. quizás también quieras enviar esta información
al servicio de backend.
El método await iapConnection.queryProductDetails(ids)
muestra los IDs que no se encuentran y los productos que se pueden comprar. 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);
for (var element in response.notFoundIDs) {
debugPrint('Purchase $element not found');
}
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();
}
Por último, cambia el valor del campo storeState
de StoreState.available
a StoreState.loading:
.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Muestra los productos que se pueden comprar
Considera el archivo purchase_page.dart
. El widget PurchasePage
muestra _PurchasesLoading
, _PurchaseList,
o _PurchasesNotAvailable,
, según el StoreState
. El widget también muestra las compras anteriores del usuario, que se usarán 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 puede pasar un tiempo hasta que las compras estén disponibles cuando se ingresan en las respectivas consolas.
Regresa a dash_purchases.dart
y, luego, implementa la función para comprar un producto. Solo debes separar los productos consumibles de los no consumibles. La actualización y los productos de suscripción no son 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);
break;
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
Antes de continuar, crea la variable _beautifiedDashUpgrade
y actualiza el método get beautifiedDash
para hacer 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 de contador. Es importante llamar a completePurchase
después de procesar la compra para que la tienda sepa que esta se efectuó 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();
break;
case storeKeyConsumable:
counter.addBoughtDashes(2000);
break;
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. Configura el backend
Antes de continuar con el seguimiento y la verificación de compras, configura un backend de Dart para admitir esto.
En esta sección, trabaja 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
Debido a que algunas partes de este proyecto se consideran fuera del alcance de este codelab, se incluyen en el código de partida. Te recomendamos revisar lo que ya hay en el código de partida antes de comenzar para tener una idea de cómo estructurarás las cosas.
Este código de backend puede ejecutarse de forma local en tu máquina, por lo que no necesitas implementarlo para usarlo. Sin embargo, debes poder conectarte desde tu dispositivo de desarrollo (Android o iPhone) a la máquina donde se ejecutará el servidor. Para eso, deben estar en la misma red, y necesitas 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 manejar el proceso de verificación de compras.
Una parte que ya está incluida en el código de partida es IapRepository
en lib/iap_repository.dart
. Dado que aprender a interactuar con Firestore, o con las bases de datos en general, no se considera relevante para este codelab, el código de partida contiene funciones para crear o actualizar 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 de cuenta de servicio. Genera una abriendo 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 compras, debes generar una cuenta de servicio con estos permisos y descargar sus credenciales JSON.
- Ve a Google Play Console y comienza desde la página Todas las apps.
- Ve a Configuración > Acceso a la API. Si Google Play Console te solicita crear un proyecto o vincular uno existente, hazlo primero y, luego, regresa a esta página.
- Busca la sección en la que puedes definir las cuentas de servicio y haz clic en Crear una cuenta de servicio nueva.
- En el cuadro de diálogo que aparece, haz clic en el vínculo Google Cloud Platform.
- Elige tu proyecto. Si no ves esta opción, asegúrate de haber accedido a la Cuenta de Google correcta en la lista desplegable Cuenta ubicada en la parte superior derecha.
- Después de seleccionar tu proyecto, haz clic en + Crear cuenta de servicio en la barra de menú superior.
- Proporciona un nombre para la cuenta de servicio y, de forma opcional, una descripción para que recuerdes para qué sirve y, luego, vayas al siguiente paso.
- Asigna el rol de Editor a la cuenta de servicio.
- Finaliza el asistente, vuelve a la página Acceso a la API en Play Console y haz clic en Actualizar cuentas de servicio. Deberías ver la cuenta que acabas de crear en la lista.
- Haz clic en Otorgar acceso a la nueva cuenta de servicio.
- Desplázate hacia abajo en la página siguiente hasta el bloque Datos financieros. Selecciona Ver datos financieros, pedidos y respuestas a la encuesta de cancelación y Administrar pedidos y suscripciones.
- Haz clic en Invitar a un usuario.
- Ahora que la cuenta está configurada, solo debes generar algunas credenciales. En la consola de Cloud, busca tu cuenta de servicio en la lista correspondiente, haz clic en los tres puntos verticales y elige Administrar claves.
- Crea una clave JSON nueva y descárgala.
- Cambia el nombre del archivo descargado a
service-account-google-play.json,
y muévelo al directorioassets/
.
Algo más que debemos hacer es abrir lib/constants.dart,
y reemplazar el valor de androidPackageId
con el ID de paquete que elegiste para tu app para Android.
Configura el acceso a la App Store de Apple
Para acceder a la 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 barra de navegación de la barra lateral, ve a Compras directas desde la aplicación > Administrar.
- En la esquina superior derecha de la lista, haz clic en 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 de paquete que se usa en Android. p.ej.,com.example.dashclicker
appStoreSharedSecret
: Es un secreto compartido para acceder a App Store Connect y realizar la verificación de compras.bundleId
: Es el ID del paquete que se usa en iOS. p.ej.,com.example.dashclicker
Por el momento, puedes ignorar el resto de las constantes.
10. Verifica las compras
El flujo general para verificar compras es similar en iOS y Android.
Para ambas tiendas, la aplicación recibe un token cuando se realiza una compra.
La app envía este token a tu servicio de backend, el cual, a su vez, verifica la compra con los servidores de la tienda respectiva mediante el token proporcionado.
El servicio de backend puede elegir almacenar la compra y responder a la aplicación si la compra fue válida o no.
Si haces que el servicio de backend realice la validación con las tiendas en lugar de que la aplicación se ejecute en el dispositivo de tu usuario, puedes evitar que el usuario obtenga acceso a funciones premium, por ejemplo, retrocediendo el reloj del sistema.
Cómo configurar el lado de Flutter
Configura la autenticación
Como vas a enviar las compras a tu servicio de backend, debes asegurarte de que el usuario esté autenticado mientras realiza una compra. La mayor parte de la lógica de autenticación ya se agregó al proyecto inicial, 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 comienzo del método de compilación de PurchasePage
:
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
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();
}
// omitted
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 mediante una llamada http post.
Envía la tienda seleccionada (google_play
para Play Store o app_store
para App Store), el serverVerificationData
y el productID
. El servidor muestra un código de estado que indica si se verificó la compra.
En las constantes de la app, configura la IP del servidor en función de la dirección IP de tu máquina local.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
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 método get para el usuario en el FirebaseNotifier de modo que puedas pasar el ID del usuario a la función de verificación de compra.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
Agrega la función _verifyPurchase
a la clase DashPurchases
. Esta función async
muestra 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) {
print('Successfully verified purchase');
return true;
} else {
print('failed request: ${response.statusCode} - ${response.body}');
return false;
}
}
Llama a la función _verifyPurchase
en _handlePurchase
justo antes de aplicar la compra. Solo debes aplicar la compra cuando esté verificada. En una app de producción, puedes especificar esto más, por ejemplo, para aplicar una suscripción de prueba cuando la tienda no está disponible temporalmente. Sin embargo, en este ejemplo, simplifica el proceso y solo aplica la compra cuando esta se haya verificado 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();
break;
case storeKeyConsumable:
counter.addBoughtDashes(1000);
break;
}
}
}
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 la Cloud Function para verificar las compras en el backend.
Cómo compilar controladores de compra
Como el flujo de verificación para ambas tiendas es casi idéntico, configura una clase abstracta PurchaseHandler
con implementaciones independientes para cada tienda.
Para comenzar, agrega un archivo purchase_handler.dart
a la carpeta lib/
, en la que defines una clase abstracta PurchaseHandler
con dos métodos abstractos para verificar dos tipos diferentes de compras: suscripciones y no 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ó, de modo 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 proporciona al usuario.
Además, para facilitar el uso de estos controladores de compra, agrega un método verifyPurchase()
que se pueda usar para suscripciones y no 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 aún tienes implementaciones separadas.
La clase ProductData
contiene información básica sobre los diferentes productos que se pueden comprar, que incluye el ID del producto (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 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 App Store de Apple. 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, muestra true
para los métodos del controlador. llegarás a ellos más tarde.
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.
Luego, 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,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
¡Genial! Ahora tienes dos controladores de compra. A continuación, crearemos el extremo de la API de verificación de compras.
Cómo usar controladores de compra
Abre bin/server.dart
y crea un extremo de API con shelf_route
:
bin/server.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);
}
({
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 anterior 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 con sesión activasource
: Se usa la tienda, ya seaapp_store
ogoogle_play
.productData
: Se obtiene delproductDataMap
que creaste antes.token
: Contiene los datos de verificación que se enviarán a las tiendas.- Llama al método
verifyPurchase
, ya sea paraGooglePlayPurchaseHandler
oAppStorePurchaseHandler
, según la fuente. - Si la verificación se realizó correctamente, el método muestra un
Response.ok
al cliente. - Si la verificación falla, el método muestra un
Response.internalServerError
al cliente.
Después de crear el extremo de API, debes configurar los dos controladores de compra. Para ello, debes cargar las claves de 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
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 de Android: Implementa el manual de compra
A continuación, continúa con la implementación del controlador de compras de Google Play.
Google ya proporciona paquetes de Dart para interactuar con las APIs que necesitas para verificar las 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 del tipo de suscripción:
lib/google_play_purchase_handler.dart
@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 do not 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 una 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 do not 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 pedido, así como dos métodos para analizar el estado de la compra.
lib/google_play_purchase_handler.dart
/// 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;
}
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,
};
}
Tus compras en Google Play ya deberían estar verificadas y almacenadas en la base de datos.
Luego, continúa con las compras en la App Store para iOS.
Verifica las compras de iOS: Implementa el controlador de compra
Para verificar compras con la App Store, existe un paquete de Dart de terceros llamado app_store_server_sdk
que facilita el proceso.
Para comenzar, crea la instancia ITunesApi
. Utiliza la configuración de la zona de pruebas y habilita los registros 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, la App Store usa los mismos extremos de la API para suscripciones y no suscripciones. Esto significa que puedes usar la misma lógica para ambos controladores. Combínalos para que llamen 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 {
//..
}
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;
}
}
Ahora tus compras en la App Store 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 compras están en el servicio de backend. Esto se debe a que tu backend puede responder a eventos del almacén y, por lo tanto, es menos propenso a encontrarse con información desactualizada debido al almacenamiento en caché, además de ser menos susceptible de ser manipulado.
Primero, configura el procesamiento de los eventos de almacenamiento en el backend con el backend de Dart que estuviste compilando.
Procesa eventos de almacenamiento 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, debes configurar esta opción tanto para Google Play Store como para la App Store de Apple.
Cómo procesar eventos de Facturación Google Play
Google Play ofrece eventos de facturación a través de lo que llaman un tema de Cloud Pub/Sub. Básicamente, son colas de mensajes desde las que se pueden publicar los mensajes y consumirlos.
Debido a que esta es una función específica de Google Play, debes incluirla en el 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();
});
}
Timer
está configurado para llamar al método _pullMessageFromSubSub
cada diez segundos. Puedes ajustar la duración como prefieras.
Luego, crea el _pullMessageFromSubSub
.
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/$googlePlayProjectName/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/$googlePlayProjectName/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 desde 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 (de suscripciones y no suscripciones) y llama a los handleSubscription
o handleNonSubscription
existentes si es necesario.
Se debe confirmar cada mensaje 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
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
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, // new
),
'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 publicas ningún evento de facturación. Es hora de configurar esto.
Primero, crea un tema de Pub/Sub:
- 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 + Crear tema.
- Asigna un nombre al tema nuevo, idéntico al valor establecido para
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
enconstants.ts
. En este caso, asígnale el nombreplay_billing
. Si eliges otra opción, asegúrate de actualizarconstants.ts
. Crea el tema. - En la lista de 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 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 el permiso.
- Copia el nombre del tema que acabas de crear en Nombre del tema.
- Vuelve a abrir Play Console y elige la app en la lista Todas las apps.
- Desplázate hacia abajo y ve a Monetizar > Configuración de la monetización
- Completa el tema y guarda los cambios.
Todos los eventos de Facturación Google Play se publicarán ahora en el tema.
Procesa eventos de facturación de la App Store
A continuación, haz lo mismo con los eventos de facturación de la App Store. Existen dos formas eficaces de implementar el manejo de actualizaciones en las compras de App Store. Una es implementar un webhook que 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 conectarte a la API del servidor de App Store y obtener la información de suscripción de forma manual.
Este codelab se enfoca en la segunda solución porque 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;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
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 del 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 como se indica a continuación:
lib/app_store_purchase_handler.dart
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 mediante el IapRepository.
- Para cada pedido, solicita el estado de la suscripción a la API del servidor de App Store.
- Obtiene la última transacción para 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 del servidor de App Store:
bin/server.dart
// 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, // new
),
};
Configuración de App Store
A continuación, configura la App Store:
- Accede a App Store Connect y selecciona Usuarios y acceso.
- Ve a Tipo de clave > Compra directa desde la aplicación
- Presiona el signo "más". para agregar una nueva.
- Asígnale un nombre, por ejemplo, “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 clave de la clave recién creada y establécelo en la constante
appStoreKeyId
en el archivolib/constants.dart
. - Copia el ID de la entidad emisora en la parte superior de la lista de claves y configúralo como la constante
appStoreIssuerId
en el archivolib/constants.dart
.
Cómo hacer un seguimiento de las compras en el dispositivo
La forma más segura de hacer un seguimiento de tus compras es a través del servidor, ya que el cliente es difícil de proteger, pero debes tener alguna manera de devolverle la información al cliente para que la app pueda actuar en función de la información del estado de la suscripción. Cuando almacenas las compras en Firestore, puedes sincronizar fácilmente los datos con el cliente y mantenerlos actualizados automáticamente.
Ya incluiste el 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((DocumentSnapshot 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 está en la clase DashPurchases
y es donde se deben aplicar o quitar las suscripciones. Por lo tanto, agrega iapRepo
como propiedad en la clase y asigna el iapRepo
en el constructor. A continuación, agrega directamente un objeto de escucha en el constructor y quítalo en el método dispose()
. Al principio, el objeto de escucha puede ser solo una función vacía. Debido a que 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
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated =
iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
iapRepo.removeListener(purchasesUpdate);
_subscription.cancel();
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 está creado en un Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
A continuación, escribe el código de la función purchaseUpdate()
. En dash_counter.dart,
, los métodos applyPaidMultiplier
y removePaidMultiplier
establecen el multiplicador en 10 o 1, respectivamente, para que no tengas que verificar si ya se aplicó la suscripción. Cuando cambie el estado de la suscripción, también actualizarás el estado del producto que se puede comprar para mostrar en la página de compra que ya está activo. Configura la propiedad _beautifiedDashUpgrade
según se compre 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 siempre esté 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 hacer clic con Dash.
12. Todo listo
¡Felicitaciones! Completaste el codelab. Puedes encontrar el código completo de este codelab en la carpeta completa.
Para obtener más información, prueba los otros codelabs de Flutter.