Ajouter des achats via une application à votre application Flutter

1. Introduction

Pour ajouter des achats via une application Flutter, vous devez configurer correctement les plates-formes App Store et Play Store, vérifier l'achat et accorder les autorisations nécessaires, comme les avantages des abonnements.

Dans cet atelier de programmation, vous allez ajouter trois types d'achats via une application (fournie) et les valider à l'aide d'un backend Dart avec Firebase. L'application fournie, Dash Clicker, contient un jeu qui utilise la mascotte Dash comme monnaie. Vous ajouterez les options d'achat suivantes :

  1. Option d'achat répétable pour 2 000 Dashes à la fois.
  2. Achat d'une mise à niveau ponctuelle pour transformer l'ancien style de Dash en un style moderne.
  3. Abonnement qui double les clics générés automatiquement.

La première option d'achat offre à l'utilisateur un avantage direct de 2 000 Dashes. Ces articles sont directement disponibles pour l'utilisateur et peuvent être achetés plusieurs fois. C'est ce qu'on appelle un produit consommable, car il est directement consommé et peut l'être plusieurs fois.

La deuxième option permet de mettre à niveau le tableau de bord pour le rendre plus esthétique. Vous n'avez besoin de l'acheter qu'une seule fois, et il est disponible pour toujours. On parle d'achat non consommable, car il ne peut pas être consommé par l'application, mais il est valable pour toujours.

La troisième et dernière option d'achat est un abonnement. Tant que l'abonnement est actif, l'utilisateur reçoit des Dashs plus rapidement, mais lorsqu'il cesse de payer l'abonnement, il perd également ces avantages.

Le service de backend (également fourni) s'exécute en tant qu'application Dart, vérifie que les achats sont effectués et les stocke à l'aide de Firestore. Firestore est utilisé pour faciliter le processus, mais dans votre application de production, vous pouvez utiliser n'importe quel type de service de backend.

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

Ce que vous allez faire

  • Vous allez étendre une application pour qu'elle accepte les achats consommables et les abonnements.
  • Vous allez également étendre une application backend Dart pour valider et stocker les articles achetés.

Points abordés

  • Configurer l'App Store et le Play Store avec des produits achetables
  • Découvrez comment communiquer avec les magasins pour valider les achats et les stocker dans Firestore.
  • Gérer les achats dans votre application

Prérequis

  • Android Studio
  • Xcode (pour le développement sur iOS)
  • SDK Flutter

2. Configurer l'environnement de développement

Pour commencer cet atelier de programmation, téléchargez le code et modifiez l'identifiant du bundle pour iOS et le nom du package pour Android.

Télécharger le code

Pour cloner le dépôt GitHub à partir de la ligne de commande, utilisez la commande suivante :

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

Si l'outil CLI de GitHub est installé, utilisez la commande suivante :

gh repo clone flutter/codelabs flutter-codelabs

L'exemple de code est cloné dans un répertoire flutter-codelabs qui contient le code d'une collection d'ateliers de programmation. Le code de cet atelier de programmation se trouve dans flutter-codelabs/in_app_purchases.

La structure de répertoire sous flutter-codelabs/in_app_purchases contient une série d'instantanés de l'état dans lequel vous devriez vous trouver à la fin de chaque étape nommée. Le code de démarrage se trouve à l'étape 0. Pour y accéder, procédez comme suit :

cd flutter-codelabs/in_app_purchases/step_00

Si vous souhaitez passer à une étape ultérieure ou voir à quoi doit ressembler un élément après une étape, consultez le répertoire portant le nom de l'étape qui vous intéresse. Le code de la dernière étape se trouve dans le dossier complete.

Configurer le projet de démarrage

Ouvrez le projet de départ à partir de step_00/app dans votre IDE favori. Nous avons utilisé Android Studio pour les captures d'écran, mais Visual Studio Code est également une excellente option. Quel que soit l'éditeur utilisé, assurez-vous que les derniers plug-ins Dart et Flutter sont installés.

Les applications que vous allez créer doivent communiquer avec l'App Store et le Play Store pour connaître les produits disponibles et leur prix. Chaque application est identifiée par un ID unique. Pour l'App Store iOS, il s'agit de l'identifiant du bundle, et pour le Play Store Android, il s'agit de l'ID de l'application. Ces identifiants sont généralement créés à l'aide d'une notation de nom de domaine inversée. Par exemple, lorsque vous effectuez un achat via l'application flutter.dev, vous utilisez dev.flutter.inapppurchase. Choisissez un identifiant pour votre application, que vous allez maintenant définir dans les paramètres du projet.

Commencez par configurer l'identifiant du bundle pour iOS. Pour ce faire, ouvrez le fichier Runner.xcworkspace dans l'application Xcode.

a9fbac80a31e28e0.png

Dans la structure de dossiers de Xcode, le projet Runner se trouve en haut, et les cibles Flutter, Runner et Products se trouvent sous le projet Runner. Double-cliquez sur Runner pour modifier les paramètres de votre projet, puis cliquez sur Signing & Capabilities (Signature et fonctionnalités). Saisissez l'identifiant du bundle que vous venez de choisir dans le champ Team (Équipe) pour définir votre équipe.

812f919d965c649a.jpeg

Vous pouvez maintenant fermer Xcode et revenir à Android Studio pour terminer la configuration d'Android. Pour ce faire, ouvrez le fichier build.gradle.kts sous android/app, et remplacez votre applicationId (ligne 24 dans la capture d'écran ci-dessous) par l'ID d'application, qui est le même que l'identifiant du bundle iOS. Notez que les ID des plates-formes iOS et Android ne doivent pas nécessairement être identiques. Toutefois, il est moins risqué de les conserver identiques. C'est pourquoi nous utiliserons également des identifiants identiques dans cet atelier de programmation.

e320a49ff2068ac2.png

3. Installer le plug-in

Dans cette partie de l'atelier de programmation, vous allez installer le plug-in in_app_purchase.

Ajouter une dépendance dans pubspec

Ajoutez in_app_purchase au fichier pubspec en ajoutant in_app_purchase aux dépendances de votre projet :

$ 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.

Ouvrez votre fichier pubspec.yaml et vérifiez que in_app_purchase est désormais listé en tant qu'entrée sous dependencies et que in_app_purchase_platform_interface est listé sous 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. Configurer l'App Store

Pour configurer les achats via une application et les tester sur iOS, vous devez créer une application dans l'App Store et y créer des produits achetables. Vous n'avez rien à publier ni à envoyer l'application à Apple pour examen. Pour cela, vous devez disposer d'un compte de développeur. Si vous n'en avez pas, inscrivez-vous au programme Apple Developer.

