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

1. Introducción

Última actualización: 11/07/2023

Para agregar compras directas desde la aplicación a una app creada con Flutter, debes configurar correctamente las App Store y Play Store, verificar la compra y otorgar los permisos necesarios, como los beneficios de la suscripción.

En este codelab, agregarás tres tipos de compras directas desde la aplicación (proporcionadas para ti) y verificarás estas compras mediante un backend de Dart con Firebase. La app proporcionada, Dash Clicker, contiene un juego que usa la mascota de Dash como moneda. Agregarás las siguientes opciones de compra:

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

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

La segunda opción mejora a Dash para que sea más atractivo. Solo se debe comprar una vez y está disponible para siempre. Este tipo de compra se denomina no consumible porque la aplicación no puede consumirlo, pero es válida para siempre.

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

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

300123416ebc8dc1.png 8071c08141185461.png 646317a79be08214.png

Qué compilarás

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

Qué aprenderás

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

Requisitos

  • Android Studio 4.1 o versiones posteriores
  • Xcode 12 o versiones posteriores (para el desarrollo de iOS)
  • SDK de Flutter

2. Cómo configurar el entorno de desarrollo

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

Descarga el código

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

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

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

gh repo clone flutter/codelabs flutter-codelabs

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

La estructura de directorios en flutter-codelabs/in_app_purchases contiene una serie de instantáneas de la ubicación que deberías encontrar al final de cada paso con nombre. El código de partida se encuentra en el paso 0, por lo que ubicar los archivos coincidentes es tan fácil como se muestra a continuación:

cd flutter-codelabs/in_app_purchases/step_00

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

Configura el proyecto inicial

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

Las apps que vas a crear deben comunicarse con App Store y Play Store para saber qué productos están disponibles y a qué precio. Cada app se identifica con un ID único. Para la App Store de iOS, esto se denomina identificador de paquete y para Play Store de Android es el ID de aplicación. Por lo general, estos identificadores se realizan con una notación de nombre de dominio inverso. Por ejemplo, cuando se crea una app de compra directa desde la aplicación para flutter.dev, usamos dev.flutter.inapppurchase. Piensa en un identificador para tu aplicación; ahora lo vas a establecer en la configuración del proyecto.

Primero, configura el identificador de paquete para iOS.

Con el proyecto abierto en Android Studio, haz clic con el botón derecho en la carpeta de iOS, selecciona Flutter y abre el módulo en la app de Xcode.

942772eb9a73bfaa.png

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

812f919d965c649a.jpeg

Ahora puedes cerrar Xcode y volver a Android Studio para finalizar la configuración para Android. Para ello, abre el archivo build.gradle en android/app, y cambia tu applicationId (en la línea 37 de la captura de pantalla a continuación) al ID de aplicación, al igual que el identificador de paquete de iOS. Ten en cuenta que no es necesario que los IDs de las tiendas de iOS y Android sean idénticos. Sin embargo, mantenerlos iguales es menos propenso a errores y, por lo tanto, en este codelab también usaremos identificadores idénticos.

5c4733ac560ae8c2.png

3. Instala el complemento

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

Cómo agregar una dependencia en pubspec

Para agregar in_app_purchase a pubspec, agrega in_app_purchase a las dependencias de tu pubspec:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

dependencies:
  ..
  cloud_firestore: ^4.0.3
  firebase_auth: ^4.2.2
  firebase_core: ^2.5.0
  google_sign_in: ^6.0.1
  http: ^0.13.4
  in_app_purchase: ^3.0.1
  intl: ^0.18.0
  provider: ^6.0.2
  ..

Haz clic en pub get para descargar el paquete o ejecutar flutter pub get en la línea de comandos.

4. Configura la App Store

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

Para usar compras directas desde la aplicación, también debes tener un acuerdo activo para aplicaciones pagadas en App Store Connect. Ve a https://appstoreconnect.apple.com/ y haz clic en Acuerdos, Impuestos y Banca.

