Cómo agregar compras directas desde la aplicación a tu app de Flutter

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:

  1. Es una opción de compra repetible de 2,000 Dashes a la vez.
  2. Es una compra de actualización única para convertir el Dash de estilo antiguo en un Dash de estilo moderno.
  3. Es una suscripción que duplica los clics generados automáticamente.

La primera opción de compra le otorga al usuario un beneficio directo de 2,000 Dashes. Están disponibles directamente para el usuario y se pueden comprar muchas veces. Se denomina consumible porque se consume directamente y se puede consumir varias veces.

La segunda opción actualiza el tablero a uno más atractivo. Solo se debe comprar una vez y está disponible para siempre. Este tipo de compra se denomina no consumible porque la app no la puede consumir, pero es válida para siempre.

La tercera y última opción de compra es una suscripción. Mientras la suscripción esté activa, el usuario obtendrá Dashes más rápido, pero cuando deje de pagar la suscripción, los beneficios también desaparecerán.

El servicio de backend (que también se te proporciona) se ejecuta como una app de Dart, verifica que se realicen las compras y las almacena con Firestore. Firestore se usa para facilitar el proceso, pero en tu app de producción, puedes usar cualquier tipo de servicio de backend.

300123416ebc8dc1.png 7145d0fffe6ea741.png 646317a79be08214.png

Qué compilarás

  • Extenderás una app para admitir compras y suscripciones de elementos consumibles.
  • También extenderás una app de backend de Dart para verificar y almacenar los elementos comprados.

Qué aprenderás

  • Cómo configurar App Store y Play Store con productos que se pueden comprar
  • Cómo comunicarse con las tiendas para verificar las compras y almacenarlas en Firestore
  • Cómo administrar las compras en tu app

Requisitos

2. Cómo configurar el entorno de desarrollo

Para comenzar este codelab, descarga el código y cambia el identificador del paquete para iOS y el nombre del paquete para Android.

Descarga el código

Para clonar el repositorio de GitHub desde la línea de comandos, usa el siguiente comando:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

O bien, si tienes instalada la herramienta de CLI de GitHub, usa el siguiente comando:

gh repo clone flutter/codelabs flutter-codelabs

El código de muestra se clona en un directorio flutter-codelabs que contiene el código de una colección de codelabs. El código de este codelab se encuentra en flutter-codelabs/in_app_purchases.

La estructura de directorios en flutter-codelabs/in_app_purchases contiene una serie de instantáneas de dónde deberías estar al final de cada paso con nombre. El código de partida se encuentra en el paso 0, por lo que debes navegar a él de la siguiente manera:

cd flutter-codelabs/in_app_purchases/step_00

Si quieres adelantar o ver cómo debería verse algo después de un paso, busca en el directorio que lleva el nombre del paso que te interesa. El código del último paso se encuentra en la carpeta complete.

Configura el proyecto inicial

Abre el proyecto inicial desde step_00/app en tu IDE favorito. Usamos Android Studio para las capturas de pantalla, pero Visual Studio Code también es una excelente opción. Con cualquiera de los editores, asegúrate de que estén instalados los complementos más recientes de Dart y Flutter.

Las apps que crearás deben comunicarse con App Store y Play Store para saber qué productos están disponibles y a qué precio. Cada app se identifica con un ID único. En la App Store de iOS, se llama identificador de paquete, y en Play Store de Android, ID de aplicación. Por lo general, estos identificadores se crean con una notación de nombre de dominio inverso. Por ejemplo, cuando creas una app de compra directa desde la aplicación para flutter.dev, usarías dev.flutter.inapppurchase. Piensa en un identificador para tu app. Ahora lo establecerás en la configuración del proyecto.

Primero, configura el identificador de paquete para iOS. Para ello, abre el archivo Runner.xcworkspace en la app de Xcode.

a9fbac80a31e28e0.png

En la estructura de carpetas de Xcode, el proyecto Runner se encuentra en la parte superior, y los destinos Flutter, Runner y Products se encuentran debajo del proyecto Runner. Haz doble clic en Runner para editar la configuración del proyecto y, luego, haz clic en Signing & Capabilities. Ingresa el identificador del paquete que acabas de elegir en el campo Equipo para establecer tu equipo.

812f919d965c649a.jpeg

Ahora puedes cerrar Xcode y volver a Android Studio para terminar la configuración de Android. Para ello, abre el archivo build.gradle.kts en android/app, y cambia tu applicationId (en la línea 24 de la captura de pantalla que se muestra a continuación) al ID de la aplicación, que es el mismo que el identificador del paquete de iOS. Ten en cuenta que los IDs de las tiendas de iOS y Android no tienen que ser idénticos, pero mantenerlos idénticos es menos propenso a errores, por lo que en este codelab también usaremos identificadores idénticos.

e320a49ff2068ac2.png

3. Instala el complemento

En esta parte del codelab, instalarás el complemento in_app_purchase.

Agrega la dependencia en pubspec

Agrega in_app_purchase al archivo pubspec agregando in_app_purchase a las dependencias de tu proyecto:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Abre tu pubspec.yaml y confirma que ahora tienes in_app_purchase como una entrada en dependencies y in_app_purchase_platform_interface en dev_dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  in_app_purchase_platform_interface: ^1.4.0

4. Configura la App Store

Para configurar las compras directas desde la aplicación y probarlas en iOS, debes crear una app nueva en App Store y crear productos que se puedan comprar allí. No es necesario que publiques nada ni que envíes la app a Apple para su revisión. Para ello, necesitas una cuenta de desarrollador. Si no tienes uno, inscríbete en el programa para desarrolladores de Apple.

Para usar las compras directas desde la app, también debes tener un acuerdo activo para las apps pagadas en App Store Connect. Ve a https://appstoreconnect.apple.com/ y haz clic en Acuerdos, impuestos y banca.

11db9fca823e7608.png

Aquí verás los acuerdos de las apps gratuitas y pagadas. El estado de las apps gratuitas debe ser activo, y el de las apps pagadas debe ser nuevo. Asegúrate de ver las condiciones, aceptarlas y completar toda la información obligatoria.

74c73197472c9aec.png

Cuando todo esté configurado correctamente, el estado de las apps pagadas estará activo. Esto es muy importante porque no podrás probar las compras integradas en la app sin un acuerdo activo.

4a100bbb8cafdbbf.jpeg

Registrar ID de la app

Crea un identificador nuevo en Apple Developer Portal. Visita developer.apple.com/account/resources/identifiers/list y haz clic en el ícono de signo más junto al encabezado Identifiers.

55d7e592d9a3fc7b.png

Elige los IDs de la app

13f125598b72ca77.png

Elegir app

41ac4c13404e2526.png

Proporciona una descripción y establece el ID del paquete para que coincida con el mismo valor que se estableció anteriormente en Xcode.

9d2c940ad80deeef.png

Para obtener más orientación sobre cómo crear un ID de app nuevo, consulta la Ayuda para cuentas de desarrollador.

Cómo crear una app nueva

Crea una app nueva en App Store Connect con tu identificador de paquete único.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Para obtener más orientación sobre cómo crear una app nueva y administrar acuerdos, consulta la Ayuda de App Store Connect.

Para probar las compras directas desde la aplicación, necesitas un usuario de prueba de zona de pruebas. Este usuario de prueba no debe estar conectado a iTunes, ya que solo se usa para probar las compras integradas en la app. No puedes usar una dirección de correo electrónico que ya se use para una cuenta de Apple. En Usuarios y acceso, ve a Sandbox para crear una nueva cuenta de Sandbox o administrar los IDs de Apple de Sandbox existentes.

2ba0f599bcac9b36.png