Pour utiliser les achats via une application, vous devez également disposer d'un contrat actif pour les applications payantes dans App Store Connect. Accédez à https://appstoreconnect.apple.com/, puis cliquez sur Accords, impôts et paiements.

11db9fca823e7608.png

Vous verrez ici les contrats pour les applications sans frais et payantes. L'état des applications sans frais doit être "Actif", et celui des applications payantes doit être "Nouveau". Assurez-vous de lire et d'accepter les conditions, et de saisir toutes les informations requises.

74c73197472c9aec.png

Si tout est configuré correctement, l'état des applications payantes sera "Actif". C'est très important, car vous ne pourrez pas essayer les achats via l'application sans contrat actif.

4a100bbb8cafdbbf.jpeg

Enregistrer l'ID de l'application

Créez un identifiant dans le portail des développeurs Apple. Accédez à developer.apple.com/account/resources/identifiers/list, puis cliquez sur l'icône Plus à côté de l'en-tête Identifiants.

55d7e592d9a3fc7b.png

Choisir des ID d'application

13f125598b72ca77.png

Sélectionner une appli

41ac4c13404e2526.png

Fournissez une description et définissez l'ID du bundle sur la même valeur que celle définie précédemment dans Xcode.

9d2c940ad80deeef.png

Pour savoir comment créer un ID d'application, consultez l'aide pour les comptes de développeur.

Créer une application

Créez une application dans App Store Connect avec votre identifiant de bundle unique.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Pour obtenir plus d'informations sur la création d'une application et la gestion des contrats, consultez l'aide d'App Store Connect.

Pour tester les achats via l'application, vous avez besoin d'un utilisateur test dans le bac à sable. Cet utilisateur de test ne doit pas être associé à iTunes. Il ne sert qu'à tester les achats via l'application. Vous ne pouvez pas utiliser une adresse e-mail déjà associée à un compte Apple. Dans Utilisateurs et accès, accédez à Bac à sable pour créer un compte bac à sable ou gérer les identifiants Apple bac à sable existants.

2ba0f599bcac9b36.png

Vous pouvez maintenant configurer votre utilisateur Sandbox sur votre iPhone en accédant à Paramètres > Développeur > Compte Apple Sandbox.

74a545210b282ad8.png eaa67752f2350f74.png

Configurer vos achats via une application

Vous allez maintenant configurer les trois articles achetables :

  • dash_consumable_2k : achat consommable qui peut être effectué plusieurs fois et qui accorde à l'utilisateur 2 000 Dashes (la devise de l'application) par achat.
  • dash_upgrade_3d : achat d'une "mise à niveau" non consommable qui ne peut être effectué qu'une seule fois et qui permet à l'utilisateur de cliquer sur un Dash à l'apparence différente.
  • dash_subscription_doubler : abonnement qui accorde à l'utilisateur deux fois plus de Dashs par clic pendant toute la durée de l'abonnement.

a118161fac83815a.png

Accédez à Achats via une application.

Créez vos achats via l'application avec les ID spécifiés :

  1. Configurez dash_consumable_2k en tant que Consommable. Utilisez dash_consumable_2k comme ID de produit. Le nom de référence n'est utilisé que dans App Store Connect. Définissez-le simplement sur dash consumable 2k. 1f8527fc03902099.png Configurez votre disponibilité. Le produit doit être disponible dans le pays de l'utilisateur du bac à sable. bd6b2ce2d9314e6e.png Ajoutez un prix et définissez-le sur $1.99 ou l'équivalent dans une autre devise. 926b03544ae044c4.png Ajoutez vos traductions pour l'achat. Appelez l'achat Spring is in the air avec 2000 dashes fly out comme description. e26dd4f966dcfece.png Ajoutez une capture d'écran de l'avis. Le contenu n'a pas d'importance, sauf si le produit est envoyé pour examen. Toutefois, il est nécessaire pour que le produit soit à l'état "Prêt à être envoyé", ce qui est indispensable lorsque l'application récupère des produits sur l'App Store. 25171bfd6f3a033a.png
  2. Configurez dash_upgrade_3d comme non consommable. Utilisez dash_upgrade_3d comme ID de produit. Définissez le nom de référence sur dash upgrade 3d. Appelez l'achat 3D Dash avec Brings your dash back to the future comme description. Définissez le prix sur $0.99. Configurez la disponibilité et importez la capture d'écran de l'avis de la même manière que pour le produit dash_consumable_2k. 83878759f32a7d4a.png
  3. Configurez dash_subscription_doubler comme abonnement à renouvellement automatique. La procédure pour les abonnements est légèrement différente. Vous devez d'abord créer un groupe d'abonnements. Lorsqu'un même groupe inclut plusieurs abonnements, un utilisateur ne peut s'abonner qu'à l'un d'eux à la fois, mais il peut passer d'un abonnement à un autre. Appelez simplement ce groupe subscriptions. 393a44b09f3cd8bf.png Ajoutez la localisation pour le groupe d'abonnements. 595aa910776349bd.png Vous allez maintenant créer l'abonnement. Définissez le nom de référence sur dash subscription doubler et l'ID du produit sur dash_subscription_doubler. 7bfff7bbe11c8eec.png Sélectionnez ensuite la durée de l'abonnement (une semaine) et les langues. Nommez cet abonnement Jet Engine et ajoutez la description Doubles your clicks. Définissez le prix sur $0.49. Configurez la disponibilité et importez la capture d'écran de l'avis de la même manière que pour le produit dash_consumable_2k. 44d18e02b926a334.png

Les produits devraient maintenant s'afficher dans les listes :

17f242b5c1426b79.png d71da951f595054a.png

5. Configurer le Play Store

Comme pour l'App Store, vous aurez également besoin d'un compte de développeur pour le Play Store. Si vous n'en avez pas encore, créez-en un.

Créer une application

Créez une application dans la Google Play Console :

  1. Ouvrez la Play Console.
  2. Sélectionnez Toutes les applications > Créer une application.
  3. Sélectionnez une langue par défaut et donnez un titre à votre application. Indiquez le nom de votre application tel qu'il doit apparaître sur Google Play. Vous pourrez modifier le nom ultérieurement.
  4. Indiquez que votre application est un jeu. Vous pourrez modifier ce choix ultérieurement.
  5. Précisez si votre application est sans frais ou payante.
  6. Remplissez les déclarations "Consignes relatives au contenu" et "Lois des États-Unis en matière d'exportation".
  7. Sélectionnez Créer une application.