6e373780e5e24a6f.png

Aquí verás los acuerdos para las aplicaciones gratuitas y pagadas. El estado de las aplicaciones gratuitas debe ser activo y el de las aplicaciones pagadas debe ser nuevo. Asegúrate de leerlas, aceptarlas y de ingresar toda la información necesaria.

8c73197472c9aec.png

Cuando todo esté configurado correctamente, el estado de las aplicaciones pagadas estará activo. Esto es muy importante porque no podrás probar compras directas desde la aplicación sin un acuerdo activo.

4a100bbb8cafdbbf.jpeg

Registrar ID de la app

Crea un identificador nuevo en el portal para desarrolladores de Apple.

55d7e592d9a3fc7b.png

Elegir los IDs de la app

13f125598b72ca77.png

Elegir app

41ac4c13404e2526.png

Proporciona alguna descripción y configura el ID del paquete para que coincida con el valor establecido anteriormente en Xcode.

9d2c940ad80deeef.png

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

Crea una app nueva

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

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

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

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

3ca2b26d4e391a4c.jpeg

Ahora puedes configurar el usuario de la zona de pruebas en tu iPhone desde Configuración > App Store > Cuenta de zona de pruebas.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

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

Ahora configurarás los tres elementos que se pueden comprar:

  • dash_consumable_2k: Es una compra de un producto consumible que se puede comprar muchas veces, lo que le otorga al usuario 2,000 guiones (la moneda integrada en la app) por compra.
  • dash_upgrade_3d: Una "actualización" no consumible compra que solo puede adquirirse una vez y le da al usuario un Dash diferente en cuanto a la estética para que haga clic.
  • dash_subscription_doubler: Es una suscripción que le otorga al usuario el doble de guiones por clic por el tiempo que dure la suscripción.

d156b2f5bac43ca8.png

Ve a Compras directas desde la aplicación > Administrar.

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

  1. Configura dash_consumable_2k como Consumable.

Usa dash_consumable_2k como el ID del producto. El nombre de referencia solo se usa en App Store Connect. Solo debes configurarlo como dash consumable 2k y agregar las localizaciones para la compra. Llama a la compra Spring is in the air con 2000 dashes fly out como descripción.

ec1701834fd8527.png

  1. Configura dash_upgrade_3d como No consumible.

Usa dash_upgrade_3d como el ID del producto. Establece el nombre de referencia en dash upgrade 3d y agrega las localizaciones para la compra. Llama a la compra 3D Dash con Brings your dash back to the future como descripción.

6765d4b711764c30.png

  1. Configura dash_subscription_doubler como una suscripción con renovación automática.

El flujo para las suscripciones es un poco diferente. Primero, debes establecer el nombre de referencia y el ID del producto:

6d29e08dae26a0c4.png

Luego, debes crear un grupo de suscripciones. Cuando varias suscripciones forman parte del mismo grupo, un usuario solo puede suscribirse a una de ellas a la vez, pero puede actualizar las suscripciones o cambiar a una versión inferior con facilidad. Simplemente llama a este grupo subscriptions.

5bd0da17a85ac076.png

A continuación, ingresa la duración de la suscripción y las localizaciones. Asigna el nombre Jet Engine a esta suscripción con la descripción Doubles your clicks. Haz clic en Guardar (Save).

bd1b1d82eeee4cb3.png

Después de hacer clic en el botón Guardar, agrega un precio para la suscripción. Elige el precio que desees.

d0bf39680ef0aa2e.png

Ahora deberías ver las tres compras en la lista de compras:

99d5c4b446e8fecf.png

5. Configura Play Store

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

Cómo crear una app nueva