Ahora puedes configurar tu usuario de espacio aislado en tu iPhone. Para ello, ve a Configuración > Desarrollador > Cuenta de Apple de espacio aislado.

74a545210b282ad8.png eaa67752f2350f74.png

Cómo configurar tus compras directas desde la aplicación

Ahora configurarás los tres elementos comprables:

  • dash_consumable_2k: Es una compra consumible que se puede realizar muchas veces y que otorga al usuario 2, 000 Dashes (la moneda integrada en la app) por compra.
  • dash_upgrade_3d: Es una compra de "actualización" no consumible que solo se puede comprar una vez y le brinda al usuario un Dash cosméticamente diferente para hacer clic.
  • dash_subscription_doubler: Es una suscripción que otorga al usuario el doble de guiones por clic durante el período de la suscripción.

a118161fac83815a.png

Ve a Compras directas desde la aplicación.

Crea tus compras directas desde la aplicación con los IDs especificados:

  1. Configura dash_consumable_2k como Consumible. Usa dash_consumable_2k como el ID del producto. El nombre de referencia solo se usa en App Store Connect, así que configúralo como dash consumable 2k. 1f8527fc03902099.png Configura la disponibilidad. El producto debe estar disponible en el país del usuario de la zona de pruebas. bd6b2ce2d9314e6e.png Agrega precios y establece el precio en $1.99 o el equivalente en otra moneda. 926b03544ae044c4.png Agrega tus localizaciones para la compra. Llama a la compra Spring is in the air con 2000 dashes fly out como descripción. e26dd4f966dcfece.png Agrega una captura de pantalla de la opinión. El contenido no importa, a menos que el producto se envíe para su revisión, pero es necesario para que el producto esté en el estado "Listo para enviar", lo que es necesario cuando la app recupera productos de App Store. 25171bfd6f3a033a.png
  2. Configura dash_upgrade_3d como no consumible. Usa dash_upgrade_3d como el ID del producto. Establece el nombre de referencia como dash upgrade 3d. Llama a la compra 3D Dash con Brings your dash back to the future como descripción. Establece el precio en $0.99. Configura la disponibilidad y sube la captura de pantalla de la revisión de la misma manera que lo hiciste para el producto dash_consumable_2k. 83878759f32a7d4a.png
  3. Configura dash_subscription_doubler como una suscripción con renovación automática. El flujo de las suscripciones es un poco diferente. Primero, debes crear un grupo de suscripciones. Cuando varias suscripciones forman parte del mismo grupo, el usuario solo puede suscribirse a una de ellas a la vez, pero puede cambiar a una versión superior o inferior entre estas suscripciones. Solo llama a este grupo subscriptions. 393a44b09f3cd8bf.png Y agrega la localización para el grupo de suscripciones. 595aa910776349bd.png A continuación, crearás la suscripción. Establece el nombre de referencia en dash subscription doubler y el ID del producto en dash_subscription_doubler. 7bfff7bbe11c8eec.png A continuación, selecciona la duración de la suscripción de 1 semana y las localizaciones. Asigna el nombre Jet Engine a esta suscripción con la descripción Doubles your clicks. Establece el precio en $0.49. Configura la disponibilidad y sube la captura de pantalla de la revisión de la misma manera que lo hiciste para el producto dash_consumable_2k. 44d18e02b926a334.png

Ahora deberías ver los productos en las listas:

17f242b5c1426b79.png d71da951f595054a.png

5. Configura Play Store

Al igual que con App Store, también necesitarás una cuenta de desarrollador para Play Store. Si aún no tienes una, regístrate para obtener una cuenta.

Crea una app nueva

Crea una app nueva en Google Play Console:

  1. Abre Play Console.
  2. Selecciona Todas las apps > Crear app.
  3. 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.
  4. Especifica que tu aplicación es un juego. Puedes cambiarlo más adelante.
  5. Especifica si se trata de una aplicación pagada o gratuita.
  6. Completa las declaraciones de Lineamientos de contenido y Leyes de exportación de EE.UU.
  7. Selecciona Crear app.

Después de crear la app, ve al panel y completa todas las tareas de la sección Configura tu app. Aquí, proporcionarás información sobre tu app, como las clasificaciones del contenido y las capturas de pantalla. 13845badcf9bc1db.png

Firma la aplicación

Para poder probar las compras directas desde la aplicación, debes subir al menos una compilación a Google Play.

Para ello, debes firmar la compilación de lanzamiento con algo que no sean las claves de depuración.

Crea un almacén de claves

Si ya tienes un almacén de claves, avanza al siguiente paso. De lo contrario, crea uno ejecutando lo siguiente en la línea de comandos.

En Mac o Linux, usa el siguiente comando:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

En Windows, usa el siguiente comando:

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

Este comando almacena el archivo key.jks en tu directorio principal. Si quieres almacenar el archivo en otro lugar, cambia el argumento que pasas al parámetro -keystore. Conserva el

keystore

El archivo es privado; no lo registres en el control de código fuente público.

Cómo hacer referencia al almacén de claves desde la app

Crea un archivo llamado <your app dir>/android/key.properties que contenga una referencia a tu almacén de claves:

storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>

Cómo configurar la firma en Gradle

Edita el archivo <your app dir>/android/app/build.gradle.kts para configurar la firma de tu app.

Agrega la información del almacén de claves de tu archivo de propiedades antes del bloque android:

import java.util.Properties
import java.io.FileInputStream

plugins {
    // omitted
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // omitted
}

Carga el archivo key.properties en el objeto keystoreProperties.

Actualiza el bloque buildTypes de la siguiente manera:

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Configura el bloque signingConfigs en el archivo build.gradle.kts de tu módulo con la información de configuración de la firma:

   signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Ahora se firmarán automáticamente las compilaciones de lanzamiento de tu app.

Para obtener más información sobre cómo firmar tu app, consulta Cómo firmar tu app en developer.android.com.

Sube tu primera compilación

Después de configurar tu app para la firma, deberías poder compilarla ejecutando el siguiente comando:

flutter build appbundle

De forma predeterminada, este comando genera una compilación de lanzamiento, y el resultado se puede encontrar en <your app dir>/build/app/outputs/bundle/release/.

En el panel de Play Console, ve a Pruebas y lanzamiento > Pruebas > Pruebas cerradas y crea un nuevo lanzamiento de prueba cerrada.

A continuación, sube el paquete de aplicación app-release.aab que generó el comando de compilación.

Haz clic en Guardar y, luego, en Revisar la versión.

Por último, haz clic en Iniciar lanzamiento en la prueba cerrada para activar la versión de prueba cerrada.

Configura usuarios de prueba

Para poder probar las compras directas desde la aplicación, las Cuentas de Google de tus verificadores deben agregarse en dos ubicaciones de Google Play Console:

  1. Al segmento de pruebas específico (pruebas internas)
  2. Como verificador de licencias

Primero, agrega al verificador al segmento de pruebas internas. Regresa a Prueba y lanza > Pruebas > Pruebas internas y haz clic en la pestaña Verificadores.

a0d0394e85128f84.png

Haz clic en Crear lista de direcciones de correo electrónico para crear una nueva. Asigna un nombre a la lista y agrega las direcciones de correo electrónico de las Cuentas de Google que necesitan acceso a las pruebas de compras integradas en la aplicación.

A continuación, selecciona la casilla de verificación de la lista y haz clic en Guardar cambios.

Luego, agrega los verificadores de licencias:

  1. Regresa a la vista Todas las apps de Google Play Console.
  2. Ve a Configuración > Prueba de licencia.
  3. Agrega las mismas direcciones de correo electrónico de los verificadores que deben poder probar las compras integradas en la app.
  4. Establece Respuesta de licencia en RESPOND_NORMALLY.
  5. Haga clic en Guardar cambios.