Une fois votre application créée, accédez au tableau de bord et effectuez toutes les tâches de la section Configurer votre application. Vous y fournissez des informations sur votre application, comme la classification du contenu et des captures d'écran. 13845badcf9bc1db.png

Signer l'application

Pour pouvoir tester les achats via l'application, vous devez avoir importé au moins une version sur Google Play.

Pour ce faire, vous devez signer votre version avec autre chose que les clés de débogage.

Créer un keystore

Si vous disposez déjà d'un keystore, passez à l'étape suivante. Si ce n'est pas le cas, créez-en un en exécutant la commande suivante sur la ligne de commande.

Sous Mac/Linux, utilisez la commande suivante :

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

Sous Windows, utilisez la commande suivante :

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

Cette commande stocke le fichier key.jks dans votre répertoire d'accueil. Si vous souhaitez stocker le fichier ailleurs, modifiez l'argument que vous transmettez au paramètre -keystore. Conserver le

keystore

Fichier privé : ne l'enregistrez pas dans un système de contrôle des sources public.

Faire référence au keystore depuis l'application

Créez un fichier nommé <your app dir>/android/key.properties contenant une référence à votre keystore :

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>

Configurer la signature dans Gradle

Configurez la signature de votre application en modifiant le fichier <your app dir>/android/app/build.gradle.kts.

Ajoutez les informations du keystore à partir de votre fichier de propriétés avant le bloc 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
}

Chargez le fichier key.properties dans l'objet keystoreProperties.

Mettez à jour le bloc buildTypes comme suit :

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

Configurez le bloc signingConfigs dans le fichier build.gradle.kts de votre module avec les informations de configuration de la signature :

   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")
        }
    }

Les versions finales de votre application seront désormais signées automatiquement.

Pour en savoir plus sur la signature de votre application, consultez Signer votre application sur developer.android.com.

Importer votre premier build

Une fois votre application configurée pour la signature, vous devriez pouvoir la compiler en exécutant la commande suivante :

flutter build appbundle

Cette commande génère une version Release par défaut. Le résultat se trouve dans <your app dir>/build/app/outputs/bundle/release/.

Dans le tableau de bord de la Google Play Console, accédez à Tester et publier > Tests > Tests fermés, puis créez une version de test fermée.

Ensuite, importez le bundle d'application app-release.aab généré par la commande de compilation.

Cliquez sur Enregistrer, puis sur Examiner la version.

Enfin, cliquez sur Démarrer le déploiement vers le test fermé pour activer la version de test fermé.

Configurer des utilisateurs de test

Pour pouvoir tester les achats via l'application, les comptes Google de vos testeurs doivent être ajoutés à deux endroits dans la Google Play Console :

  1. Sur le canal de test spécifique (test interne)
  2. En tant que testeur de licence

Commencez par ajouter le testeur au canal de test interne. Revenez à Tester et publier > Tests > Tests internes, puis cliquez sur l'onglet Testeurs.

a0d0394e85128f84.png

Créez une liste de diffusion en cliquant sur Créer une liste de diffusion. Donnez un nom à la liste, puis ajoutez les adresses e-mail des comptes Google qui doivent avoir accès aux achats via l'application de test.

Cochez ensuite la case à côté de la liste, puis cliquez sur Enregistrer les modifications.

Ajoutez ensuite les testeurs de licence :

  1. Revenez à la vue Toutes les applications de la Google Play Console.
  2. Accédez à Paramètres > Test de licence.
  3. Ajoutez les adresses e-mail des testeurs qui doivent pouvoir tester les achats via l'application.
  4. Définissez Réponse de licence sur RESPOND_NORMALLY.
  5. Cliquez sur Enregistrer les modifications.

a1a0f9d3e55ea8da.png

Configurer vos achats via une application

Vous allez maintenant configurer les articles qui peuvent être achetés dans l'application.

Comme dans l'App Store, vous devez définir trois achats différents :

  • dash_consumable_2k : achat consommable qui peut être effectué plusieurs fois et qui accorde à l'utilisateur 2 000 Dashes (la devise de l'application) par achat.
  • dash_upgrade_3d : achat d'une "mise à niveau" non consommable qui ne peut être effectué qu'une seule fois et qui permet à l'utilisateur de cliquer sur un Dash à l'apparence différente.
  • dash_subscription_doubler : abonnement qui accorde à l'utilisateur deux fois plus de Dashs par clic pendant toute la durée de l'abonnement.

Commencez par ajouter le produit consommable et le produit non consommable.

  1. Accédez à la Google Play Console, puis sélectionnez votre application.
  2. Accédez à Monétiser > Produits > Produits intégrés.
  3. Cliquez sur Créer un produitc8d66e32f57dee21.png.
  4. Saisissez toutes les informations requises pour votre produit. Assurez-vous que l'ID de produit correspond exactement à celui que vous souhaitez utiliser.
  5. Cliquez sur Enregistrer.
  6. Cliquez sur Activer.
  7. Répétez la procédure pour l'achat "Mise à niveau" non consommable.

Ensuite, ajoutez l'abonnement :

  1. Accédez à la Google Play Console, puis sélectionnez votre application.
  2. Accédez à Monétiser > Produits > Abonnements.
  3. Cliquez sur Créer un abonnement32a6a9eefdb71dd0.png.
  4. Saisissez toutes les informations requises pour votre abonnement. Assurez-vous que l'ID de produit correspond exactement à celui que vous souhaitez utiliser.
  5. Cliquez sur Enregistrer.

Vos achats devraient maintenant être configurés dans la Play Console.

6. Configurer Firebase

Dans cet atelier de programmation, vous allez utiliser un service de backend pour valider et suivre les achats des utilisateurs.

L'utilisation d'un service de backend présente plusieurs avantages :

  • Vous pouvez valider les transactions de manière sécurisée.
  • Vous pouvez réagir aux événements de facturation des plates-formes de téléchargement d'applications.
  • Vous pouvez suivre les achats dans une base de données.
  • Les utilisateurs ne pourront pas tromper votre application pour obtenir des fonctionnalités premium en rembobinant l'horloge système.

Bien qu'il existe de nombreuses façons de configurer un service de backend, vous le ferez à l'aide de fonctions Cloud et de Firestore, en utilisant Firebase de Google.

L'écriture du backend est considérée comme hors champ pour cet atelier de programmation. Le code de démarrage inclut donc déjà un projet Firebase qui gère les achats de base pour vous aider à démarrer.

Les plug-ins Firebase sont également inclus dans l'application de démarrage.

Il ne vous reste plus qu'à créer votre propre projet Firebase, à configurer l'application et le backend pour Firebase, puis à déployer le backend.

Créer un projet Firebase