Sigue estos pasos para crear una app nueva en Google Play Console:

  1. Abre Play Console.
  2. Selecciona Todas las aplicaciones > Crear app
  3. Selecciona un idioma predeterminado y agrega un título para tu app. Escribe el nombre de la app como quieras que aparezca en Google Play. Puedes cambiar el nombre más adelante.
  4. Especifica que tu aplicación es un juego. Puedes cambiarlo más adelante.
  5. Especifica si la aplicación es gratuita o pagada.
  6. Agrega una dirección de correo electrónico para que los usuarios de Play Store puedan comunicarse contigo acerca de esta aplicación.
  7. Completa los lineamientos de contenido y las declaraciones de leyes de exportación de EE.UU.
  8. Selecciona Crear app.

Después de crear tu app, ve al panel y completa todas las tareas de la sección Configura tu app. Aquí, debes proporcionar información sobre la app, como clasificaciones del contenido y capturas de pantalla. 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, tu compilación de lanzamiento debe estar firmada con un elemento que no sea las claves de depuración.

Crea un almacén de claves

Si ya tienes un almacén de claves, continúa con el siguiente paso. De lo contrario, ejecuta el siguiente comando en la línea de comandos para crear uno:

En Mac/Linux, usa el siguiente comando:

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

En Windows, usa el siguiente comando:

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

Este comando almacena el archivo key.jks en tu directorio principal. Si deseas almacenar el archivo en otro lugar, cambia el argumento que pasas al parámetro -keystore. Mantén la

keystore

archivo privado; no la registres en el control de código público.

Haz referencia al almacén de claves desde la app

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

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

Cómo configurar el acceso en Gradle

Para configurar la firma de tu app, edita el archivo <your app dir>/android/app/build.gradle.

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

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         // omitted
   }

Carga el archivo key.properties en el objeto keystoreProperties.

Agrega el siguiente código antes del bloque buildTypes:

   buildTypes {
       release {
           // TODO: Add your own signing config for the release build.
           // Signing with the debug keys for now,
           // so `flutter run --release` works.
           signingConfig signingConfigs.debug
       }
   }

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

   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

Las compilaciones de lanzamiento de tu app ahora se firmarán automáticamente.

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

Cómo subir tu primera compilación

Una vez que tu app esté configurada para firmar, deberías poder compilarla mediante la ejecución del siguiente comando:

flutter build appbundle

Este comando genera una compilación de lanzamiento de forma predeterminada. El resultado se puede encontrar en <your app dir>/build/app/outputs/bundle/release/.

En el panel de Google Play Console, ve a Versión > Prueba > Pruebas cerradas, y crea una nueva versión de prueba cerrada.

En este codelab, deberás mantener la firma de la app por parte de Google, por lo que debes presionar Continuar en Firma de apps de Play para habilitarla.

ba98446d9c5c40e0.png

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

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

Por último, haz clic en Start rollout to Internal testing para activar la versión de prueba interna.

Configura usuarios de prueba

Para poder probar las compras directas desde la aplicación, debes agregar las Cuentas de Google de los verificadores a Google Play Console en dos ubicaciones:

  1. En el segmento de pruebas específico (prueba interna)
  2. Como verificador de licencias

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

a0d0394e85128f84.png

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

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

Luego, agrega los verificadores de licencias:

  1. Vuelve a la vista Todas las apps de Google Play Console.
  2. Ve a Configuración > Prueba de licencias.
  3. Agrega las mismas direcciones de correo electrónico de los verificadores que podrán realizar pruebas de las compras directas desde la aplicación.
  4. Establece License response en RESPOND_NORMALLY.
  5. Haga clic en Guardar cambios.

a1a0f9d3e55ea8da.png

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

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

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

  • dash_consumable_2k: Es una compra de un producto consumible que se puede comprar muchas veces, lo que le otorga al usuario 2,000 guiones (la moneda integrada en la app) por compra.
  • dash_upgrade_3d: Una "actualización" no consumible compra que solo se puede adquirir una vez, lo que le da al usuario un Dash diferente en cuanto a la estética para hacer clic.
  • dash_subscription_doubler: Es una suscripción que le otorga al usuario el doble de guiones por clic por el tiempo que dure la suscripción.