a1a0f9d3e55ea8da.png

Cómo configurar tus compras directas desde la aplicación

Ahora configurarás los elementos que se pueden comprar en la app.

Al igual que en App Store, debes definir tres compras diferentes:

  • dash_consumable_2k: Es una compra consumible que se puede realizar muchas veces y que otorga al usuario 2, 000 Dashes (la moneda integrada en la app) por compra.
  • dash_upgrade_3d: Es una compra de "actualización" no consumible que solo se puede comprar una vez y que le da al usuario un Dash cosméticamente diferente para hacer clic.
  • dash_subscription_doubler: Es una suscripción que otorga al usuario el doble de guiones por clic durante el período de la suscripción.

Primero, agrega el producto consumible y el no consumible.

  1. Ve a Google Play Console y selecciona tu aplicación.
  2. Ve a Monetización > Productos > Productos integrados en la aplicación.
  3. Haz clic en Crear producto.c8d66e32f57dee21.png
  4. Ingresa toda la información requerida sobre tu producto. Asegúrate de que el ID de producto coincida exactamente con el ID que deseas usar.
  5. Haga clic en Guardar.
  6. Haz clic en Activar.
  7. Repite el proceso para la compra de "actualización" no consumible.

A continuación, agrega la suscripción:

  1. Ve a Google Play Console y selecciona tu aplicación.
  2. Ve a Monetización > Productos > Suscripciones.
  3. Haz clic en Crear suscripción.32a6a9eefdb71dd0.png
  4. Ingresa toda la información requerida para tu suscripción. Asegúrate de que el ID de producto coincida exactamente con el ID que deseas usar.
  5. Haga clic en Guardar.

Tus compras ya deberían estar configuradas en Play Console.

6. Configura Firebase

En este codelab, usarás un servicio de backend para verificar y hacer un seguimiento de las compras de los usuarios.

Usar un servicio de backend tiene varios beneficios:

  • Puedes verificar transacciones de forma segura.
  • Puedes reaccionar a los eventos de facturación de las tiendas de aplicaciones.
  • Puedes hacer un seguimiento de las compras en una base de datos.
  • Los usuarios no podrán engañar a tu app para que proporcione funciones premium adelantando el reloj del sistema.

Si bien existen muchas formas de configurar un servicio de backend, lo harás con Cloud Functions y Firestore, usando Firebase de Google.

Escribir el backend se considera fuera del alcance de este codelab, por lo que el código de inicio ya incluye un proyecto de Firebase que controla las compras básicas para que puedas comenzar.

Los complementos de Firebase también se incluyen con la app de inicio.

Lo que te queda por hacer es crear tu propio proyecto de Firebase, configurar la app y el backend para Firebase y, por último, implementar el backend.

Crea un proyecto de Firebase

Ve a Firebase console y crea un proyecto de Firebase nuevo. Para este ejemplo, llama al proyecto Dash Clicker.

En la app de backend, vinculas las compras a un usuario específico, por lo que necesitas autenticación. Para ello, usa el módulo de autenticación de Firebase con el acceso con Google.

  1. En el panel de Firebase, ve a Authentication y habilítalo, si es necesario.
  2. Ve a la pestaña Método de acceso y habilita el proveedor de acceso de Google.

fe2e0933d6810888.png

Como también usarás la base de datos de Firestore de Firebase, habilita esta opción.

d02d641821c71e2c.png

Establece las reglas de Cloud Firestore de la siguiente manera:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /purchases/{purchaseId} {
      allow read: if request.auth != null && request.auth.uid == resource.data.userId
    }
  }
}

Configura Firebase para Flutter

La forma recomendada de instalar Firebase en la app de Flutter es usar la CLI de FlutterFire. Sigue las instrucciones que se explican en la página de configuración.

Cuando ejecutes flutterfire configure, selecciona el proyecto que acabas de crear en el paso anterior.

$ flutterfire configure

i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
  other-flutter-codelab-1 (other-flutter-codelab-1)
  other-flutter-codelab-2 (other-flutter-codelab-2)
  other-flutter-codelab-3 (other-flutter-codelab-3)
  other-flutter-codelab-4 (other-flutter-codelab-4)
  <create a new project>

A continuación, selecciona las dos plataformas para habilitar iOS y Android.

? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
  macos
  web

Cuando se te solicite que anules firebase_options.dart, selecciona sí.

? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes

Configura Firebase para Android: Pasos adicionales

En el panel de Firebase, ve a Project Overview,elige Settings y selecciona la pestaña General.

Desplázate hacia abajo hasta Tus apps y selecciona la app dashclicker (android).

b22d46a759c0c834.png

Para permitir el Acceso con Google en el modo de depuración, debes proporcionar la huella digital del hash SHA-1 de tu certificado de depuración.

Obtén el hash de tu certificado de firma de depuración

En la raíz del proyecto de tu app de Flutter, cambia el directorio a la carpeta android/ y, luego, genera un informe de firma.

cd android
./gradlew :app:signingReport

Verás una lista extensa de claves de firma. Como buscas el hash del certificado de depuración, busca el certificado con las propiedades Variant y Config establecidas en debug. Es probable que el almacén de claves se encuentre en tu carpeta principal en .android/debug.keystore.

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

Copia el hash SHA-1 y completa el último campo del diálogo modal de envío de la app.

Por último, vuelve a ejecutar el comando flutterfire configure para actualizar la app y que incluya la configuración de firma.

$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes

Configura Firebase para iOS: Pasos adicionales

Abre el archivo ios/Runner.xcworkspace con Xcode. También puedes usar el IDE que prefieras.

En VS Code, haz clic con el botón derecho en la carpeta ios/ y, luego, en open in xcode.

En Android Studio, haz clic con el botón derecho en la carpeta ios/ y, luego, en flutter seguido de la opción open iOS module in Xcode.

Para permitir el acceso con Google en iOS, agrega la opción de configuración CFBundleURLTypes a tus archivos de compilación plist. (Consulta la documentación del paquete google_sign_in para obtener más información). En este caso, el archivo es ios/Runner/Info.plist.

El par clave-valor ya se agregó, pero sus valores deben reemplazarse:

  1. Obtén el valor de REVERSED_CLIENT_ID del archivo GoogleService-Info.plist, sin el elemento <string>..</string> que lo rodea.
  2. Reemplaza el valor en tu archivo ios/Runner/Info.plist en la clave CFBundleURLTypes.
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

Ya terminaste con la configuración de Firebase.

7. Cómo escuchar actualizaciones de compras

En esta parte del codelab, prepararás la app para comprar los productos. Este proceso incluye escuchar las actualizaciones y los errores de compra después de que se inicia la app.

Escucha las actualizaciones de compras

En main.dart,, busca el widget MyHomePage que tiene un Scaffold con un BottomNavigationBar que contiene dos páginas. En esta página, también se crean tres objetos Provider para DashCounter, DashUpgrades, y DashPurchases. DashCounter hace un seguimiento del recuento actual de guiones y los incrementa automáticamente. DashUpgrades administra las actualizaciones que puedes comprar con Dashes. Este codelab se enfoca en DashPurchases.

De forma predeterminada, el objeto de un proveedor se define cuando se solicita por primera vez. Este objeto escucha las actualizaciones de compras directamente cuando se inicia la app, por lo que debes inhabilitar la carga diferida en este objeto con lazy: false:

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create: (context) => DashPurchases(
    context.read<DashCounter>(),
  ),
  lazy: false,                                             // Add this line
),