Accédez à la console Firebase et créez un projet Firebase. Pour cet exemple, appelez le projet "Dash Clicker".

Dans l'application backend, vous associez les achats à un utilisateur spécifique. Vous avez donc besoin d'une authentification. Pour ce faire, utilisez le module d'authentification de Firebase avec Google Sign-In.

  1. Dans le tableau de bord Firebase, accédez à Authentification et activez-la si nécessaire.
  2. Accédez à l'onglet Mode de connexion et activez le fournisseur de connexion Google.

fe2e0933d6810888.png

Étant donné que vous utiliserez également la base de données Firestore de Firebase, activez-la également.

d02d641821c71e2c.png

Définissez les règles Cloud Firestore comme suit :

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
    }
  }
}

Configurer Firebase pour Flutter

La méthode recommandée pour installer Firebase sur l'application Flutter consiste à utiliser la CLI FlutterFire. Suivez les instructions de la page de configuration.

Lorsque vous exécutez flutterfire configure, sélectionnez le projet que vous venez de créer à l'étape précédente.

$ 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>

Ensuite, activez iOS et Android en sélectionnant les deux plates-formes.

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

Lorsque vous êtes invité à remplacer firebase_options.dart, sélectionnez "Oui".

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

Configurer Firebase pour Android : étapes supplémentaires

Dans le tableau de bord Firebase, accédez à Présentation du projet, choisissez Paramètres, puis sélectionnez l'onglet Général.

Faites défiler la page jusqu'à Vos applications, puis sélectionnez l'application dashclicker (android).

b22d46a759c0c834.png

Pour autoriser Google Sign-in en mode débogage, vous devez fournir l'empreinte SHA-1 de votre certificat de débogage.

Obtenir le hachage de votre certificat de signature de débogage

À la racine de votre projet d'application Flutter, accédez au dossier android/, puis générez un rapport de signature.

cd android
./gradlew :app:signingReport

Une longue liste de clés de signature s'affiche. Comme vous recherchez le hachage du certificat de débogage, recherchez le certificat dont les propriétés Variant et Config sont définies sur debug. Il est probable que le fichier keystore se trouve dans votre dossier personnel, sous .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

Copiez le hachage SHA-1 et renseignez le dernier champ de la boîte de dialogue modale d'envoi de l'application.

Enfin, exécutez à nouveau la commande flutterfire configure pour mettre à jour l'application et inclure la configuration de signature.

$ 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

Configurer Firebase pour iOS : étapes supplémentaires

Ouvrez le ios/Runner.xcworkspace avec Xcode. ou avec l'IDE de votre choix.

Dans VSCode, effectuez un clic droit sur le dossier ios/, puis sur open in xcode.

Dans Android Studio, effectuez un clic droit sur le dossier ios/, puis cliquez sur flutter, suivi de l'option open iOS module in Xcode.

Pour autoriser la connexion avec Google sur iOS, ajoutez l'option de configuration CFBundleURLTypes à vos fichiers plist de compilation. (Pour en savoir plus, consultez la documentation sur le package google_sign_in.) Dans ce cas, le fichier est ios/Runner/Info.plist.

La paire clé-valeur a déjà été ajoutée, mais ses valeurs doivent être remplacées :

  1. Obtenez la valeur de REVERSED_CLIENT_ID à partir du fichier GoogleService-Info.plist, sans l'élément <string>..</string> qui l'entoure.
  2. Remplacez la valeur dans votre fichier ios/Runner/Info.plist sous la clé 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>

Vous avez terminé la configuration de Firebase.

7. Écouter les notifications sur les achats

Dans cette partie de l'atelier de programmation, vous allez préparer l'application à l'achat de produits. Ce processus inclut l'écoute des mises à jour et des erreurs d'achat après le démarrage de l'application.

Écouter les notifications d'achat

Dans main.dart,, recherchez le widget MyHomePage qui comporte un Scaffold avec un BottomNavigationBar contenant deux pages. Cette page crée également trois Provider pour DashCounter, DashUpgrades, et DashPurchases. DashCounter suit le nombre actuel de tirets et l'incrémente automatiquement. DashUpgrades gère les mises à niveau que vous pouvez acheter avec des Soutiens. Cet atelier de programmation est consacré à DashPurchases.

Par défaut, l'objet d'un fournisseur est défini lors de sa première demande. Cet objet écoute les mises à jour des achats directement au démarrage de l'application. Désactivez donc le chargement différé sur cet objet avec lazy: false :

lib/main.dart

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

Vous avez également besoin d'une instance de InAppPurchaseConnection. Toutefois, pour que l'application reste testable, vous avez besoin d'un moyen de simuler la connexion. Pour ce faire, créez une méthode d'instance qui peut être remplacée dans le test et ajoutez-la à 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!;
  }
}

Mettez à jour le test comme suit :

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.

Dans lib/logic/dash_purchases.dart, accédez au code de DashPurchasesChangeNotifier. Pour le moment, vous ne pouvez ajouter qu'un seul DashCounter à vos Dashs achetés.

Ajoutez une propriété d'abonnement au flux, _subscription (de type StreamSubscription<List<PurchaseDetails>> _subscription;), IAPConnection.instance, et les importations. Le code obtenu doit se présenter comme suit :

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();
  }
}

Le mot clé late est ajouté à _subscription, car _subscription est initialisé dans le constructeur. Ce projet est configuré pour être non-nullable par défaut (NNBD), ce qui signifie que les propriétés qui ne sont pas déclarées comme acceptant la valeur null doivent avoir une valeur non nulle. Le qualificatif late vous permet de retarder la définition de cette valeur.

Dans le constructeur, obtenez le flux purchaseUpdated et commencez à l'écouter. Dans la méthode dispose(), annulez l'abonnement au flux.

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.
}

L'application reçoit désormais les mises à jour des achats. Dans la section suivante, vous allez donc effectuer un achat.

Avant de continuer, exécutez les tests avec flutter test" pour vérifier que tout est correctement configuré.

$ flutter test

00:01 +1: All tests passed!

8. Effectuer des achats

Dans cette partie de l'atelier de programmation, vous allez remplacer les produits fictifs existants par de vrais produits achetables. Ces produits sont chargés depuis les magasins, affichés dans une liste et achetés lorsque l'utilisateur appuie dessus.

Adapter PurchasableProduct

PurchasableProduct affiche un produit fictif. Pour afficher le contenu réel, remplacez la classe PurchasableProduct dans purchasable_product.dart par le code suivant :

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;
}

Dans dash_purchases.dart,, supprimez les achats fictifs et remplacez-les par une liste vide, List<PurchasableProduct> products = [];.