Primero, agrega los consumibles y los no consumibles.

  1. Ve a Google Play Console y selecciona tu aplicación.
  2. Ve a Monetizar > Productos > Productos integrados en la aplicación
  3. Haz clic en Crear producto.c8d66e32f57dee21.png
  4. Ingresa toda la información requerida de tu producto. Asegúrate de que el ID del producto coincida exactamente con el que quieres usar.
  5. Haz clic en Guardar.
  6. Haz clic en Activar.
  7. Repite el proceso para la “actualización” de los productos no consumibles compra.

A continuación, agrega la suscripción:

  1. Ve a Google Play Console y selecciona tu aplicación.
  2. Ve a Monetizar > Productos > Suscripciones.
  3. Haz clic en Crear suscripción.32a6a9eefdb71dd0.png
  4. Ingresa toda la información necesaria para tu suscripción. Asegúrate de que el ID del producto coincida exactamente con el que quieres usar.
  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 contraseñas compras.

Usar un servicio de backend tiene varios beneficios:

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

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

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

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

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

Crea un proyecto de Firebase

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

En la app de backend, las compras se vinculan a un usuario específico; por lo tanto, necesitas autenticación. Para ello, aprovecha el módulo de autenticación de Firebase con el Acceso con Google.

  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.

8babb48832fbef29.png

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

e20553e0de5ac331.png

Configura reglas de Cloud Firestore de la siguiente manera:

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

Cómo configurar Firebase para Flutter

Para instalar Firebase en la app de Flutter, se recomienda usar la CLI de FlutterFire. Sigue las instrucciones que se explican en la página de configuración.

Cuando ejecutes la configuración de Flutterfire, selecciona el proyecto que acabas de crear en el paso anterior.

$ flutterfire configure

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

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

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

Cuando se te solicite anular firebase_options.dart, selecciona Sí.

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

Cómo configurar Firebase para Android: Más pasos

En el panel de Firebase, ve a Descripción general del proyecto,elige Configuración y selecciona la pestaña General.

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

b22d46a759c0c834.png

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

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

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

cd android
./gradlew :app:signingReport

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

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

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

Cómo configurar Firebase para iOS: Más pasos

Abre ios/Runnder.xcworkspace con Xcode. o con el IDE que prefieras.

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

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

Para permitir el Acceso con Google en iOS, agrega la opción de configuración CFBundleURLTypes a tus archivos plist de compilación. (consulta los documentos del paquete google_sign_in para obtener más información). En este caso, los archivos son ios/Runner/Info-Debug.plist y ios/Runner/Info-Release.plist.

Ya se agregó el par clave-valor, pero se deben reemplazar sus valores:

  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 los archivos ios/Runner/Info-Debug.plist y ios/Runner/Info-Release.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 la configuración de Firebase.

7. Escucha actualizaciones sobre compras

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

Cómo escuchar actualizaciones sobre compras

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

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

lib/main.dart

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

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

lib/main.dart

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

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

Debes actualizar un poco la prueba si quieres que siga funcionando. Consulta widget_test.dart en GitHub para obtener el código completo de TestIAPConnection.

test/widget_test.dart

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

En lib/logic/dash_purchases.dart, ve al código de DashPurchases ChangeNotifier. Por el momento, solo hay un DashCounter que puedes agregar a los Dash que compraste.

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

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

class DashPurchases extends ChangeNotifier {
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter);
}

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

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

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

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

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

  Future<void> buy(PurchasableProduct product) async {
    // omitted
  }

  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

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

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

Ahora, la app recibe las actualizaciones de compra, de modo que, en la próxima sección, realizarás una compra.

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

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. Realizar compras

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

Adapta PurchasableProduct

PurchasableProduct muestra un producto simulado. Actualízala para que muestre contenido real reemplazando la clase PurchasableProduct en purchasable_product.dart con el siguiente código:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus {
  purchasable,
  purchased,
  pending,
}

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

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

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

Cómo cargar compras disponibles