También necesitas una instancia de InAppPurchaseConnection. Sin embargo, para que la app se pueda probar, necesitas alguna forma de simular la conexión. Para ello, crea un método de instancia que se pueda anular en la prueba y agrégalo a main.dart.

lib/main.dart

// Gives the option to override in tests.
class IAPConnection {
  static InAppPurchase? _instance;
  static set instance(InAppPurchase value) {
    _instance = value;
  }

  static InAppPurchase get instance {
    _instance ??= InAppPurchase.instance;
    return _instance!;
  }
}

Actualiza la prueba de la siguiente manera:

test/widget_test.dart

import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart';     // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import

void main() {
  testWidgets('App starts', (tester) async {
    IAPConnection.instance = TestIAPConnection();          // Add this line
    await tester.pumpWidget(const MyApp());
    expect(find.text('Tim Sneath'), findsOneWidget);
  });
}

class TestIAPConnection implements InAppPurchase {         // Add from here
  @override
  Future<bool> buyConsumable({
    required PurchaseParam purchaseParam,
    bool autoConsume = true,
  }) {
    return Future.value(false);
  }

  @override
  Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
    return Future.value(false);
  }

  @override
  Future<void> completePurchase(PurchaseDetails purchase) {
    return Future.value();
  }

  @override
  Future<bool> isAvailable() {
    return Future.value(false);
  }

  @override
  Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
    return Future.value(
      ProductDetailsResponse(productDetails: [], notFoundIDs: []),
    );
  }

  @override
  T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
    // TODO: implement getPlatformAddition
    throw UnimplementedError();
  }

  @override
  Stream<List<PurchaseDetails>> get purchaseStream =>
      Stream.value(<PurchaseDetails>[]);

  @override
  Future<void> restorePurchases({String? applicationUserName}) {
    // TODO: implement restorePurchases
    throw UnimplementedError();
  }

  @override
  Future<String> countryCode() {
    // TODO: implement countryCode
    throw UnimplementedError();
  }
}                                                          // To here.

En lib/logic/dash_purchases.dart, ve al código de DashPurchasesChangeNotifier. En este punto, solo hay un DashCounter que puedes agregar a los Dashes que compraste.

Agrega una propiedad de suscripción a la transmisión, _subscription (de tipo StreamSubscription<List<PurchaseDetails>> _subscription;), el IAPConnection.instance, y las importaciones. El código resultante debería verse de la siguiente manera:

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';           // Add this import

import '../main.dart';                                           // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.available;
  late StreamSubscription<List<PurchaseDetails>> _subscription;  // Add this line
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;                  // And this line

  DashPurchases(this.counter);

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
}

La palabra clave late se agrega a _subscription porque _subscription se inicializa en el constructor. Este proyecto está configurado para que no acepte valores nulos de forma predeterminada (NNBD), lo que significa que las propiedades que no se declaran como anulables deben tener un valor no nulo. El calificador late te permite retrasar la definición de este valor.

En el constructor, obtén la transmisión de purchaseUpdated y comienza a escucharla. En el método dispose(), cancela la suscripción a la transmisión.

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter) {                            // Add from here
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }                                                        // To here.

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }                                                        // To here.
}

Ahora, la app recibe las actualizaciones de compra, por lo que, en la siguiente sección, realizarás una compra.

Antes de continuar, ejecuta las pruebas con "flutter test" para verificar que todo esté configurado correctamente.

$ flutter test

00:01 +1: All tests passed!

8. Realizar compras

En esta parte del codelab, reemplazarás los productos simulados existentes por productos reales que se pueden comprar. Estos productos se cargan desde las tiendas, se muestran en una lista y se compran cuando se presiona el producto.

Adapt PurchasableProduct

PurchasableProduct muestra un producto simulado. Actualízalo para que muestre contenido real. Para ello, reemplaza la clase PurchasableProduct en purchasable_product.dart con el siguiente código:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus { purchasable, purchased, pending }

class PurchasableProduct {
  String get id => productDetails.id;
  String get title => productDetails.title;
  String get description => productDetails.description;
  String get price => productDetails.price;
  ProductStatus status;
  ProductDetails productDetails;

  PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

En dash_purchases.dart,, quita las compras simuladas y reemplázalas por una lista vacía, List<PurchasableProduct> products = [];.

Carga las compras disponibles

Para permitir que un usuario realice una compra, carga las compras desde la tienda. Primero, verifica si la tienda está disponible. Cuando la tienda no está disponible, configurar storeState como notAvailable muestra un mensaje de error al usuario.

lib/logic/dash_purchases.dart

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
  }

Cuando la tienda esté disponible, carga las compras disponibles. Con la configuración anterior de Google Play y App Store, deberías ver storeKeyConsumable, storeKeySubscription, y storeKeyUpgrade. Cuando no esté disponible una compra esperada, imprime esta información en la consola. También puedes enviar esta información al servicio de backend.

El método await iapConnection.queryProductDetails(ids) devuelve tanto los IDs que no se encontraron como los productos que se pueden comprar que sí se encontraron. Usa el productDetails de la respuesta para actualizar la IU y establece StoreState en available.

lib/logic/dash_purchases.dart

import '../constants.dart';

// ...

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    products = response.productDetails
        .map((e) => PurchasableProduct(e))
        .toList();
    storeState = StoreState.available;
    notifyListeners();
  }

Llama a la función loadPurchases() en el constructor:

lib/logic/dash_purchases.dart

  DashPurchases(this.counter) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    loadPurchases();                                       // Add this line
  }

Por último, cambia el valor del campo storeState de StoreState.available a StoreState.loading:.

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Mostrar los productos que se pueden comprar

Considera el archivo purchase_page.dart. El widget PurchasePage muestra _PurchasesLoading, _PurchaseList, o _PurchasesNotAvailable, según el valor de StoreState. El widget también muestra las compras anteriores del usuario, que se usan en el siguiente paso.

El widget _PurchaseList muestra la lista de productos que se pueden comprar y envía una solicitud de compra al objeto DashPurchases.

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map(
            (product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              },
            ),
          )
          .toList(),
    );
  }
}

Si están configurados correctamente, deberías poder ver los productos disponibles en las tiendas de iOS y Android. Ten en cuenta que las compras pueden tardar un tiempo en estar disponibles cuando se ingresan en las consolas respectivas.

ca1a9f97c21e552d.png

Vuelve a dash_purchases.dart y, luego, implementa la función para comprar un producto. Solo debes separar los consumibles de los no consumibles. La actualización y los productos de suscripción son no consumibles.

lib/logic/dash_purchases.dart

  Future<void> buy(PurchasableProduct product) async {
    final purchaseParam = PurchaseParam(productDetails: product.productDetails);
    switch (product.id) {
      case storeKeyConsumable:
        await iapConnection.buyConsumable(purchaseParam: purchaseParam);
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
      default:
        throw ArgumentError.value(
          product.productDetails,
          '${product.id} is not a known product',
        );
    }
  }

Antes de continuar, crea la variable _beautifiedDashUpgrade y actualiza el getter beautifiedDash para que haga referencia a ella.

lib/logic/dash_purchases.dart

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;

El método _onPurchaseUpdate recibe las actualizaciones de compra, actualiza el estado del producto que se muestra en la página de compra y aplica la compra a la lógica del contador. Es importante llamar a completePurchase después de procesar la compra para que la tienda sepa que se procesó correctamente.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
    List<PurchaseDetails> purchaseDetailsList,
  ) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

9. Configura el backend

Antes de continuar con el seguimiento y la verificación de las compras, configura un backend de Dart para admitir esta acción.

En esta sección, trabajarás desde la carpeta dart-backend/ como raíz.

Asegúrate de tener instaladas las siguientes herramientas:

Descripción general del proyecto base

Dado que algunas partes de este proyecto se consideran fuera del alcance de este codelab, se incluyen en el código de partida. Es una buena idea revisar lo que ya está en el código de partida antes de comenzar, para tener una idea de cómo vas a estructurar las cosas.

Este código de backend se puede ejecutar de forma local en tu máquina, por lo que no es necesario que lo implementes para usarlo. Sin embargo, debes poder conectarte desde tu dispositivo de desarrollo (Android o iPhone) a la máquina en la que se ejecutará el servidor. Para ello, deben estar en la misma red y debes conocer la dirección IP de tu máquina.

Intenta ejecutar el servidor con el siguiente comando:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

El backend de Dart usa shelf y shelf_router para entregar extremos de API. De forma predeterminada, el servidor no proporciona ninguna ruta. Más adelante, crearás una ruta para controlar el proceso de verificación de la compra.

Una parte que ya se incluye en el código de partida es el IapRepository en lib/iap_repository.dart. Dado que aprender a interactuar con Firestore, o con bases de datos en general, no se considera relevante para este codelab, el código de partida contiene funciones para que crees o actualices compras en Firestore, así como todas las clases para esas compras.

Configura el acceso a Firebase

Para acceder a Firebase Firestore, necesitas una clave de acceso a la cuenta de servicio. Para generar una, abre la configuración del proyecto de Firebase, navega a la sección Cuentas de servicio y, luego, selecciona Generar nueva clave privada.

27590fc77ae94ad4.png

Copia el archivo JSON descargado en la carpeta assets/ y cámbiale el nombre a service-account-firebase.json.

Configura el acceso a Google Play

Para acceder a Play Store y verificar las compras, debes generar una cuenta de servicio con estos permisos y descargar sus credenciales JSON.

  1. Visita la página de la API de Google Play Android Developer en la consola de Google Cloud. 629f0bd8e6b50be8.png En caso de que Google Play Console te solicite que crees un proyecto o que lo vincules a uno existente, hazlo primero y, luego, vuelve a esta página.
  2. A continuación, ve a la página Cuentas de servicio y haz clic en + Crear cuenta de servicio. 8dc97e3b1262328a.png
  3. Ingresa el Nombre de la cuenta de servicio y haz clic en Crear y continuar. 4fe8106af85ce75f.png
  4. Selecciona el rol Suscriptor de Pub/Sub y haz clic en Listo. a5b6fa6ea8ee22d.png
  5. Una vez que se cree la cuenta, ve a Administrar claves. eb36da2c1ad6dd06.png
  6. Selecciona Agregar clave > Crear clave nueva. e92db9557a28a479.png
  7. Crea y descarga una clave JSON. 711d04f2f4176333.png
  8. Cambia el nombre del archivo descargado a service-account-google-play.json, y muévelo al directorio assets/.
  9. A continuación, ve a la página Usuarios y permisos en Play Console.28fffbfc35b45f97.png
  10. Haz clic en Invitar a usuarios nuevos y, luego, ingresa la dirección de correo electrónico de la cuenta de servicio que creaste antes. Puedes encontrar el correo electrónico en la tabla de la página Cuentas de servicio.e3310cc077f397d.png
  11. Otorga los permisos Ver datos financieros y Administrar pedidos y suscripciones para la aplicación. a3b8cf2b660d1900.png
  12. Haz clic en Invitar a un usuario.

Una cosa más que debemos hacer es abrir lib/constants.dart, y reemplazar el valor de androidPackageId por el ID de paquete que elegiste para tu app para Android.

Configura el acceso a la App Store de Apple

Para acceder a App Store y verificar las compras, debes configurar un secreto compartido:

  1. Abre App Store Connect.
  2. Ve a Mis apps y selecciona tu app.
  3. En la navegación de la barra lateral, ve a General > Información de la app.
  4. Haz clic en Administrar en el encabezado Secreto compartido específico de la app. ad419782c5fbacb2.png
  5. Genera un secreto nuevo y cópialo. b5b72a357459b0e5.png
  6. Abre lib/constants.dart, y reemplaza el valor de appStoreSharedSecret por el secreto compartido que acabas de generar.

Archivo de configuración de constantes

Antes de continuar, asegúrate de que las siguientes constantes estén configuradas en el archivo lib/constants.dart:

  • androidPackageId: Es el ID del paquete que se usa en Android, como com.example.dashclicker.
  • appStoreSharedSecret: Es el secreto compartido para acceder a App Store Connect y realizar la verificación de compras.
  • bundleId: ID del paquete que se usa en iOS, como com.example.dashclicker

Por el momento, puedes ignorar el resto de las constantes.

10. Verificar compras

El flujo general para verificar las compras es similar en iOS y Android.

En ambas tiendas, tu aplicación recibe un token cuando se realiza una compra.

La app envía este token a tu servicio de backend, que, a su vez, verifica la compra con los servidores de la tienda correspondiente usando el token proporcionado.

Luego, el servicio de backend puede optar por almacenar la compra y responder a la aplicación si la compra fue válida o no.

Si el servicio de backend realiza la validación con las tiendas en lugar de la aplicación que se ejecuta en el dispositivo del usuario, puedes evitar que el usuario obtenga acceso a las funciones premium, por ejemplo, rebobinando el reloj del sistema.

Configura el lado de Flutter

Configura la autenticación

Como enviarás las compras a tu servicio de backend, debes asegurarte de que el usuario esté autenticado cuando realice una compra. La mayor parte de la lógica de autenticación ya se agregó en el proyecto de inicio. Solo debes asegurarte de que PurchasePage muestre el botón de acceso cuando el usuario aún no haya accedido. Agrega el siguiente código al principio del método de compilación de PurchasePage:

lib/pages/purchase_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

class PurchasePage extends StatelessWidget {
  const PurchasePage({super.key});

  @override
  Widget build(BuildContext context) {                     // Update from here
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }                                                      // To here.

    // ...

Llama al extremo de verificación desde la app

En la app, crea la función _verifyPurchase(PurchaseDetails purchaseDetails) que llama al extremo /verifypurchase en tu backend de Dart con una llamada POST de HTTP.

Envía la tienda seleccionada (google_play para Play Store o app_store para App Store), el serverVerificationData y el productID. El servidor devuelve un código de estado que indica si se verificó la compra.

En las constantes de la app, configura la IP del servidor con la dirección IP de tu máquina local.

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart';                           // And this one

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;

  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter, this.firebaseNotifier) {     // Update this line
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    loadPurchases();
  }

Agrega firebaseNotifier con la creación de DashPurchases en main.dart:

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
          ),
          lazy: false,
        ),

Agrega un getter para el usuario en FirebaseNotifier, de modo que puedas pasar el ID del usuario a la función de verificación de compra.

lib/logic/firebase_notifier.dart

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

Agrega la función _verifyPurchase a la clase DashPurchases. Esta función async devuelve un valor booleano que indica si se validó la compra.

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      return true;
    } else {
      return false;
    }
  }