Charger les achats disponibles

Pour permettre à un utilisateur d'effectuer un achat, chargez les achats depuis le magasin. Commencez par vérifier si le magasin est disponible. Lorsque le magasin n'est pas disponible, la définition de storeState sur notAvailable affiche un message d'erreur à l'utilisateur.

lib/logic/dash_purchases.dart

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

Lorsque le magasin est disponible, chargez les achats disponibles. Étant donné la configuration précédente de Google Play et de l'App Store, vous devriez voir storeKeyConsumable, storeKeySubscription, et storeKeyUpgrade. Lorsqu'un achat attendu n'est pas disponible, imprimez ces informations dans la console. Vous pouvez également envoyer ces informations au service de backend.

La méthode await iapConnection.queryProductDetails(ids) renvoie à la fois les ID introuvables et les produits achetables trouvés. Utilisez le productDetails de la réponse pour mettre à jour l'UI et définissez StoreState sur 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();
  }

Appelez la fonction loadPurchases() dans le constructeur :

lib/logic/dash_purchases.dart

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

Enfin, remplacez la valeur du champ storeState (StoreState.available) par StoreState.loading:.

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Afficher les produits disponibles à l'achat

Prenons l'exemple du fichier purchase_page.dart. Le widget PurchasePage affiche _PurchasesLoading, _PurchaseList, ou _PurchasesNotAvailable, en fonction de StoreState. Le widget affiche également les achats passés de l'utilisateur, qui seront utilisés à l'étape suivante.

Le widget _PurchaseList affiche la liste des produits pouvant être achetés et envoie une demande d'achat à l'objet 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 les produits sont correctement configurés, vous devriez pouvoir les voir dans les stores Android et iOS. Notez qu'un certain temps peut s'écouler avant que les achats ne soient disponibles lorsqu'ils sont saisis dans les consoles respectives.

ca1a9f97c21e552d.png

Revenez à dash_purchases.dart et implémentez la fonction permettant d'acheter un produit. Vous n'avez qu'à séparer les consommables des non-consommables. La mise à niveau et les produits d'abonnement ne sont pas consommables.

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',
        );
    }
  }

Avant de continuer, créez la variable _beautifiedDashUpgrade et mettez à jour le getter beautifiedDash pour qu'il la référence.

lib/logic/dash_purchases.dart

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

La méthode _onPurchaseUpdate reçoit les mises à jour des achats, met à jour l'état du produit affiché sur la page d'achat et applique l'achat à la logique du compteur. Il est important d'appeler completePurchase après avoir géré l'achat afin que le magasin sache que l'achat a été traité correctement.

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. Configurez le backend

Avant de passer au suivi et à la vérification des achats, configurez un backend Dart pour les prendre en charge.

Dans cette section, travaillez à partir du dossier dart-backend/ en tant que racine.

Assurez-vous que les outils suivants sont installés :

Présentation du projet de base

Étant donné que certaines parties de ce projet sont considérées comme hors champ pour cet atelier de programmation, elles sont incluses dans le code de démarrage. Avant de commencer, il est judicieux de parcourir le code de démarrage pour vous faire une idée de la façon dont vous allez structurer les choses.

Ce code de backend peut s'exécuter localement sur votre machine. Vous n'avez pas besoin de le déployer pour l'utiliser. Toutefois, vous devez pouvoir vous connecter depuis votre appareil de développement (Android ou iPhone) à la machine sur laquelle le serveur s'exécutera. Pour cela, ils doivent se trouver sur le même réseau et vous devez connaître l'adresse IP de votre machine.

Essayez d'exécuter le serveur à l'aide de la commande suivante :

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Le backend Dart utilise shelf et shelf_router pour diffuser les points de terminaison de l'API. Par défaut, le serveur ne fournit aucun itinéraire. Vous allez ensuite créer une route pour gérer le processus de validation des achats.

Le code de démarrage inclut déjà IapRepository dans lib/iap_repository.dart. Étant donné que l'apprentissage de l'interaction avec Firestore, ou les bases de données en général, n'est pas considéré comme pertinent pour cet atelier de programmation, le code de démarrage contient des fonctions permettant de créer ou de mettre à jour des achats dans Firestore, ainsi que toutes les classes pour ces achats.

Configurer l'accès à Firebase

Pour accéder à Firebase Firestore, vous avez besoin d'une clé d'accès au compte de service. Pour en générer une, ouvrez les paramètres du projet Firebase, accédez à la section Comptes de service, puis sélectionnez Générer une nouvelle clé privée.

27590fc77ae94ad4.png

Copiez le fichier JSON téléchargé dans le dossier assets/ et renommez-le service-account-firebase.json.

Configurer l'accès à Google Play

Pour accéder au Play Store et valider les achats, vous devez générer un compte de service avec ces autorisations et télécharger les identifiants JSON correspondants.

  1. Accédez à la page API Google Play Android Developer dans la console Google Cloud. 629f0bd8e6b50be8.png Si la Google Play Console vous demande de créer un projet ou de l'associer à un projet existant, faites-le d'abord, puis revenez sur cette page.
  2. Ensuite, accédez à la page Comptes de service et cliquez sur + Créer un compte de service. 8dc97e3b1262328a.png
  3. Saisissez le Nom du compte de service, puis cliquez sur Créer et continuer. 4fe8106af85ce75f.png
  4. Sélectionnez le rôle Abonné Pub/Sub, puis cliquez sur OK. a5b6fa6ea8ee22d.png
  5. Une fois le compte créé, accédez à Gérer les clés. eb36da2c1ad6dd06.png
  6. Sélectionnez Ajouter une clé > Créer une clé. e92db9557a28a479.png
  7. Créez et téléchargez une clé JSON. 711d04f2f4176333.png
  8. Renommez le fichier téléchargé service-account-google-play.json, et déplacez-le dans le répertoire assets/.
  9. Ensuite, accédez à la page Utilisateurs et autorisations de la Play Console.28fffbfc35b45f97.png
  10. Cliquez sur Inviter de nouveaux utilisateurs, puis saisissez l'adresse e-mail du compte de service créé précédemment. Vous trouverez l'adresse e-mail dans le tableau de la page Comptes de service.e3310cc077f397d.png
  11. Accordez les autorisations Afficher les données financières et Gérer les commandes et les abonnements pour l'application. a3b8cf2b660d1900.png
  12. Cliquez sur Inviter un utilisateur.

Il nous reste une dernière chose à faire : ouvrir lib/constants.dart, et remplacer la valeur de androidPackageId par l'ID de package que vous avez choisi pour votre application Android.

Configurer l'accès à l'App Store d'Apple

