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 para 2,000 Dashes a la vez.
- Es una compra de actualización única para cambiar el diseño del panel del Dash antiguo al moderno.
- Una suscripción que duplica los clics generados automáticamente.
La primera opción de compra le brinda al usuario un beneficio directo de 2,000 Dashes. Están disponibles directamente para el usuario y se pueden comprar varias veces. Esto se denomina consumible, ya que se consume directamente y se puede consumir varias veces.
La segunda opción actualiza el Dash a uno más atractivo. Solo se debe comprar una vez y está disponible para siempre. Esta compra se denomina no consumible porque la app no puede consumirla, 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 pagarla, 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 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 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 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 de flutter-codelabs/in_app_purchases
contiene una serie de instantáneas de dónde deberías estar al final de cada paso nombrado. El código de partida está en el paso 0, por lo que ubicar los archivos coincidentes es tan fácil como lo siguiente:
cd flutter-codelabs/in_app_purchases/step_00
Si quieres avanzar 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 preferido. 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 de Dart y Flutter más recientes.
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 denomina identificador de paquete y, en Play Store de Android, es el 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 compras directas desde la aplicación para flutter.dev, debes usar dev.flutter.inapppurchase
. Piensa en un identificador para tu app, que ahora establecerás 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, haz clic en Flutter y abre el módulo 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 de tu proyecto y, luego, haz clic en Firma y 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 terminar 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 siguiente captura de pantalla) al ID de aplicación, 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. Sin embargo, mantenerlos idénticos 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
Agrega in_app_purchase
a pubspec. Para ello, agrega in_app_purchase
a las dependencias de tu pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
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: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
Haz clic en pub get para descargar el paquete o ejecuta flutter pub get
en la línea de comandos.
4. Cómo configurar App Store
Para configurar 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 tienes que publicar nada ni enviar la app a Apple para su revisión. Para ello, necesitas una cuenta de desarrollador. Si no tienes una, inscríbete en el programa para desarrolladores de Apple.
Acuerdos de Aplicaciones Pagadas
Para usar las compras directas desde la aplicación, también debes tener un acuerdo activo para apps pagadas en App Store Connect. Ve a https://appstoreconnect.apple.com/ y haz clic en Acuerdos, impuestos y servicios bancarios.
Aquí verás los acuerdos de las apps gratuitas y pagadas. El estado de las apps gratuitas debe ser activo, y el de las pagadas, nuevo. Asegúrate de ver las condiciones, aceptarlas y, luego, ingresar toda la información requerida.
Cuando todo esté configurado correctamente, el estado de las apps pagadas será activo. Esto es muy importante porque no podrás probar las compras integradas en la app sin un acuerdo activo.
Registra el ID de la app
Crea un identificador nuevo en el portal para desarrolladores de Apple.
Elige los IDs de app
Elegir app
Proporciona una descripción y establece el ID del paquete para que coincida con el mismo valor que se configuró anteriormente en XCode.
Para obtener más orientación sobre cómo crear un ID de app nuevo, consulta la Ayuda de la cuenta 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 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, ya que solo se usa para probar las compras directas desde 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 Verificadores en Zona de pruebas para crear una cuenta de zona de pruebas nueva o administrar los IDs de Apple de zona de pruebas existentes.
Ahora puedes configurar el usuario de la zona de pruebas en tu iPhone. Para ello, ve a Configuración > App Store > Cuenta de zona de pruebas.
Cómo configurar tus compras directas desde la aplicación
Ahora, configurarás los tres artículos que se pueden comprar:
dash_consumable_2k
: Es una compra consumible que se puede comprar muchas veces y que le otorga al usuario 2, 000 Dashes (la moneda de la aplicación) 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 le otorga al usuario el doble de Dashes por clic durante el período de la suscripción.
Ve a Compras directas desde la aplicación > Administrar.
Crea tus compras directas desde la aplicación con los IDs especificados:
- Configura
dash_consumable_2k
como un bien consumible.
Usa dash_consumable_2k
como el ID del producto. El nombre de referencia solo se usa en App Store Connect. Solo debes establecerlo en dash consumable 2k
y agregar tus 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 un elemento no consumible.
Usa dash_upgrade_3d
como el ID del producto. Establece el nombre de referencia como dash upgrade 3d
y agrega tus 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 de las suscripciones es un poco diferente. Primero, deberás establecer el nombre de referencia y el ID del producto:
A continuación, 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 cambiar fácilmente a una versión superior o inferior entre estas suscripciones. Solo llama a este grupo subscriptions
.
Luego, 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.
Después de hacer clic en el botón Guardar, agrega un precio de suscripción. Elige el precio que quieras.
Ahora deberías ver las tres compras en la lista de compras:
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.
Cómo crear 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.
- Agrega una dirección de correo electrónico para que los usuarios de Play Store puedan comunicarse contigo si tienen consultas sobre la app.
- Completa las declaraciones de los Lineamientos de contenido y las leyes de exportación de EE.UU.
- Selecciona Crear app.
Una vez que crees la app, ve al panel y completa todas las tareas de la sección Configura tu app. Aquí, proporcionas 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 tener al menos una compilación subida 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 tienes un almacén de claves existente, avanza al siguiente paso. De lo contrario, crea uno ejecutando el siguiente comando 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
. Mantén la
keystore
archivo 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
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 la firma de tu app, consulta Cómo firmar tu app en developer.android.com.
Sube tu primera compilación
Una vez que tu app esté configurada para la firma, deberías poder compilarla ejecutando lo siguiente:
flutter build appbundle
Este comando genera una compilación de lanzamiento de forma predeterminada, y 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 > Pruebas > Pruebas cerradas y crea una nueva versión de pruebas cerradas.
En este codelab, te centrarás en que Google firme la app, así que presiona Continuar en Firma de apps de Play para habilitar esta opción.
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 pruebas internas.
Configura usuarios de prueba
Para poder probar las compras directas desde la aplicación, las Cuentas de Google de los verificadores deben agregarse en Google Play Console en dos ubicaciones:
- Al segmento de pruebas específico (pruebas internas)
- Como verificador de licencias
Primero, agrega el verificador al segmento de pruebas internas. Regresa a Versión > Pruebas > Pruebas internas y haz clic en la pestaña Verificadores.
Para crear una nueva lista de direcciones de correo electrónico, haz clic en Crear 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 necesiten acceso para probar las 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 directas desde la app.
- Establece Respuesta de la 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 comprar muchas veces y que le otorga al usuario 2, 000 Dashes (la moneda de la aplicación) por compra.dash_upgrade_3d
: Es una compra de "actualización" no consumible que solo se puede comprar una vez, lo que le brinda al usuario un Dash cosméticamente diferente para hacer clic.dash_subscription_doubler
: Es una suscripción que le otorga al usuario el doble de Dashes por clic durante el período de la suscripción.
Primero, agrega los productos consumibles y no consumibles.
- 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 necesaria para tu producto. Asegúrate de que el ID del producto coincida exactamente con el 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 necesaria para tu suscripción. Asegúrate de que el ID del producto coincida exactamente con el 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 las compras de los usuarios y hacer un seguimiento de ellas.
Usar un servicio de backend tiene varios beneficios:
- Puedes verificar las transacciones de forma segura.
- Puedes reaccionar a los eventos de facturación desde 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 rebobinando el reloj del sistema.
Si bien hay muchas formas de configurar un servicio de backend, lo harás con Cloud Functions y Firestore, con el 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 comiences.
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. En 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, aprovecha el módulo de autenticación de Firebase con el Acceso con Google.
- En el panel de Firebase, ve a Autenticación y habilítala 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.
Configura 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 anular 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: 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 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 de tu certificado de firma de depuración
En la raíz del proyecto de tu app de Flutter, cambia de 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 en debug
. Es probable que el almacén de claves esté 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.
Configura Firebase para iOS: Más pasos
Abre el 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/
y, luego, 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 la documentación 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
.
El par clave-valor ya se agregó, 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 con la configuración de Firebase.
7. Escucha actualizaciones de compras
En esta parte del codelab, prepararás la app para comprar los productos. Este proceso incluye escuchar actualizaciones y 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 Provider
para DashCounter
, DashUpgrades,
y DashPurchases
. DashCounter
realiza 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 una 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!;
}
}
Debes actualizar ligeramente la prueba si quieres que siga funcionando.
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 DashPurchases ChangeNotifier
. Actualmente, solo hay un DashCounter
que puedes agregar a tus Dashes comprados.
Agrega una propiedad de suscripción de flujo, _subscription
(de tipo StreamSubscription<List<PurchaseDetails>> _subscription;
), 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 no admitir valores nulos de forma predeterminada (NNBD), lo que significa que las propiedades que no se declaran como admiten valores nulos 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
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 compras, 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. Cómo 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.
Adapta PurchasableProduct
PurchasableProduct
muestra un producto simulado. Para actualizarlo y mostrar contenido real, reemplaza la clase PurchasableProduct
en purchasable_product.dart
por 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 = [];
.
Cómo cargar las compras disponibles
Para permitir que un usuario realice una compra, carga las compras de 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. Dada la configuración de Firebase anterior, deberías ver storeKeyConsumable
, storeKeySubscription,
y storeKeyUpgrade
. Cuando una compra esperada no esté disponible, imprime esta información en la consola. También te recomendamos que envíes esta información al servicio de backend.
El método await iapConnection.queryProductDetails(ids)
muestra los IDs que no se encontraron 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);
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;
Cómo mostrar 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 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 Android y iOS. Ten en cuenta que las compras pueden tardar un tiempo en estar disponibles cuando se ingresan en las consolas correspondientes.
Regresa 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 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);
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 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 compras, 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 controlar la compra para que la tienda sepa que se realizó 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 compras, configura un backend de Dart para poder hacerlo.
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
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 estructurarás todo.
Este código de backend se puede ejecutar de forma local en tu máquina, 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 compras.
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 de la cuenta de servicio. Para generar una, abre la configuración del proyecto de Firebase y navega a la sección Cuentas de servicio. 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 las credenciales JSON correspondientes.
- Ve a Google Play Console y comienza en la página Todas las apps.
- Ve a Configuración > Acceso a la API.
En caso de que Google Play Console te solicite que crees un proyecto existente o que lo vincules a uno, hazlo primero y, luego, vuelve a esta página.
- Busca la sección en la que puedes definir cuentas de servicio y haz clic en Crear una cuenta de servicio nueva.
- Haz clic en el vínculo Google Cloud Platform en el cuadro de diálogo que aparece.
- Elige tu proyecto. Si no la ves, asegúrate de haber accedido a la Cuenta de Google correcta en la lista desplegable Cuenta, 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 ve al siguiente paso.
- Asigna a la cuenta de servicio el rol de editor.
- Termina el asistente, vuelve a la página Acceso a la API en la consola para desarrolladores 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 para tu cuenta de servicio nueva.
- 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 de cuentas de servicio, 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/
.
Otra cosa que debemos hacer es abrir lib/constants.dart,
y reemplazar el valor de androidPackageId
por el ID del paquete que elegiste para tu app para Android.
Cómo configurar 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 lateral de navegación, ve a Compras directas desde la aplicación > Administrar.
- En la parte 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 las compras es similar para 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 con el token proporcionado.
Luego, el servicio de backend puede optar por almacenar la compra y responderle 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 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 se autentique mientras realiza una compra. La mayor parte de la lógica de autenticación ya se agregó en el proyecto de partida. 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 '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.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
Cómo llamar al extremo de verificación desde la app
En la app, crea la función _verifyPurchase(PurchaseDetails purchaseDetails)
que llame al extremo /verifypurchase
en tu backend de Dart con una llamada post HTTP.
Envía la tienda seleccionada (google_play
para Play Store o app_store
para App Store), serverVerificationData
y 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 con 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 FirebaseNotifier, de modo que puedas pasar el ID de 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 la compra se validó.
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 con más detalle para, por ejemplo, aplicar una suscripción de prueba cuando la tienda no esté disponible temporalmente. Sin embargo, para este ejemplo, mantén la simplicidad y aplica la compra solo 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(1000);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
En la app, ya está todo listo para validar las compras.
Configura el servicio de backend
A continuación, configura la función de Cloud Functions para verificar las compras en el backend.
Cómo compilar controladores de compra
Como el flujo de verificación de ambas tiendas es casi idéntico, configura una clase PurchaseHandler
abstracta con implementaciones independientes para cada tienda.
Para comenzar, agrega un archivo purchase_handler.dart
a la carpeta lib/
, en la que defines una clase PurchaseHandler
abstracta 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:
El ID del usuario que accedió para que puedas vincular las compras al usuario.productData:
Datos sobre el producto. Lo definirás en un momento.token:
Es el token que la tienda le proporciona al usuario.
Además, para que estos controladores de compras sean más fáciles de usar, 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 así tener 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, muestra true
para los métodos del controlador. Los verás más adelante.
Como te habrás dado cuenta, 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,
}) {
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.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 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 que accediósource
: Almacenamiento utilizado,app_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 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
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,
),
};
}
Cómo verificar las compras de Android: Implementa el controlador de compras
A continuación, continúa implementando el 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 compras que no son de 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 compras de suscripción 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
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;
}
Tus compras de Google Play deberían verificarse y almacenarse en la base de datos.
A continuación, continúa con las compras de App Store para iOS.
Cómo verificar las compras de 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.
Primero, crea la instancia ITunesApi
. Usa la configuración de la 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 las no suscripciones. Esto significa que puedes usar la misma lógica para ambos controladores. Únelos 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 {
//..
}
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) {
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 de App Store ya 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 tus usuarios es en el servicio de backend. Esto se debe a que tu backend puede responder a eventos de la tienda y, por lo tanto, es menos propenso a encontrar información desactualizada debido a la caché y es menos susceptible de ser manipulado.
Primero, configura el procesamiento de eventos de tienda en el backend con el backend de Dart que compilaste.
Cómo procesar eventos de 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 las compras de tu base de datos actualizadas. En esta sección, configura esta opción para Google Play Store y App Store de Apple.
Cómo procesar eventos de facturación de Google Play
Google Play proporciona eventos de facturación a través de lo que denominan un tema de Cloud Pub/Sub. Básicamente, son colas de mensajes en las que se pueden publicar y consumir mensajes.
Como esta es una funcionalidad específica de Google Play, debes incluirla 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();
});
}
Timer
está configurado para llamar al método _pullMessageFromSubSub
cada diez segundos. Puedes ajustar la duración según tus preferencias.
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 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 no suscripciones, y llama a handleSubscription
o handleNonSubscription
existentes si es necesario.
Cada mensaje debe confirmarse con el método _askMessage
.
Luego, agrega las dependencias requeridas 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ásala 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 configurarlo.
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 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 derecha, elige Agregar principal.
- Aquí, agrega
google-play-developer-notifications@system.gserviceaccount.com
y asígnale 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 Monetización > Configuración de monetización.
- Completa el tema completo y guarda los cambios.
Ahora, todos los eventos de facturación de Google Play se publicarán en el tema.
Cómo procesar eventos de facturación de App Store
Luego, 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 para App Store. Una es implementando un webhook que le proporcionas a Apple y que 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 la suscripción de forma manual.
El motivo por el que este codelab se enfoca en la segunda solución es porque tendrías que exponer tu servidor a Internet para implementar el webhook.
En un entorno de producción, lo ideal es tener ambos. El webhook para obtener eventos de App Store y la API del servidor en caso de que hayas perdido un evento o necesites volver a 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 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
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 esa compra de 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 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 ícono de signo más para agregar uno nuevo.
- Asóciale un nombre, p.ej., "Clave del 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 constante
appStoreKeyId
en el archivolib/constants.dart
. - Copia el ID del emisor justo en la parte superior de la lista de claves y configúralo como 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 del lado del servidor, ya que el cliente es difícil de proteger, pero debes tener alguna forma de devolver la información al cliente para que la app pueda actuar en función de la información del estado de la suscripción. Si almacenas las compras en Firestore, puedes sincronizar los datos con el cliente fácilmente y mantenerlos actualizados automáticamente.
Ya incluiste IAPRepo en la app, que es el repositorio de Firestore que contiene todos los datos de compras 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 se encuentra en la clase DashPurchases
y es allí donde se deben aplicar o quitar las suscripciones. Por lo tanto, agrega iapRepo
como una propiedad en la clase y asígnale iapRepo
en el constructor. A continuación, agrega un objeto de escucha directamente en el constructor y quítalo del 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
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() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
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>(),
),
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, por lo que no tienes que verificar si la suscripción ya se aplicó. Cuando cambia el estado de la suscripción, también debes actualizar 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 siempre esté actualizado en el servicio de backend y sincronizado con la app. La app actúa según corresponda 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.