Llama a la función _verifyPurchase en _handlePurchase justo antes de aplicar la compra. Solo debes aplicar la compra cuando se verifique. En una app de producción, puedes especificar esto aún más, por ejemplo, para aplicar una suscripción de prueba cuando la tienda no esté disponible temporalmente. Sin embargo, para este ejemplo, aplica la compra cuando se verifique correctamente.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
    List<PurchaseDetails> purchaseDetailsList,
  ) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      // Send to server
      var validPurchase = await _verifyPurchase(purchaseDetails);

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
          case storeKeyConsumable:
            counter.addBoughtDashes(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

En la app, todo está listo para validar las compras.

Configura el servicio de backend

A continuación, configura el backend para verificar las compras en el backend.

Compila controladores de compra

Dado que el flujo de verificación de ambas tiendas es casi idéntico, configura una clase abstracta PurchaseHandler con implementaciones separadas para cada tienda.

be50c207c5a2a519.png

Comienza por agregar un archivo purchase_handler.dart a la carpeta lib/, en el que definirás una clase PurchaseHandler abstracta con dos métodos abstractos para verificar dos tipos diferentes de compras: suscripciones y compras que no son suscripciones.

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
  /// Verify if non-subscription purchase (aka consumable) is valid
  /// and update the database
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });

  /// Verify if subscription purchase (aka non-consumable) is valid
  /// and update the database
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });
}

Como puedes ver, cada método requiere tres parámetros:

  • userId: Es el ID del usuario que accedió, para que puedas vincular las compras al usuario.
  • productData: Datos sobre el producto. Definirás esto en un minuto.
  • token: Es el token que la tienda le proporciona al usuario.

Además, para que estos controladores de compra sean más fáciles de usar, agrega un método verifyPurchase() que se pueda usar tanto para suscripciones como para productos que no sean suscripciones:

lib/purchase_handler.dart

  /// Verify if purchase is valid and update the database
  Future<bool> verifyPurchase({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    switch (productData.type) {
      case ProductType.subscription:
        return handleSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
      case ProductType.nonSubscription:
        return handleNonSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
    }
  }

Ahora, puedes llamar a verifyPurchase para ambos casos, pero seguir teniendo implementaciones separadas.

La clase ProductData contiene información básica sobre los diferentes productos que se pueden comprar, lo que incluye el ID del producto (a veces también denominado SKU) y el ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

  const ProductData(this.productId, this.type);
}

El objeto ProductType puede ser una suscripción o no.

lib/products.dart

enum ProductType { subscription, nonSubscription }

Por último, la lista de productos se define como un mapa en el mismo archivo.

lib/products.dart

const productDataMap = {
  'dash_consumable_2k': ProductData(
    'dash_consumable_2k',
    ProductType.nonSubscription,
  ),
  'dash_upgrade_3d': ProductData(
    'dash_upgrade_3d',
    ProductType.nonSubscription,
  ),
  'dash_subscription_doubler': ProductData(
    'dash_subscription_doubler',
    ProductType.subscription,
  ),
};

A continuación, define algunas implementaciones de marcadores de posición para Google Play Store y Apple App Store. Comienza con Google Play:

Crea lib/google_play_purchase_handler.dart y agrega una clase que extienda el PurchaseHandler que acabas de escribir:

lib/google_play_purchase_handler.dart

import 'dart:async';

import 'package:googleapis/androidpublisher/v3.dart' as ap;

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;

  GooglePlayPurchaseHandler(this.androidPublisher, this.iapRepository);

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }
}

Por ahora, devuelve true para los métodos del controlador. Los verás más adelante.

Como habrás notado, el constructor toma una instancia de IapRepository. El controlador de compras usa esta instancia para almacenar información sobre las compras en Firestore más adelante. Para comunicarte con Google Play, usa el AndroidPublisherApi proporcionado.

A continuación, haz lo mismo con el controlador de la tienda de aplicaciones. Crea lib/app_store_purchase_handler.dart y agrega una clase que extienda PurchaseHandler nuevamente:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(this.iapRepository);

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }
}

¡Genial! Ahora tienes dos controladores de compra. A continuación, crea el extremo de la API de verificación de compras.

Usa controladores de compra

Abre bin/server.dart y crea un extremo de API con shelf_route:

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

Future<void> main() async {
  final router = Router();

  final purchaseHandlers = await _createPurchaseHandlers();

  router.post('/verifypurchase', (Request request) async {
    final dynamic payload = json.decode(await request.readAsString());

    final (:userId, :source, :productData, :token) = getPurchaseData(payload);

    final result = await purchaseHandlers[source]!.verifyPurchase(
      userId: userId,
      productData: productData,
      token: token,
    );

    if (result) {
      return Response.ok('all good!');
    } else {
      return Response.internalServerError();
    }
  });

  await serveHandler(router.call);
}

({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
  if (payload case {
    'userId': String userId,
    'source': String source,
    'productId': String productId,
    'verificationData': String token,
  }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

El código hace lo siguiente:

  1. Define un extremo POST al que se llamará desde la app que creaste anteriormente.
  2. Decodifica la carga útil de JSON y extrae la siguiente información:
    1. userId: ID del usuario que accedió
    2. source: Es la tienda que se usó, ya sea app_store o google_play.
    3. productData: Se obtiene del objeto productDataMap que creaste anteriormente.
    4. token: Contiene los datos de verificación que se enviarán a las tiendas.
  3. Llamada al método verifyPurchase, ya sea para GooglePlayPurchaseHandler o AppStorePurchaseHandler, según la fuente.
  4. Si la verificación se realizó correctamente, el método devuelve un Response.ok al cliente.
  5. Si la verificación falla, el método devuelve un Response.internalServerError al cliente.

Después de crear el extremo de la API, debes configurar los dos controladores de compra. Para ello, debes cargar las claves de la cuenta de servicio que obtuviste en el paso anterior y configurar el acceso a los diferentes servicios, incluidas la API de Android Publisher y la API de Firebase Firestore. Luego, crea los dos controladores de compra con las diferentes dependencias:

bin/server.dart

import 'dart:convert';
import 'dart:io'; // new

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
  // Configure Android Publisher API access
  final serviceAccountGooglePlay =
      File('assets/service-account-google-play.json').readAsStringSync();
  final clientCredentialsGooglePlay =
      auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
  final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
  ]);
  final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

  // Configure Firestore API access
  final serviceAccountFirebase =
      File('assets/service-account-firebase.json').readAsStringSync();
  final clientCredentialsFirebase =
      auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
  final clientFirebase =
      await auth.clientViaServiceAccount(clientCredentialsFirebase, [
    fs.FirestoreApi.cloudPlatformScope,
  ]);
  final firestoreApi = fs.FirestoreApi(clientFirebase);
  final dynamic json = jsonDecode(serviceAccountFirebase);
  final projectId = json['project_id'] as String;
  final iapRepository = IapRepository(firestoreApi, projectId);

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };
}

Verifica las compras en Android: Implementa el controlador de compras

A continuación, sigue implementando el controlador de compras de Google Play.

Google ya proporciona paquetes de Dart para interactuar con las APIs que necesitas para verificar compras. Los inicializaste en el archivo server.dart y ahora los usas en la clase GooglePlayPurchaseHandler.

Implementa el controlador para las compras que no son de tipo suscripción:

lib/google_play_purchase_handler.dart

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order ID exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

Puedes actualizar el controlador de compra de suscripciones de manera similar:

lib/google_play_purchase_handler.dart

  /// Handle subscription purchases.
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.subscriptions.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Subscription response: ${response.toJson()}');

      // Make sure an order ID exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = extractOrderId(response.orderId!);

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

Agrega el siguiente método para facilitar el análisis de los IDs de pedidos, así como dos métodos para analizar el estado de la compra.

lib/google_play_purchase_handler.dart

NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
  return switch (state) {
    0 => NonSubscriptionStatus.completed,
    2 => NonSubscriptionStatus.pending,
    _ => NonSubscriptionStatus.cancelled,
  };
}

SubscriptionStatus _subscriptionStatusFrom(int? state) {
  return switch (state) {
    // Payment pending
    0 => SubscriptionStatus.pending,
    // Payment received
    1 => SubscriptionStatus.active,
    // Free trial
    2 => SubscriptionStatus.active,
    // Pending deferred upgrade/downgrade
    3 => SubscriptionStatus.pending,
    // Expired or cancelled
    _ => SubscriptionStatus.expired,
  };
}