Pour accéder à l'App Store et vérifier les achats, vous devez configurer une clé secrète partagée :

  1. Ouvrez App Store Connect.
  2. Accédez à Mes applications,puis sélectionnez votre application.
  3. Dans la barre de navigation latérale, accédez à Général > Informations sur l'application.
  4. Cliquez sur Gérer sous l'en-tête Secret partagé spécifique à l'application. ad419782c5fbacb2.png
  5. Générez un secret et copiez-le. b5b72a357459b0e5.png
  6. Ouvrez lib/constants.dart, et remplacez la valeur de appStoreSharedSecret par la clé secrète partagée que vous venez de générer.

Fichier de configuration des constantes

Avant de continuer, assurez-vous que les constantes suivantes sont configurées dans le fichier lib/constants.dart :

  • androidPackageId : ID du package utilisé sur Android, tel que com.example.dashclicker
  • appStoreSharedSecret : clé secrète partagée pour accéder à App Store Connect et valider les achats.
  • bundleId : ID du bundle utilisé sur iOS, tel que com.example.dashclicker

Pour le moment, vous pouvez ignorer le reste des constantes.

10. Valider les achats

La procédure générale de validation des achats est semblable pour iOS et Android.

Pour les deux plates-formes, votre application reçoit un jeton lorsqu'un achat est effectué.

Ce jeton est envoyé par l'application à votre service de backend, qui valide ensuite l'achat auprès des serveurs de la plate-forme concernée à l'aide du jeton fourni.

Le service de backend peut alors choisir de stocker l'achat et de répondre à l'application pour indiquer si l'achat était valide ou non.

En faisant valider les achats par le service backend auprès des plates-formes de téléchargement plutôt que par l'application exécutée sur l'appareil de l'utilisateur, vous pouvez empêcher l'utilisateur d'accéder aux fonctionnalités Premium en, par exemple, rembobinant l'horloge système.

Configurer le côté Flutter

Configurez l'authentification.

Comme vous allez envoyer les achats à votre service de backend, vous devez vous assurer que l'utilisateur est authentifié lorsqu'il effectue un achat. La majeure partie de la logique d'authentification est déjà ajoutée pour vous dans le projet de démarrage. Il vous suffit de vous assurer que PurchasePage affiche le bouton de connexion lorsque l'utilisateur n'est pas encore connecté. Ajoutez le code suivant au début de la méthode de compilation 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.

    // ...

Appeler le point de terminaison de validation depuis l'application

Dans l'application, créez la fonction _verifyPurchase(PurchaseDetails purchaseDetails) qui appelle le point de terminaison /verifypurchase sur votre backend Dart à l'aide d'un appel HTTP POST.

Envoyez le magasin sélectionné (google_play pour le Play Store ou app_store pour l'App Store), le serverVerificationData et le productID. Le serveur renvoie un code d'état indiquant si l'achat est validé.

Dans les constantes de l'application, configurez l'adresse IP du serveur sur l'adresse IP de votre machine locale.

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();
  }

Ajoutez le firebaseNotifier lors de la création de DashPurchases dans main.dart:.

lib/main.dart

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

Ajoutez un getter pour l'utilisateur dans FirebaseNotifier afin de pouvoir transmettre l'ID utilisateur à la fonction de validation de l'achat.

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 {
    // ...

Ajoutez la fonction _verifyPurchase à la classe DashPurchases. Cette fonction async renvoie une valeur booléenne indiquant si l'achat est validé.

lib/logic/dash_purchases.dart

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

Appelez la fonction _verifyPurchase dans _handlePurchase juste avant d'appliquer l'achat. Vous ne devez appliquer l'achat que lorsqu'il est validé. Dans une application de production, vous pouvez spécifier davantage cette option, par exemple pour appliquer un abonnement d'essai lorsque le Play Store est temporairement indisponible. Toutefois, pour cet exemple, appliquez l'achat lorsqu'il a bien été validé.

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);
    }
  }

Dans l'application, tout est maintenant prêt pour valider les achats.

Configurer le service de backend

Ensuite, configurez le backend pour valider les achats sur le backend.

Créer des gestionnaires d'achats

Étant donné que le processus de validation des deux magasins est presque identique, configurez une classe abstraite PurchaseHandler avec des implémentations distinctes pour chaque magasin.

be50c207c5a2a519.png

Commencez par ajouter un fichier purchase_handler.dart au dossier lib/, dans lequel vous définissez une classe PurchaseHandler abstraite avec deux méthodes abstraites pour vérifier deux types d'achats différents : les abonnements et les achats ponctuels.

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,
  });
}

Comme vous pouvez le voir, chaque méthode nécessite trois paramètres :

  • userId: ID de l'utilisateur connecté, pour que vous puissiez associer les achats à l'utilisateur.
  • productData: Données sur le produit. Vous allez définir cela dans une minute.
  • token: Jeton fourni à l'utilisateur par le magasin.

De plus, pour faciliter l'utilisation de ces gestionnaires d'achats, ajoutez une méthode verifyPurchase() qui peut être utilisée pour les abonnements et les achats ponctuels :

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,
        );
    }
  }

Vous pouvez désormais appeler verifyPurchase pour les deux cas, tout en conservant des implémentations distinctes.

La classe ProductData contient des informations de base sur les différents produits achetables, y compris l'ID du produit (parfois appelé SKU) et le ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType peut être un abonnement ou un produit sans abonnement.

lib/products.dart

enum ProductType { subscription, nonSubscription }

Enfin, la liste des produits est définie comme une carte dans le même fichier.

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,
  ),
};

Définissez ensuite des implémentations d'espace réservé pour le Google Play Store et l'App Store d'Apple. Commencez par Google Play :

Créez lib/google_play_purchase_handler.dart et ajoutez une classe qui étend le PurchaseHandler que vous venez d'écrire :

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;
  }
}

Pour l'instant, il renvoie true pour les méthodes de gestionnaire. Vous y reviendrez plus tard.

Comme vous l'avez peut-être remarqué, le constructeur accepte une instance de IapRepository. Le gestionnaire d'achats utilise cette instance pour stocker ultérieurement des informations sur les achats dans Firestore. Pour communiquer avec Google Play, vous utilisez le AndroidPublisherApi fourni.

Ensuite, faites de même pour le gestionnaire de plates-formes de téléchargement d'applications. Créez lib/app_store_purchase_handler.dart et ajoutez à nouveau une classe qui étend PurchaseHandler :

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;
  }
}

Parfait ! Vous avez maintenant deux gestionnaires d'achats. Créez ensuite le point de terminaison de l'API de validation des achats.