Para que un usuario pueda realizar una compra, carga las compras realizadas en la tienda. Primero, verifica si la tienda está disponible. Cuando la tienda no está disponible, se muestra un mensaje de error al usuario si se establece storeState en notAvailable.

lib/logic/dash_purchases.dart

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

Cuando la tienda esté disponible, carga las compras disponibles. Dada la configuración anterior de Firebase, se espera que se muestren storeKeyConsumable, storeKeySubscription, y storeKeyUpgrade. Cuando una compra esperada no esté disponible, imprime esta información en la consola. quizás también quieras enviar esta información al servicio de backend.

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

lib/logic/dash_purchases.dart

import '../constants.dart';

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

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

lib/logic/dash_purchases.dart

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

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

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Muestra los productos que se pueden comprar

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

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

lib/pages/purchase_page.dart

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

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

ca1a9f97c21e552d.png

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

lib/logic/dash_purchases.dart

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

Antes de continuar, crea la variable _beautifiedDashUpgrade y actualiza el método get beautifiedDash para hacer referencia a ella.

lib/logic/dash_purchases.dart

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

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

lib/logic/dash_purchases.dart

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

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

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

9. Configura el backend

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

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

Asegúrate de tener instaladas las siguientes herramientas:

Descripción general del proyecto base

Debido a que algunas partes de este proyecto se consideran fuera del alcance de este codelab, se incluyen en el código de partida. Te recomendamos revisar lo que ya hay en el código de partida antes de comenzar para tener una idea de cómo estructurarás las cosas.

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

Intenta ejecutar el servidor con el siguiente comando:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

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

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

Configura el acceso a Firebase

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

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 compras, debes generar una cuenta de servicio con estos permisos y descargar sus credenciales JSON.

  1. Ve a Google Play Console y comienza desde la página Todas las apps.
  2. Ve a Configuración > Acceso a la API. 317fdfb54921f50e.png Si Google Play Console te solicita crear un proyecto o vincular uno existente, hazlo primero y, luego, regresa a esta página.
  3. Busca la sección en la que puedes definir las cuentas de servicio y haz clic en Crear una cuenta de servicio nueva.1e70d3f8d794bebb.png
  4. En el cuadro de diálogo que aparece, haz clic en el vínculo Google Cloud Platform. 8c9536336dd9e9b4.png
  5. Elige tu proyecto. Si no ves esta opción, asegúrate de haber accedido a la Cuenta de Google correcta en la lista desplegable Cuenta ubicada en la parte superior derecha. 3fb3a25bad803063.png
  6. Después de seleccionar tu proyecto, haz clic en + Crear cuenta de servicio en la barra de menú superior. 62fe4c3f8644acd8.png
  7. Proporciona un nombre para la cuenta de servicio y, de forma opcional, una descripción para que recuerdes para qué sirve y, luego, vayas al siguiente paso. 8a92d5d6a3dff48c.png
  8. Asigna el rol de Editor a la cuenta de servicio. 6052b7753667ed1a.png
  9. Finaliza el asistente, vuelve a la página Acceso a la API en Play Console y haz clic en Actualizar cuentas de servicio. Deberías ver la cuenta que acabas de crear en la lista. 5895a7db8b4c7659.png
  10. Haz clic en Otorgar acceso a la nueva cuenta de servicio.
  11. Desplázate hacia abajo en la página siguiente hasta el bloque Datos financieros. Selecciona Ver datos financieros, pedidos y respuestas a la encuesta de cancelación y Administrar pedidos y suscripciones. 75b22d0201cf67e.png
  12. Haz clic en Invitar a un usuario. 70ea0b1288c62a59.png
  13. Ahora que la cuenta está configurada, solo debes generar algunas credenciales. En la consola de Cloud, busca tu cuenta de servicio en la lista correspondiente, haz clic en los tres puntos verticales y elige Administrar claves. 853ee186b0e9954e.png
  14. Crea una clave JSON nueva y descárgala. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Cambia el nombre del archivo descargado a service-account-google-play.json, y muévelo al directorio assets/.

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