/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
  final orderIdSplit = orderId.split('..');
  if (orderIdSplit.isNotEmpty) {
    orderId = orderIdSplit[0];
  }
  return orderId;
}

Ahora se deberían verificar tus compras de Google Play y almacenarse en la base de datos.

A continuación, pasa a las compras en App Store para iOS.

Verifica las compras en iOS: Implementa el controlador de compras

Para verificar las compras con App Store, existe un paquete de Dart de terceros llamado app_store_server_sdk que facilita el proceso.

Comienza por crear la instancia ITunesApi. Usa la configuración de zona de pruebas y habilita el registro para facilitar la depuración de errores.

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(ITunesEnvironment.sandbox(), loggingEnabled: true),
  );

Ahora, a diferencia de las APIs de Google Play, App Store usa los mismos extremos de API para las suscripciones y los productos que no son de suscripción. Esto significa que puedes usar la misma lógica para ambos controladores. Combínalos para que llamen a la misma implementación:

lib/app_store_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) {
    return handleValidation(userId: userId, token: token);
  }

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {

    // See next step
  }

Ahora, implementa handleValidation:

lib/app_store_purchase_handler.dart

  /// Handle purchase validation.
  Future<bool> handleValidation({
    required String userId,
    required String token,
  }) async {
    print('AppStorePurchaseHandler.handleValidation');
    final response = await _iTunesAPI.verifyReceipt(
      password: appStoreSharedSecret,
      receiptData: token,
    );
    print('response: $response');
    if (response.status == 0) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(
              NonSubscriptionPurchase(
                userId: userId,
                productId: receipt.productId ?? '',
                iapSource: IAPSource.appstore,
                orderId: receipt.originalTransactionId ?? '',
                purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0'),
                ),
                type: product.type,
                status: NonSubscriptionStatus.completed,
              ),
            );
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(
              SubscriptionPurchase(
                userId: userId,
                productId: receipt.productId ?? '',
                iapSource: IAPSource.appstore,
                orderId: receipt.originalTransactionId ?? '',
                purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0'),
                ),
                type: product.type,
                expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0'),
                ),
                status: SubscriptionStatus.active,
              ),
            );
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

Tus compras en App Store ahora deberían estar verificadas y almacenadas en la base de datos.

Ejecuta el backend

En este punto, puedes ejecutar dart bin/server.dart para entregar el extremo /verifypurchase.

$ dart bin/server.dart
Serving at http://0.0.0.0:8080

11. Haz un seguimiento de las compras

La forma recomendada de hacer un seguimiento de las compras de los usuarios es en el servicio de backend. Esto se debe a que tu backend puede responder a los eventos de la tienda y, por lo tanto, es menos propenso a tener información desactualizada debido al almacenamiento en caché, además de ser menos susceptible a la manipulación.

Primero, configura el procesamiento de eventos de la tienda en el backend con el backend de Dart que has estado compilando.

Procesa eventos de la tienda en el backend

Las tiendas pueden informar a tu backend sobre cualquier evento de facturación que ocurra, como cuando se renuevan las suscripciones. Puedes procesar estos eventos en tu backend para mantener actualizadas las compras en tu base de datos. En esta sección, configura esta opción para Google Play Store y App Store de Apple.

Procesa eventos de facturación de Google Play

Google Play proporciona eventos de facturación a través de lo que se denomina un tema de Cloud Pub/Sub. Básicamente, son colas de mensajes en las que se pueden publicar mensajes y desde las que se pueden consumir.

Como esta funcionalidad es específica de Google Play, la incluyes en GooglePlayPurchaseHandler.

Para comenzar, abre lib/google_play_purchase_handler.dart y agrega la importación de PubsubApi:

lib/google_play_purchase_handler.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;

Luego, pasa el PubsubApi al GooglePlayPurchaseHandler y modifica el constructor de la clase para crear un Timer de la siguiente manera:

lib/google_play_purchase_handler.dart

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;
  final pubsub.PubsubApi pubsubApi; // new

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
    this.pubsubApi, // new
  ) {
    // Poll messages from Pub/Sub every 10 seconds
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullMessageFromPubSub();
    });
  }

El Timer está configurado para llamar al método _pullMessageFromPubSub cada diez segundos. Puedes ajustar la duración según tus preferencias.

Luego, crea el _pullMessageFromPubSub.

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(maxMessages: 1000);
    final topicName =
        'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(ackIds: [id]);
    final subscriptionName =
        'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

El código que acabas de agregar se comunica con el tema de Pub/Sub de Google Cloud cada diez segundos y solicita mensajes nuevos. Luego, procesa cada mensaje en el método _processMessage.

Este método decodifica los mensajes entrantes y obtiene la información actualizada sobre cada compra, tanto de suscripciones como de compras que no son de suscripciones, y llama a los métodos handleSubscription o handleNonSubscription existentes si es necesario.

Cada mensaje debe confirmarse con el método _askMessage.

A continuación, agrega las dependencias necesarias al archivo server.dart. Agrega PubsubApi.cloudPlatformScope a la configuración de credenciales:

bin/server.dart

import 'package:googleapis/pubsub/v1.dart' as pubsub;      // Add this import

  final clientGooglePlay = await auth
      .clientViaServiceAccount(clientCredentialsGooglePlay, [
        ap.AndroidPublisherApi.androidpublisherScope,
        pubsub.PubsubApi.cloudPlatformScope,               // Add this line
      ]);

Luego, crea la instancia de PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Por último, pásalo al constructor GooglePlayPurchaseHandler:

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,                                           // Add this line
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

Configuración de Google Play

Escribiste el código para consumir eventos de facturación del tema de Pub/Sub, pero no creaste el tema de Pub/Sub ni publicaste ningún evento de facturación. Es hora de configurarlo.

Primero, crea un tema de Pub/Sub:

  1. Establece el valor de googleCloudProjectId en constants.dart como el ID de tu proyecto de Google Cloud.
  2. Visita la página de Cloud Pub/Sub en la consola de Google Cloud.
  3. Asegúrate de estar en tu proyecto de Firebase y haz clic en + Create Topic. d5ebf6897a0a8bf5.png
  4. Asigna un nombre al tema nuevo, que sea idéntico al valor establecido para googlePlayPubsubBillingTopic en constants.dart. En este caso, asígnale el nombre play_billing. Si eliges otra opción, asegúrate de actualizar constants.dart. Crea el tema. 20d690fc543c4212.png
  5. 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. ea03308190609fb.png
  6. En la barra lateral de la derecha, elige Agregar principal.
  7. Aquí, agrega google-play-developer-notifications@system.gserviceaccount.com y otórgale el rol de Publicador de Pub/Sub. 55631ec0549215bc.png
  8. Guarda los cambios en los permisos.
  9. Copia el Nombre del tema del tema que acabas de crear.
  10. Vuelve a abrir Play Console y elige tu app en la lista Todas las apps.
  11. Desplázate hacia abajo y ve a Monetizar > Configuración de la monetización.
  12. Completa el tema completo y guarda los cambios. 7e5e875dc6ce5d54.png

Todos los eventos de facturación de Google Play ahora se publicarán en el tema.

Procesa eventos de facturación de App Store

A continuación, haz lo mismo con los eventos de facturación de App Store. Existen dos formas eficaces de implementar el control de actualizaciones en las compras de App Store. Una es implementar un webhook que le proporcionas a Apple y que este usa para comunicarse con tu servidor. La segunda forma, que es la que encontrarás en este codelab, es conectándote a la API de App Store Server y obteniendo la información de la suscripción de forma manual.