Utiliser des gestionnaires d'achats

Ouvrez bin/server.dart et créez un point de terminaison d'API à l'aide de 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');
  }
}

Le code effectue les opérations suivantes :

  1. Définissez un point de terminaison POST qui sera appelé à partir de l'application que vous avez créée précédemment.
  2. Décodez la charge utile JSON et extrayez les informations suivantes :
    1. userId : ID de l'utilisateur connecté
    2. source : magasin utilisé (app_store ou google_play).
    3. productData : obtenu à partir du productDataMap que vous avez créé précédemment.
    4. token : contient les données de validation à envoyer aux plates-formes.
  3. Appel à la méthode verifyPurchase, soit pour GooglePlayPurchaseHandler, soit pour AppStorePurchaseHandler, selon la source.
  4. Si la validation a réussi, la méthode renvoie un Response.ok au client.
  5. Si la validation échoue, la méthode renvoie un Response.internalServerError au client.

Après avoir créé le point de terminaison de l'API, vous devez configurer les deux gestionnaires d'achats. Pour ce faire, vous devez charger les clés de compte de service que vous avez obtenues à l'étape précédente et configurer l'accès aux différents services, y compris l'API Android Publisher et l'API Firebase Firestore. Créez ensuite les deux gestionnaires d'achats avec les différentes dépendances :

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,
    ),
  };
}

Valider les achats Android : implémenter le gestionnaire d'achats

Ensuite, continuez à implémenter le gestionnaire d'achats Google Play.

Google fournit déjà des packages Dart pour interagir avec les API dont vous avez besoin pour valider les achats. Vous les avez initialisés dans le fichier server.dart et vous allez maintenant les utiliser dans la classe GooglePlayPurchaseHandler.

Implémentez le gestionnaire pour les achats de type non abonnement :

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;
  }

Vous pouvez mettre à jour le gestionnaire d'achat d'abonnement de la même manière :

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;
  }
}

Ajoutez la méthode suivante pour faciliter l'analyse des ID de commande, ainsi que deux méthodes pour analyser l'état de l'achat.

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;
}

Vos achats Google Play devraient maintenant être validés et stockés dans la base de données.

Passez ensuite aux achats sur l'App Store pour iOS.

Valider les achats iOS : implémenter le gestionnaire d'achats

Pour valider les achats avec l'App Store, il existe un package Dart tiers nommé app_store_server_sdk qui facilite le processus.

Commencez par créer l'instance ITunesApi. Utilisez la configuration du bac à sable et activez la journalisation pour faciliter le débogage des erreurs.

lib/app_store_purchase_handler.dart

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

Contrairement aux API Google Play, l'App Store utilise désormais les mêmes points de terminaison d'API pour les abonnements et les achats ponctuels. Cela signifie que vous pouvez utiliser la même logique pour les deux gestionnaires. Fusionnez-les pour qu'ils appellent la même implémentation :

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
  }

Maintenant, implémentez 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;
    }
  }

Vos achats sur l'App Store devraient maintenant être validés et stockés dans la base de données.

Exécuter le backend

À ce stade, vous pouvez exécuter dart bin/server.dart pour diffuser le point de terminaison /verifypurchase.

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

11. Suivre les achats

La méthode recommandée pour suivre les achats de vos utilisateurs consiste à utiliser le service de backend. En effet, votre backend peut répondre aux événements du magasin et est donc moins susceptible de rencontrer des informations obsolètes en raison de la mise en cache, et moins susceptible d'être falsifié.

Commencez par configurer le traitement des événements du magasin sur le backend avec le backend Dart que vous avez créé.

Traiter les événements du magasin sur le backend

Les magasins peuvent informer votre backend de tous les événements de facturation qui se produisent, par exemple lors du renouvellement d'abonnements. Vous pouvez traiter ces événements dans votre backend pour que les achats de votre base de données restent à jour. Dans cette section, configurez-le pour le Google Play Store et l'App Store d'Apple.

Traiter les événements Google Play Billing

Google Play fournit des événements de facturation via ce qu'il appelle un sujet Cloud Pub/Sub. Il s'agit essentiellement de files d'attente de messages dans lesquelles les messages peuvent être publiés et à partir desquelles ils peuvent être consommés.

Comme il s'agit d'une fonctionnalité spécifique à Google Play, vous l'incluez dans GooglePlayPurchaseHandler.

Commencez par ouvrir lib/google_play_purchase_handler.dart et ajoutez l'importation PubsubApi :

lib/google_play_purchase_handler.dart

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

Ensuite, transmettez le PubsubApi au GooglePlayPurchaseHandler et modifiez le constructeur de classe pour créer un Timer comme suit :

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 configuré pour appeler la méthode _pullMessageFromPubSub toutes les dix secondes. Vous pouvez ajuster la durée selon vos préférences.

Créez ensuite le _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,
    );
  }

Le code que vous venez d'ajouter communique avec le sujet Pub/Sub de Google Cloud toutes les dix secondes et demande de nouveaux messages. Il traite ensuite chaque message dans la méthode _processMessage.

Cette méthode décode les messages entrants et obtient les informations mises à jour sur chaque achat, qu'il s'agisse d'abonnements ou d'achats ponctuels, en appelant les méthodes handleSubscription ou handleNonSubscription existantes si nécessaire.

Chaque message doit être confirmé avec la méthode _askMessage.

Ensuite, ajoutez les dépendances requises au fichier server.dart. Ajoutez PubsubApi.cloudPlatformScope à la configuration des identifiants :

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
      ]);

Créez ensuite l'instance PubsubApi :

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Enfin, transmettez-le au constructeur GooglePlayPurchaseHandler :

bin/server.dart

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

Configuration de Google Play

Vous avez écrit le code pour consommer les événements de facturation à partir du sujet Pub/Sub, mais vous n'avez pas créé le sujet Pub/Sub et vous ne publiez aucun événement de facturation. Il est temps de configurer cette fonctionnalité.