Configura el acceso a la App Store de Apple

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

  1. Abre App Store Connect.
  2. Ve a Mis apps y selecciona tu app.
  3. En la barra de navegación de la barra lateral, ve a Compras directas desde la aplicación > Administrar.
  4. En la esquina superior derecha de la lista, haz clic en Secreto compartido específico de la app.
  5. Genera un secreto nuevo y cópialo.
  6. Abre lib/constants.dart, y reemplaza el valor de appStoreSharedSecret por el secreto compartido que acabas de generar.

d8b8042470aaeff.png

b72f4565750e2f40.png

Archivo de configuración de constantes

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

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

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

10. Verifica las compras

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

Para ambas tiendas, la aplicación recibe un token cuando se realiza una compra.

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

El servicio de backend puede elegir almacenar la compra y responder a la aplicación si la compra fue válida o no.

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

Cómo configurar el lado de Flutter

Configura la autenticación

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

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

class PurchasePage extends StatelessWidget {  
  const PurchasePage({Key? key}) : super(key: key);

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

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

Llama al extremo de verificación desde la app

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

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

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

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

  DashPurchases(this.counter, this.firebaseNotifier) {
    // omitted
  }

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

lib/main.dart

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

Agrega un método get para el usuario en el FirebaseNotifier de modo que puedas pasar el ID del usuario a la función de verificación de compra.

lib/logic/firebase_notifier.dart

  User? get user => FirebaseAuth.instance.currentUser;

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

lib/logic/dash_purchases.dart

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

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

lib/logic/dash_purchases.dart

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

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

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

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

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

Configura el servicio de backend

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

Cómo compilar controladores de compra

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

be50c207c5a2a519.png

Para comenzar, agrega un archivo purchase_handler.dart a la carpeta lib/, en la que defines una clase abstracta PurchaseHandler con dos métodos abstractos para verificar dos tipos diferentes de compras: suscripciones y no suscripciones.

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {

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

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

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

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

Además, para facilitar el uso de estos controladores de compra, agrega un método verifyPurchase() que se pueda usar para suscripciones y no suscripciones:

lib/purchase_handler.dart

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

Ahora, puedes llamar a verifyPurchase para ambos casos, pero aún tienes implementaciones separadas.

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

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

El ProductType puede ser una suscripción o no.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

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

lib/products.dart

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

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

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

lib/google_play_purchase_handler.dart

import 'dart:async';

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

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

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

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
  );

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

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

Por ahora, muestra true para los métodos del controlador. llegarás a ellos más tarde.

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

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

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

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

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

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

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

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

Cómo usar controladores de compra

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

bin/server.dart

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router);
}

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

El código anterior hace lo siguiente:

  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:
  3. userId: ID del usuario con sesión activa
  4. source: Se usa la tienda, ya sea app_store o google_play.
  5. productData: Se obtiene del productDataMap que creaste antes.
  6. token: Contiene los datos de verificación que se enviarán a las tiendas.
  7. Llama al método verifyPurchase, ya sea para GooglePlayPurchaseHandler o AppStorePurchaseHandler, según la fuente.
  8. Si la verificación se realizó correctamente, el método muestra un Response.ok al cliente.
  9. Si la verificación falla, el método muestra un Response.internalServerError al cliente.

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

bin/server.dart

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

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

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

Verifica las compras de Android: Implementa el manual de compra

A continuación, continúa con la implementación del controlador de compras de Google Play.

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

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

lib/google_play_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

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

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

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

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

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

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

lib/google_play_purchase_handler.dart

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

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

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

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

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

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

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

lib/google_play_purchase_handler.dart

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

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

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

Tus compras en Google Play ya deberían estar verificadas y almacenadas en la base de datos.

Luego, continúa con las compras en la App Store para iOS.

Verifica las compras de iOS: Implementa el controlador de compra

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