El motivo por el que este codelab se enfoca en la segunda solución es que tendrías que exponer tu servidor a Internet para implementar el webhook.

En un entorno de producción, lo ideal sería tener ambos. El webhook para obtener eventos de App Store y la API del servidor en caso de que te hayas perdido un evento o necesites verificar el estado de una suscripción.

Para comenzar, abre lib/app_store_purchase_handler.dart y agrega la dependencia AppStoreServerAPI:

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,                                  // And this parameter
  );

Modifica el constructor para agregar un temporizador que llame al método _pullStatus. Este temporizador llamará al método _pullStatus cada 10 segundos. Puedes ajustar la duración de este temporizador según tus necesidades.

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

Luego, crea el método _pullStatus de la siguiente manera:

lib/app_store_purchase_handler.dart

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where(
      (element) =>
          element.type == ProductType.subscription &&
          element.iapSource == IAPSource.appstore,
    );
    for (final purchase in appStoreSubscriptions) {
      final status = await appStoreServerAPI.getAllSubscriptionStatuses(
        purchase.orderId,
      );
      // Obtain all subscriptions for the order ID.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
            transaction.transactionInfo.expiresDate ?? 0,
          );
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(
            SubscriptionPurchase(
              userId: null,
              productId: transaction.transactionInfo.productId,
              iapSource: IAPSource.appstore,
              orderId: transaction.originalTransactionId,
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate,
              ),
              type: ProductType.subscription,
              expiryDate: expirationDate,
              status: isExpired
                  ? SubscriptionStatus.expired
                  : SubscriptionStatus.active,
            ),
          );
        }
      }
    }
  }

Este método funciona de la siguiente manera:

  1. Obtiene la lista de suscripciones activas de Firestore con IapRepository.
  2. Para cada pedido, solicita el estado de la suscripción a la API del servidor de App Store.
  3. Obtiene la última transacción de la compra de esa suscripción.
  4. Verifica la fecha de vencimiento.
  5. Actualiza el estado de la suscripción en Firestore. Si venció, se marcará como tal.

Por último, agrega todo el código necesario para configurar el acceso a la API de App Store Server:

bin/server.dart

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


  // add from here
  final subscriptionKeyAppStore = File(
    'assets/SubscriptionKey.p8',
  ).readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
      appStoreServerAPI,                                     // Add this argument
    ),
  };

Configuración de App Store

A continuación, configura App Store:

  1. Accede a App Store Connect y selecciona Usuarios y acceso.
  2. Ve a Integraciones > Claves > Compra integrada en la aplicación.
  3. Presiona el ícono de signo más para agregar uno nuevo.
  4. Asigna un nombre, como "Clave de codelab".
  5. Descarga el archivo .p8 que contiene la clave.
  6. Cópialo en la carpeta de recursos con el nombre SubscriptionKey.p8.
  7. Copia el ID de la clave recién creada y configúralo como la constante appStoreKeyId en el archivo lib/constants.dart.
  8. Copia el ID del emisor que se encuentra en la parte superior de la lista de claves y configúralo como la constante appStoreIssuerId en el archivo lib/constants.dart.

9540ea9ada3da151.png

Realiza un seguimiento de las compras en el dispositivo

La forma más segura de hacer un seguimiento de tus compras es del lado del servidor, ya que el cliente es difícil de proteger, pero necesitas alguna forma de devolver la información al cliente para que la app pueda actuar según la información del estado de la suscripción. Si almacenas las compras en Firestore, puedes sincronizar los datos con el cliente y mantenerlos actualizados automáticamente.

Ya incluiste IAPRepo en la app, que es el repositorio de Firestore que contiene todos los datos de compra del usuario en List<PastPurchase> purchases. El repositorio también contiene hasActiveSubscription,, que es verdadero cuando hay una compra con productId storeKeySubscription con un estado que no venció. Cuando el usuario no accedió, la lista está vacía.

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

      hasActiveSubscription = purchases.any(
        (element) =>
            element.productId == storeKeySubscription &&
            element.status != Status.expired,
      );

      hasUpgrade = purchases.any(
        (element) => element.productId == storeKeyUpgrade,
      );

      notifyListeners();
    });
  }

Toda la lógica de compra se encuentra en la clase DashPurchases, que es donde se deben aplicar o quitar las suscripciones. Por lo tanto, agrega iapRepo como una propiedad en la clase y asigna iapRepo en el constructor. A continuación, agrega un objeto de escucha directamente en el constructor y quita el objeto de escucha en el método dispose(). Al principio, el objeto de escucha puede ser solo una función vacía. Como IAPRepo es un ChangeNotifier y llamas a notifyListeners() cada vez que cambian las compras en Firestore, siempre se llama al método purchasesUpdate() cuando cambian los productos comprados.

lib/logic/dash_purchases.dart

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

  // Add this.iapRepo as a parameter
  DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    iapRepo.addListener(purchasesUpdate);
    loadPurchases();
  }

  Future<void> loadPurchases() async {
    // Elided.
  }

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);                 // Add this line
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

A continuación, proporciona el IAPRepo al constructor en main.dart.. Puedes obtener el repositorio con context.read porque ya se creó en un Provider.

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),                         // Add this line
          ),
          lazy: false,
        ),

A continuación, escribe el código para la función purchaseUpdate(). En dash_counter.dart,, los métodos applyPaidMultiplier y removePaidMultiplier establecen el multiplicador en 10 o 1, respectivamente, por lo que no tienes que verificar si ya se aplicó la suscripción. Cuando cambia el estado de la suscripción, también actualizas el estado del producto que se puede comprar para mostrar en la página de compra que ya está activo. Establece la propiedad _beautifiedDashUpgrade según si se compra la actualización.

lib/logic/dash_purchases.dart

  void purchasesUpdate() {
    var subscriptions = <PurchasableProduct>[];
    var upgrades = <PurchasableProduct>[];
    // Get a list of purchasable products for the subscription and upgrade.
    // This should be 1 per type.
    if (products.isNotEmpty) {
      subscriptions = products
          .where((element) => element.productDetails.id == storeKeySubscription)
          .toList();
      upgrades = products
          .where((element) => element.productDetails.id == storeKeyUpgrade)
          .toList();
    }

    // Set the subscription in the counter logic and show/hide purchased on the
    // purchases page.
    if (iapRepo.hasActiveSubscription) {
      counter.applyPaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchased);
      }
    } else {
      counter.removePaidMultiplier();
      for (var element in subscriptions) {
        _updateStatus(element, ProductStatus.purchasable);
      }
    }

    // Set the Dash beautifier and show/hide purchased on
    // the purchases page.
    if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
      _beautifiedDashUpgrade = iapRepo.hasUpgrade;
      for (var element in upgrades) {
        _updateStatus(
          element,
          _beautifiedDashUpgrade
              ? ProductStatus.purchased
              : ProductStatus.purchasable,
        );
      }
      notifyListeners();
    }
  }

  void _updateStatus(PurchasableProduct product, ProductStatus status) {
    if (product.status != ProductStatus.purchased) {
      product.status = ProductStatus.purchased;
      notifyListeners();
    }
  }

Ahora te aseguraste de que el estado de la suscripción y la actualización esté siempre actualizado en el servicio de backend y sincronizado con la app. La app actúa en consecuencia y aplica las funciones de suscripción y actualización a tu juego de clics de Dash.

12. Todo listo

¡Felicitaciones! Completaste el codelab. Puedes encontrar el código completo de este codelab en la carpeta android_studio_folder.png complete.

Para obtener más información, prueba los otros codelabs de Flutter.