Commencez par créer un sujet Pub/Sub :

  1. Définissez la valeur de googleCloudProjectId dans constants.dart sur l'ID de votre projet Google Cloud.
  2. Accédez à la page Cloud Pub/Sub dans la console Google Cloud.
  3. Assurez-vous d'être dans votre projet Firebase, puis cliquez sur + Créer un sujet. d5ebf6897a0a8bf5.png
  4. Donnez au nouveau thème un nom identique à la valeur définie pour googlePlayPubsubBillingTopic dans constants.dart. Dans ce cas, nommez-le play_billing. Si vous choisissez une autre option, veillez à mettre à jour constants.dart. Créez le sujet. 20d690fc543c4212.png
  5. Dans la liste de vos sujets Pub/Sub, cliquez sur les trois points verticaux du sujet que vous venez de créer, puis sur Afficher les autorisations. ea03308190609fb.png
  6. Dans la barre latérale de droite, sélectionnez Ajouter un compte principal.
  7. Ici, ajoutez google-play-developer-notifications@system.gserviceaccount.com et accordez-lui le rôle d'Éditeur Pub/Sub. 55631ec0549215bc.png
  8. Enregistrez les modifications apportées aux autorisations.
  9. Copiez le nom du sujet que vous venez de créer.
  10. Ouvrez à nouveau la Play Console, puis sélectionnez votre application dans la liste Toutes les applications.
  11. Faites défiler la page vers le bas, puis accédez à Monétiser > Configuration de la monétisation.
  12. Renseignez l'intégralité de la section "Thème", puis enregistrez vos modifications. 7e5e875dc6ce5d54.png

Tous les événements de facturation Google Play seront désormais publiés dans la rubrique.

Traiter les événements de facturation de l'App Store

Ensuite, faites de même pour les événements de facturation de l'App Store. Il existe deux façons efficaces de gérer les mises à jour des achats sur l'App Store. La première consiste à implémenter un webhook que vous fournissez à Apple et qu'il utilise pour communiquer avec votre serveur. La deuxième méthode, que vous trouverez dans cet atelier de programmation, consiste à se connecter à l'API Server App Store et à obtenir manuellement les informations sur l'abonnement.

Cet atelier de programmation se concentre sur la deuxième solution, car vous devriez exposer votre serveur à Internet pour implémenter le webhook.

Dans un environnement de production, l'idéal est de disposer des deux. Le webhook pour obtenir des événements de l'App Store et l'API Server si vous avez manqué un événement ou si vous devez vérifier l'état d'un abonnement.

Commencez par ouvrir lib/app_store_purchase_handler.dart et ajoutez la dépendance AppStoreServerAPI :

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

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

Modifiez le constructeur pour ajouter un minuteur qui appellera la méthode _pullStatus. Ce minuteur appellera la méthode _pullStatus toutes les 10 secondes. Vous pouvez ajuster la durée de ce minuteur selon vos besoins.

lib/app_store_purchase_handler.dart

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

Créez ensuite la méthode _pullStatus comme suit :

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,
            ),
          );
        }
      }
    }
  }

Cette méthode fonctionne comme suit :

  1. Obtient la liste des abonnements actifs à partir de Firestore à l'aide d'IapRepository.
  2. Pour chaque commande, il demande l'état de l'abonnement à l'API App Store Server.
  3. Obtient la dernière transaction pour cet achat d'abonnement.
  4. Vérifie la date d'expiration.
  5. Met à jour l'état de l'abonnement sur Firestore. S'il a expiré, il sera marqué comme tel.

Enfin, ajoutez tout le code nécessaire pour configurer l'accès à l'API Server App Store :

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
    ),
  };

Configuration de l'App Store

Ensuite, configurez l'App Store :

  1. Connectez-vous à App Store Connect, puis sélectionnez Utilisateurs et accès.
  2. Accédez à Integrations > Keys > In-App Purchase (Intégrations > Clés > Achat via l'application).
  3. Appuyez sur l'icône Plus pour en ajouter un.
  4. Attribuez-lui un nom, par exemple "Clé de l'atelier de programmation".
  5. Téléchargez le fichier p8 contenant la clé.
  6. Copiez-le dans le dossier des éléments, en lui donnant le nom SubscriptionKey.p8.
  7. Copiez l'ID de la clé nouvellement créée et définissez-le sur la constante appStoreKeyId dans le fichier lib/constants.dart.
  8. Copiez l'ID de l'émetteur en haut de la liste des clés et définissez-le sur la constante appStoreIssuerId dans le fichier lib/constants.dart.

9540ea9ada3da151.png

Suivre les achats sur l'appareil

Le moyen le plus sûr de suivre vos achats est côté serveur, car il est difficile de sécuriser le client. Toutefois, vous devez trouver un moyen de renvoyer les informations au client afin que l'application puisse agir en fonction de l'état de l'abonnement. En stockant les achats dans Firestore, vous pouvez synchroniser les données avec le client et les tenir à jour automatiquement.

Vous avez déjà inclus IAPRepo dans l'application, qui est le dépôt Firestore contenant toutes les données d'achat de l'utilisateur dans List<PastPurchase> purchases. Le dépôt contient également hasActiveSubscription,, qui est défini sur "true" lorsqu'il existe un achat avec productId storeKeySubscription dont l'état n'est pas "Expiré". Lorsque l'utilisateur n'est pas connecté, la liste est vide.

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();
    });
  }

Toute la logique d'achat se trouve dans la classe DashPurchases, où les abonnements doivent être appliqués ou supprimés. Ajoutez donc iapRepo en tant que propriété dans la classe et attribuez iapRepo dans le constructeur. Ensuite, ajoutez directement un écouteur dans le constructeur et supprimez-le dans la méthode dispose(). Au début, l'écouteur peut être une fonction vide. Comme IAPRepo est un ChangeNotifier et que vous appelez notifyListeners() chaque fois que les achats dans Firestore changent, la méthode purchasesUpdate() est toujours appelée lorsque les produits achetés changent.

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
  }

Ensuite, fournissez le IAPRepo au constructeur dans main.dart.. Vous pouvez obtenir le dépôt en utilisant context.read, car il est déjà créé dans un Provider.

lib/main.dart

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

Écrivez ensuite le code de la fonction purchaseUpdate(). Dans dash_counter.dart,, les méthodes applyPaidMultiplier et removePaidMultiplier définissent le multiplicateur sur 10 ou 1, respectivement. Vous n'avez donc pas besoin de vérifier si l'abonnement est déjà appliqué. Lorsque l'état de l'abonnement change, vous devez également mettre à jour l'état du produit achetable afin d'indiquer sur la page d'achat qu'il est déjà actif. Définissez la propriété _beautifiedDashUpgrade selon que la mise à niveau est achetée ou non.

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();
    }
  }

Vous avez maintenant la certitude que l'état de l'abonnement et de la mise à niveau est toujours à jour dans le service backend et synchronisé avec l'application. L'application agit en conséquence et applique les fonctionnalités d'abonnement et de mise à niveau à votre jeu Dash Clicker.

12. Terminé !

Félicitations ! Vous avez terminé l'atelier de programmation. Vous trouverez le code final de cet atelier de programmation dans le dossier android_studio_folder.png complete.

Pour en savoir plus, essayez les autres ateliers de programmation Flutter.