Para comenzar, crea la instancia ITunesApi. Utiliza la configuración de la zona de pruebas y habilita los registros para facilitar la depuración de errores.

lib/app_store_purchase_handler.dart

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

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

lib/app_store_purchase_handler.dart

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

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

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

Ahora, implementa handleValidation:

lib/app_store_purchase_handler.dart

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

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

Ejecuta el backend

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

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

11. Haz un seguimiento de las compras

La forma recomendada de hacer un seguimiento compras están en el servicio de backend. Esto se debe a que tu backend puede responder a eventos del almacén y, por lo tanto, es menos propenso a encontrarse con información desactualizada debido al almacenamiento en caché, además de ser menos susceptible de ser manipulado.

Primero, configura el procesamiento de los eventos de almacenamiento en el backend con el backend de Dart que estuviste compilando.

Procesa eventos de almacenamiento en el backend

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

Cómo procesar eventos de Facturación Google Play

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

Debido a que esta es una función específica de Google Play, debes incluirla en el GooglePlayPurchaseHandler.

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

lib/google_play_purchase_handler.dart

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

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

lib/google_play_purchase_handler.dart

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

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

Timer está configurado para llamar al método _pullMessageFromSubSub cada diez segundos. Puedes ajustar la duración como prefieras.

Luego, crea el _pullMessageFromSubSub.

lib/google_play_purchase_handler.dart

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

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

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

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

Este método decodifica los mensajes entrantes y obtiene la información actualizada sobre cada compra (de suscripciones y no suscripciones) y llama a los handleSubscription o handleNonSubscription existentes si es necesario.

Se debe confirmar cada mensaje con el método _askMessage.

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

bin/server.dart

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

Luego, crea la instancia de PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Por último, pásalo al constructor GooglePlayPurchaseHandler:

bin/server.dart

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

Configuración de Google Play

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

Primero, crea un tema de Pub/Sub:

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

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

Procesa eventos de facturación de la App Store

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

Este codelab se enfoca en la segunda solución porque tendrías que exponer tu servidor a Internet para implementar el webhook.

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

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

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

AppStorePurchaseHandler(
  this.iapRepository,
  this.appStoreServerAPI, // new
)

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

lib/app_store_purchase_handler.dart

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

Luego, crea el método _pullStatus como se indica a continuación:

lib/app_store_purchase_handler.dart

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

Este método funciona de la siguiente manera:

  1. Obtiene la lista de suscripciones activas de Firestore mediante el 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 para 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 del servidor de App Store:

bin/server.dart

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

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

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

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


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

Configuración de App Store

A continuación, configura la App Store:

  1. Accede a App Store Connect y selecciona Usuarios y acceso.
  2. Ve a Tipo de clave > Compra directa desde la aplicación
  3. Presiona el signo "más". para agregar una nueva.
  4. Asígnale un nombre, por ejemplo, “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 clave de la clave recién creada y establécelo en la constante appStoreKeyId en el archivo lib/constants.dart.
  8. Copia el ID de la entidad emisora en la parte superior de la lista de claves y configúralo como la constante appStoreIssuerId en el archivo lib/constants.dart.

9540ea9ada3da151.png

Cómo hacer un seguimiento de las compras en el dispositivo

La forma más segura de hacer un seguimiento de tus compras es a través del servidor, ya que el cliente es difícil de proteger, pero debes tener alguna manera de devolverle la información al cliente para que la app pueda actuar en función de la información del estado de la suscripción. Cuando almacenas las compras en Firestore, puedes sincronizar fácilmente los datos con el cliente y mantenerlos actualizados automáticamente.

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

lib/repo/iap_repo.dart

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

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

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

      notifyListeners();
    });
  }

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

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

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

lib/main.dart

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

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

lib/logic/dash_purchases.dart

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

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

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

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

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

12. Todo listo

¡Felicitaciones! Completaste el codelab. Puedes encontrar el código completo de este codelab en la android_studio_folder.pngcarpeta completa.

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