Ajouter des achats via une application à votre application Flutter

1. Introduction

Dernière mise à jour:11/07/2023

Pour ajouter des achats via une application à une application Flutter, vous devez configurer correctement l'App Store et le Play Store, valider l'achat et accorder les autorisations nécessaires, comme les avantages liés aux abonnements.

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

  1. Une option d'achat reproductible pour 2 000 tirets à la fois.
  2. Une mise à niveau unique pour transformer l'ancien style Dash en un Dash de style moderne.
  3. Abonnement qui double le nombre de clics générés automatiquement.

La première option d'achat permet à l'utilisateur de bénéficier directement de 2 000 tirets. Ils sont directement disponibles pour l'utilisateur et peuvent être achetés plusieurs fois. C'est ce qu'on appelle un consommable, car il est directement consommé et peut être consommé plusieurs fois.

La deuxième option permet de rendre Dash plus attrayant. Il ne peut être acheté qu'une seule fois et reste disponible indéfiniment. Un tel achat est appelé "non consommable", car il ne peut pas être consommé par l'application, mais il est valide indéfiniment.

La troisième et dernière option d'achat est l'abonnement. Tant que l'abonnement est actif, l'utilisateur bénéficie de Dashes plus rapidement, mais lorsqu'il arrête de payer l'abonnement, les avantages disparaissent également.

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 simplifier 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 afin qu'elle accepte les abonnements et les achats de consommables.
  • Vous allez également étendre une application backend Dart pour vérifier et stocker les articles achetés.

Points abordés

  • Configurer l'App Store et le Play Store avec des produits pouvant être achetés
  • Comment communiquer avec les magasins pour vérifier les achats et les stocker dans Firestore.
  • Gérer les achats dans votre application

Prérequis

  • Android Studio 4.1 ou version ultérieure
  • Xcode 12 ou version ultérieure (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 vous avez installé l'outil de CLI GitHub, 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'un ensemble d'ateliers de programmation. Le code de cet atelier de programmation est disponible en flutter-codelabs/in_app_purchases.

La structure de répertoires sous flutter-codelabs/in_app_purchases contient une série d'instantanés indiquant où vous devez vous trouver à la fin de chaque étape nommée. Le code de démarrage étant à l'étape 0, il est aussi simple de localiser les fichiers correspondants:

cd flutter-codelabs/in_app_purchases/step_00

Si vous voulez avancer ou voir à quoi devrait ressembler le résultat 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émarrage depuis step_00 dans l'IDE de votre choix. Nous avons utilisé Android Studio pour les captures d'écran, mais Visual Studio Code constitue également une excellente option. Dans l'un ou l'autre des éditeurs, 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 savoir quels produits sont disponibles et à quel prix. Chaque application est identifiée par un identifiant unique. Pour l'App Store iOS, il s'agit de l'identifiant du bundle. Pour le Play Store Android, il s'agit de l'ID application. Ces identifiants sont généralement créés à l'aide d'une notation de nom de domaine inversée. Par exemple, lorsque nous créons une application d'achat via une application pour flutter.dev, nous utilisons dev.flutter.inapppurchase. Pensez à un identifiant pour votre application. Vous allez maintenant le définir dans les paramètres du projet.

Commencez par configurer l'identifiant du bundle pour iOS.

Une fois le projet ouvert dans Android Studio, effectuez un clic droit sur le dossier iOS, cliquez sur Flutter, puis ouvrez le module dans l'application Xcode.

942772eb9a73bfaa.png

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

812f919d965c649a.jpeg

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

5c4733ac560ae8c2.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 à pubspec en ajoutant in_app_purchase aux dépendances dans votre pubspec:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

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

Cliquez sur pub get pour télécharger le package ou exécutez flutter pub get dans la ligne de commande.

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 pouvant être achetés. Vous n'avez pas besoin de publier quoi que ce soit ni d'envoyer l'application à Apple pour examen. Vous devez disposer d'un compte de développeur pour effectuer cette opération. Si vous n'en avez pas, inscrivez-vous au Apple Developer Program.

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 à la page https://appstoreconnect.apple.com/, puis cliquez sur Agreements, Tax, and Banking (Accords, taxes et services bancaires).

6e373780e5e24a6f.png

Les contrats s'afficheront ici pour les applications sans frais et payantes. L'état des applications sans frais doit être actif et celui des applications payantes est "nouveau". Veillez à consulter les conditions d'utilisation, à les accepter et à saisir toutes les informations requises.

74c73197472c9aec.png

Lorsque tout est configuré correctement, l'état des applications payantes est actif. C'est très important, car sans contrat actif, vous ne pourrez pas essayer d'effectuer des achats via l'application.

4a100bbb8cafdbbf.jpeg

Enregistrer l'ID d'application

Créez un identifiant dans le portail des développeurs Apple.

55d7e592d9a3fc7b.png

Sélectionner des ID d'applications

13f125598b72ca77.png

Sélectionner une appli

41ac4c13404e2526.png

Fournissez une description et définissez l'ID du bundle de sorte qu'il corresponde à la valeur définie précédemment dans XCode.

9d2c940ad80deeef.png

Pour en savoir plus sur la création d'un ID d'application, consultez l'aide sur 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 en savoir plus sur la création d'une application et la gestion des contrats, consultez le Centre d'aide App Store Connect.

Pour tester les achats via l'application, vous avez besoin d'un utilisateur test Sandbox. Cet utilisateur test ne doit pas être connecté à iTunes. Il n'est utilisé que pour tester les achats via une application. Vous ne pouvez pas utiliser une adresse e-mail déjà utilisée pour un compte Apple. Dans Utilisateurs et accès, accédez à Testeurs sous Bac à sable pour créer un compte de bac à sable ou pour gérer les ID Apple de bac à sable existants.

3ca2b26d4e391a4c.jpeg

Vous pouvez maintenant configurer un utilisateur Sandbox sur votre iPhone en accédant à Réglages > App Store > Compte bac à sable.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

Configurer les achats via une application

Vous allez maintenant configurer les trois éléments pouvant être achetés:

  • dash_consumable_2k: achat consommable pouvant être acheté plusieurs fois, ce qui accorde à l'utilisateur 2 000 dashes (la devise de l'application) par achat.
  • dash_upgrade_3d: une "mise à niveau" non consommable qui ne peut être acheté qu'une seule fois, et offre à l'utilisateur un clic sur un Dash de manière esthétique.
  • dash_subscription_doubler: abonnement qui accorde à l'utilisateur deux fois plus de tirets par clic pendant la durée de l'abonnement.

d156b2f5bac43ca8.png

Accédez à Achats via l'application > Gérer.

Créez vos achats via une application à l'aide des identifiants spécifiés:

  1. Configurez dash_consumable_2k en tant que consommable.

Utilisez dash_consumable_2k comme ID produit. Le nom de référence n'est utilisé que dans App Store Connect. Il vous suffit de le définir sur dash consumable 2k et d'ajouter vos versions localisées pour l'achat. Appelez l'achat Spring is in the air avec 2000 dashes fly out comme description.

ec1701834fd8527.png

  1. Configurez dash_upgrade_3d comme non consommable.

Utilisez dash_upgrade_3d comme ID produit. Définissez le nom de la référence sur dash upgrade 3d et ajoutez vos versions localisées pour l'achat. Appelez l'achat 3D Dash avec Brings your dash back to the future comme description.

6765d4b711764c30.png

  1. Configurez dash_subscription_doubler en tant qu'abonnement à renouvellement automatique.

Le parcours pour les abonnements est un peu différent. Vous devez d'abord définir le nom de la référence et l'identifiant produit:

6d29e08dae26a0c4.png

Vous devez ensuite créer un groupe d'abonnement. Lorsque plusieurs abonnements font partie d'un même groupe, un utilisateur ne peut s'abonner qu'à l'un d'entre eux en même temps, mais peut facilement passer à un abonnement supérieur ou inférieur. Il vous suffit d'appeler ce groupe subscriptions.

5bd0da17a85ac076.png

Saisissez ensuite la durée de l'abonnement et les localisations. Nommez cet abonnement Jet Engine et saisissez la description Doubles your clicks. Cliquez sur Enregistrer.

bd1b1d82eeee4cb3.png

Après avoir cliqué sur le bouton Enregistrer, ajoutez le prix de l'abonnement. Choisissez le prix de votre choix.

d0bf39680ef0aa2e.png

Les trois achats devraient maintenant s'afficher dans la liste des achats:

99d5c4b446e8fecf.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, créez-en un.

Créer une application

Créer 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. Saisissez le nom de votre application tel que vous souhaitez qu'il apparaisse sur Google Play. Vous pourrez modifier le nom ultérieurement.
  4. Spécifiez que votre application est un jeu. Vous pourrez modifier ce choix ultérieurement.
  5. Indiquez si votre application est sans frais ou payante.
  6. Ajoutez une adresse e-mail permettant aux utilisateurs du Play Store de vous contacter au sujet de cette application.
  7. Respectez les consignes relatives au contenu et les déclarations sur la législation sur l'exportation aux États-Unis.
  8. Sélectionnez Create app (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. Elle vous permet de fournir des informations sur votre application, telles que la classification du contenu et des captures d'écran. 13845badcf9bc1db.png

Signer l'application

Pour pouvoir tester les achats via une application, vous devez avoir importé au moins un build dans Google Play.

Pour ce faire, votre build doit être signé 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 dans 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, exécutez 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. Conservez le

keystore

file private; ne l'enregistrez pas dans le dépôt public source.

Référencer le keystore à partir de 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 connexion dans Gradle

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

Ajoutez les informations du keystore de votre fichier de propriétés avant le bloc android:

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

   android {
         // omitted
   }

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

Ajoutez le code suivant avant le bloc buildTypes:

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

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

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

Les versions 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 créer en exécutant la commande suivante:

flutter build appbundle

Cette commande génère un build 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 à Publier > Test > Tests fermés, puis créer une version de test fermé.

Pour cet atelier de programmation, vous utiliserez la signature d'application Google. Pour ce faire, appuyez sur Continue (Continuer) sous Play App Signing (Signature d'application Play) pour l'activer.

ba98446d9c5c40e0.png

Importez ensuite l'app bundle app-release.aab généré par la commande de compilation.

Cliquez sur Enregistrer, puis sur Examiner la version.

Enfin, cliquez sur Lancer le déploiement pour les tests internes pour activer la version de test interne.

Configurer des utilisateurs de test

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

  1. Vers 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 à la page Publier > Test > 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. Attribuez un nom à la liste, puis ajoutez les adresses e-mail des comptes Google qui doivent pouvoir tester les achats via l'application.

Cochez ensuite la case correspondant à 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 les achats via une application

Vous allez maintenant configurer les éléments pouvant être achetés dans l'application.

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

  • dash_consumable_2k: achat consommable pouvant être acheté plusieurs fois, ce qui accorde à l'utilisateur 2 000 dashes (la devise de l'application) par achat.
  • dash_upgrade_3d: une "mise à niveau" non consommable qui ne peut être acheté qu'une seule fois, ce qui permet à l'internaute de cliquer sur un élément Dash de manière esthétique.
  • dash_subscription_doubler: abonnement qui accorde à l'utilisateur deux fois plus de tirets par clic pendant la durée de l'abonnement.

Tout d'abord, ajoutez les consommables et les non consommables.

  1. Accédez à la Google Play Console, puis sélectionnez votre application.
  2. Accédez à Monétiser > Produits > Produits intégrés à l'application :
  3. Cliquez sur Créer un produit.c8d66e32f57dee21.png
  4. Saisissez toutes les informations requises pour votre produit. Assurez-vous que l'ID produit correspond exactement à celui que vous souhaitez utiliser.
  5. Cliquez sur Enregistrer.
  6. Cliquez sur Activer.
  7. Répéter le processus pour la "mise à niveau" des produits non consommables à l'achat.

Ajoutez ensuite 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 abonnement.32a6a9eefdb71dd0.png
  4. Saisissez toutes les informations requises pour votre abonnement. Assurez-vous que l'ID produit correspond exactement à celui que vous souhaitez utiliser.
  5. Cliquez sur Enregistrer.

Vous devriez maintenant configurer vos achats dans la Play Console.

6. Configurer Firebase

Dans cet atelier de programmation, vous utiliserez un service de backend pour vérifier et suivre l'état achats.

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 depuis les 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 en proposant des fonctionnalités premium en rembobinant leur horloge système.

Il existe de nombreuses façons de configurer un service de backend, mais vous allez le faire à l'aide de Cloud Functions et de Firestore, en utilisant la solution Firebase de Google.

La rédaction du backend est considérée comme hors du champ d'application de cet atelier de programmation. Par conséquent, le code de démarrage inclut 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 vous reste à 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, puis créez un projet Firebase. Pour cet exemple, appelez le projet DashClicker.

Dans l'application backend, vous associez les achats à un utilisateur spécifique. Vous avez donc besoin d'une authentification. Pour cela, 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, puis activez le fournisseur de connexion Google.

7babb48832fbef29.png

Comme vous utiliserez également la base de données Firestore de Firebase, activez-la également.

e20553e0de5ac331.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 fournies sur la page de configuration.

Lors de l'exécution de la configuration de Flutterfire, 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>  

Activez ensuite 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 "yes" (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 à Project Overview (Vue d'ensemble du projet), choisissez Settings (Paramètres), puis sélectionnez l'onglet General (Général).

Faites défiler l'écran vers le bas 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 de hachage SHA-1 de votre certificat de débogage.

Obtenir le hachage du certificat de signature de débogage

À la racine de votre projet d'application Flutter, remplacez le répertoire par le 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 keystore se trouve dans votre dossier d'accueil 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 remplissez le dernier champ de la boîte de dialogue modale d'envoi de l'application.

Configurer Firebase pour iOS: étapes supplémentaires

Ouvrez ios/Runnder.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 et sur l'option open iOS module in Xcode.

Pour autoriser Google Sign-In sur iOS, ajoutez l'option de configuration CFBundleURLTypes à vos fichiers de compilation plist. Pour en savoir plus, consultez la documentation sur le package google_sign_in. Dans le cas présent, les fichiers sont ios/Runner/Info-Debug.plist et ios/Runner/Info-Release.plist.

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

  1. Récupérez 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 de vos fichiers ios/Runner/Info-Debug.plist et ios/Runner/Info-Release.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>

La configuration de Firebase est maintenant terminée.

7. Écouter les notifications d'achat

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

Écouter les dernières informations concernant vos achats

Dans main.dart,, recherchez le widget MyHomePage qui a 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 les incrémente automatiquement. DashUpgrades gère les mises à niveau que vous pouvez acheter avec Dashes. Cet atelier de programmation porte sur DashPurchases.

Par défaut, l'objet d'un fournisseur est défini lors de sa première requête. Cet objet écoute directement les mises à jour d'achat lorsque l'application démarre. 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,
),

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

Vous devez légèrement modifier le test si vous souhaitez qu'il continue de fonctionner. Consultez widget_test.dart sur GitHub pour obtenir le code complet de TestIAPConnection.

test/widget_test.dart

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

Dans lib/logic/dash_purchases.dart, accédez au code pour DashPurchases ChangeNotifier. Actuellement, vous ne pouvez ajouter qu'un DashCounter aux Dashe que vous avez achetés.

Ajoutez une propriété d'abonnement à un flux, _subscription (de type StreamSubscription<List<PurchaseDetails>> _subscription;), le IAPConnection.instance, et les importations. Le code obtenu doit ressembler à ce qui suit:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

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

  DashPurchases(this.counter);
}

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

Dans le constructeur, récupérez le purchaseUpdatedStream et commencez à écouter le flux. Dans la méthode dispose(), annulez l'abonnement à la diffusion.

lib/logic/dash_purchases.dart

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

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

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

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

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

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

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

L'application reçoit maintenant les mises à jour des achats. Dans la section suivante, vous effectuerez donc 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 pouvant être achetés. Ces produits sont chargés depuis les magasins, affichés dans une liste, et achetés lorsqu'ils appuient dessus.

Adapt PurchasableProduct

PurchasableProduct affiche un produit fictif. Mettez-le à jour pour afficher le contenu réel en remplaçant 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 factices 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 dans la boutique. Commencez par vérifier si la boutique est disponible. Lorsque le magasin n'est pas disponible, si vous définissez storeState sur notAvailable, un message d'erreur s'affiche.

lib/logic/dash_purchases.dart

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

Lorsque la boutique est disponible, chargez les achats disponibles. Compte tenu de la configuration Firebase précédente, 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 pouvant être achetés. Utilisez l'productDetails de la réponse pour mettre à jour l'interface utilisateur, puis 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);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    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();
  }

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

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Présentez les produits pouvant être achetés.

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

Le widget _PurchaseList affiche la liste des produits pouvant être achetés et envoie une requête 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(),
    );
  }
}

Vous devriez pouvoir voir les produits disponibles sur les plates-formes de téléchargement Android et iOS s'ils sont correctement configurés. Sachez qu'il peut s'écouler un certain temps avant que les achats ne soient disponibles dans les consoles respectives.

ca1a9f97c21e552d.png

Revenez à dash_purchases.dart et implémentez la fonction permettant d'acheter un produit. Il vous suffit de séparer les consommables des non consommables. La mise à niveau et les produits sur abonnement ne sont pas des 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);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      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 la référencer.

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 de compteur. Il est important d'appeler completePurchase après avoir traité l'achat afin que le magasin sache qu'il est géré 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();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

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

9. Configurez le backend

Avant de passer au suivi et à la validation des achats, configurez un backend Dart compatible.

Dans cette section, vous allez travailler à 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 du champ d'application de cet atelier de programmation, elles sont incluses dans le code de démarrage. Nous vous recommandons de passer en revue le contenu du code de démarrage avant de commencer, afin d'avoir une idée de la façon dont vous allez structurer les éléments.

Ce code backend peut s'exécuter localement sur votre machine. Vous n'avez pas besoin de le déployer pour l'utiliser. Cependant, votre appareil de développement (Android ou iPhone) doit pouvoir être connecté à l'ordinateur sur lequel 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 aucune route. Par la suite, vous créerez un itinéraire pour gérer le processus de validation des achats.

Une partie déjà incluse dans le code de démarrage est le IapRepository dans lib/iap_repository.dart. Apprendre à interagir avec Firestore, ou avec les bases de données en général, n'est pas considéré comme pertinent pour cet atelier de programmation. C'est pourquoi le code de démarrage contient des fonctions vous permettant de créer ou de mettre à jour des achats dans Firestore, ainsi que toutes les classes correspondantes.

Configurer l'accès à Firebase

Pour accéder à Firebase Firestore, vous avez besoin d'une clé d'accès de compte de service. Générez-en un en ouvrant les paramètres du projet Firebase et 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 afin de valider des achats, vous devez générer un compte de service disposant de ces autorisations et télécharger les identifiants JSON correspondants.

  1. Accédez à la Google Play Console, puis à la page Toutes les applications.
  2. Accédez à Configuration > Accès à l'API. 317fdfb54921f50e.png Si la Google Play Console vous demande de créer un projet ou d'associer un projet existant, faites-le d'abord, puis revenez sur cette page.
  3. Dans la section permettant de définir des comptes de service, cliquez sur Créer un compte de service.1e70d3f8d794bebb.png
  4. Cliquez sur le lien Google Cloud Platform dans la boîte de dialogue qui s'affiche. 7c9536336dd9e9b4.png
  5. Sélectionnez votre projet. Si vous ne la voyez pas, vérifiez que vous êtes connecté au compte Google approprié dans la liste déroulante Compte en haut à droite. 3fb3a25bad803063.png
  6. Après avoir sélectionné votre projet, cliquez sur + Créer un compte de service dans la barre de menu supérieure. 62fe4c3f8644acd8.png
  7. Attribuez un nom au compte de service et éventuellement une description pour vous en souvenir, puis passez à l'étape suivante. 8a92d5d6a3dff48c.png
  8. Attribuez le rôle Éditeur au compte de service. 6052b7753667ed1a.png
  9. Suivez les instructions de l'assistant, revenez à la page Accès à l'API dans la Play Console, puis cliquez sur Actualiser les comptes de service. Le compte que vous venez de créer doit s'afficher dans la liste. 5895a7db8b4c7659.png
  10. Cliquez sur Accorder l'accès pour votre nouveau compte de service.
  11. Faites défiler la page suivante jusqu'au bloc Données financières. Sélectionnez Afficher les données financières, les commandes et les réponses à l'enquête sur les annulations et Gérer les commandes et les abonnements. 75b22d0201cf67e.png
  12. Cliquez sur Inviter un utilisateur. 70ea0b1288c62a59.png
  13. Maintenant que le compte est configuré, il ne vous reste plus qu'à générer quelques identifiants. De retour dans la console Cloud, recherchez votre compte de service dans la liste des comptes de service, cliquez sur les trois points verticaux, puis sélectionnez Gérer les clés. 853ee186b0e9954e.png
  14. Créez une clé JSON et téléchargez-la. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Renommez le fichier téléchargé service-account-google-play.json, et déplacez-le dans le répertoire assets/.

Il nous reste à 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 valider des 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 à Achats via l'application > Gérer.
  4. En haut à droite de la liste, cliquez sur Code secret partagé spécifique à l'application.
  5. Générez un nouveau secret, puis copiez-le.
  6. Ouvrez lib/constants.dart, et remplacez la valeur de appStoreSharedSecret par le secret partagé que vous venez de générer.

d8b8042470aaeff.png

b72f4565750e2f40.png

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 de package utilisé sur Android. Ex. : com.example.dashclicker
  • appStoreSharedSecret: code secret partagé permettant d'accéder à App Store Connect pour valider des achats.
  • bundleId: ID de bundle utilisé sur iOS. Ex. : com.example.dashclicker

Pour le moment, vous pouvez ignorer les autres constantes.

10. Valider les achats

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

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

Ce jeton est envoyé par l'application à votre service de backend, qui, à son tour, valide l'achat auprès des serveurs du magasin concerné à l'aide du jeton fourni.

Le service de backend peut alors choisir de stocker l'achat et de répondre à l'application, que l'achat soit valide ou non.

Si vous demandez au service de backend d'effectuer la validation avec les magasins plutôt qu'avec l'application en cours d'exécution sur l'appareil de l'utilisateur, vous pouvez empêcher l'utilisateur d'accéder aux fonctionnalités premium en rembobinant son horloge système, par exemple.

Configurer Flutter

Configurez l'authentification.

Étant donné que vous allez envoyer les achats à votre service de backend, vous devez vous assurer que l'utilisateur est authentifié lors des achats. La majeure partie de la logique d'authentification a déjà été ajoutée 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 '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

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

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

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

Point de terminaison de la validation des appels 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 la plate-forme de téléchargement sélectionnée (google_play pour le Play Store ou app_store pour l'App Store), les serverVerificationData et les 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 locale de votre machine.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

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

Ajoutez le firebaseNotifier en créant 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 des achats.

lib/logic/firebase_notifier.dart

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

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) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

Appelez la fonction _verifyPurchase dans _handlePurchase juste avant d'appliquer l'achat. Vous ne devez appliquer l'achat qu'une fois qu'il a été validé. Dans une application de production, vous pouvez préciser cette valeur afin, par exemple, d'appliquer un abonnement d'essai lorsque la boutique est temporairement indisponible. Toutefois, dans cet exemple, faites simple et n'appliquez l'achat qu'une fois qu'il a é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();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

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

L'application est maintenant prête à valider les achats.

Configurer le service de backend

Configurez ensuite la fonction Cloud pour valider les achats sur le backend.

Créer des gestionnaires d'achat

Le flux de validation des deux magasins étant presque identique, configurez une classe PurchaseHandler abstraite 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 abstraite PurchaseHandler avec deux méthodes abstraites pour vérifier deux types d'achats différents: les abonnements et les non-abonnements.

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é, qui vous permet de lui associer les achats.
  • productData: Données concernant le produit. Vous allez la définir dans un instant.
  • token: : jeton fourni à l'utilisateur par le magasin.

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

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

Désormais, vous pouvez simplement appeler verifyPurchase dans les deux cas, tout en ayant des implémentations distinctes.

La classe ProductData contient des informations de base sur les différents produits pouvant être achetés, y compris l'identifiant produit (parfois appelé SKU) et le ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

Le ProductType peut être un abonnement ou un non-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,
  ),
};

Ensuite, définissez quelques implémentations d'espace réservé pour le Google Play Store et l'App Store d'Apple. Commencez avec Google Play:

Créez lib/google_play_purchase_handler.dart et ajoutez une classe qui étend la 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, elle renvoie true pour les méthodes du gestionnaire. vous y aurez accès plus tard.

Comme vous l'avez peut-être remarqué, le constructeur utilise 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 l'AndroidPublisherApi fourni.

Ensuite, faites de même pour le gestionnaire de la plate-forme de téléchargement d'applications. Créez lib/app_store_purchase_handler.dart et ajoutez une classe qui étend à nouveau 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,
  }) {
    return true;
  }

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

Parfait ! Vous avez maintenant deux gestionnaires d'achat. À présent, créons 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

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router);
}

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

Le code ci-dessus 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:
  3. userId: ID utilisateur actuellement connecté
  4. source: magasin utilisé (app_store ou google_play).
  5. productData: issu du productDataMap que vous avez créé précédemment.
  6. token: contient les données de validation à envoyer aux magasins.
  7. Appelez la méthode verifyPurchase, pour GooglePlayPurchaseHandler ou AppStorePurchaseHandler, en fonction de la source.
  8. Si la validation réussit, la méthode renvoie un Response.ok au client.
  9. 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'achat. Pour ce faire, vous devez charger les clés de compte de service 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. Ensuite, créez les deux gestionnaires d'achat avec les différentes dépendances:

bin/server.dart

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

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

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

Valider les achats Android: implémenter le processus d'achat

Ensuite, poursuivez l'implémentation du gestionnaire d'achats Google Play.

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

Implémentez le gestionnaire pour les achats sans abonnement:

lib/google_play_purchase_handler.dart

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

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

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

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

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

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

Vous pouvez mettre à jour le gestionnaire d'achat d'abonnements 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 do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

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

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

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

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

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

Passons maintenant aux achats sur l'App Store pour iOS.

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

Pour vérifier les achats effectués sur l'App Store, il existe un package Dart tiers, nommé app_store_server_sdk, qui facilite la procédure.

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

Désormais, contrairement aux API Google Play, l'App Store utilise les mêmes points de terminaison d'API pour les abonnements et les non-abonnements. 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 {
   //..
  }

Implémentez maintenant 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 doivent maintenant être vérifié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. Effectuez le suivi de vos achats

Nous vous recommandons de suivre les performances et les achats via le service de backend. En effet, votre backend peut répondre aux événements du magasin. Il est donc moins susceptible de rencontrer des informations obsolètes en raison de la mise en cache, et d'être moins susceptible d'être falsifié.

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

Traiter les événements du magasin sur le backend

Les magasins ont la possibilité d'informer votre backend de tout événement de facturation qui se produit, comme le renouvellement des abonnements. Vous pouvez traiter ces événements dans votre backend afin d'actualiser les achats dans votre base de données. Dans cette section, configurez cette option pour le Google Play Store et l'App Store d'Apple.

Traiter les événements de facturation Google Play

Google Play fournit les événements de facturation par le biais d'un sujet Cloud Pub/Sub. Il s'agit essentiellement de files d'attente de messages dans lesquelles les messages peuvent être publiés et consommés.

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

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

lib/google_play_purchase_handler.dart

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

Ensuite, transmettez PubsubApi à 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 _pullMessageFromSubSub toutes les 10 secondes. Vous pouvez ajuster la durée selon vos préférences.

Ensuite, créez le _pullMessageFromSubSub.

lib/google_play_purchase_handler.dart

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

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

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

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. Ensuite, chaque message est traité 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 et de non-abonnements, en appelant la méthode handleSubscription ou handleNonSubscription existante si nécessaire.

Chaque message doit être confirmé à l'aide de la méthode _askMessage.

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

bin/server.dart

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

Ensuite, créez 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, // new
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

Configuration de Google Play

Vous avez écrit le code permettant d'utiliser les événements de facturation 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 mettre cela en place.

Commencez par créer un sujet Pub/Sub:

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

Tous les événements Google Play Billing seront désormais publiés dans ce thème.

Traiter les événements de facturation App Store

Procédez de la même façon pour les événements de facturation App Store. Il existe deux méthodes efficaces pour implémenter la gestion des mises à jour dans les achats sur l'App Store. Premièrement, implémenter un webhook que vous fournissez à Apple, qui l'utilise pour communiquer avec votre serveur. La deuxième méthode, qui sera présentée dans cet atelier de programmation, consiste à se connecter à l'API du serveur App Store et à obtenir manuellement les informations sur l'abonnement.

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

Dans un environnement de production, il est préférable d'avoir les deux. Le webhook pour obtenir les événements de l'App Store et l'API Server si vous avez manqué un événement ou devez vérifier l'état d'un abonnement.

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

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

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

Modifiez le constructeur pour ajouter un minuteur qui appelle 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

  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 de l'IapRepository.
  2. Pour chaque commande, l'état de l'abonnement est demandé à l'API du serveur App Store.
  3. Obtient la dernière transaction pour cet 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 du serveur App Store:

bin/server.dart

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

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

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

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


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

Configuration de l'App Store

Configurez ensuite l'App Store:

  1. Connectez-vous à App Store Connect, puis sélectionnez Users and Access (Utilisateurs et accès).
  2. Accédez à Type de clé > Achat via l'application :
  3. Appuyez sur le "plus" pour en ajouter une.
  4. Donnez-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 composants, sous le nom SubscriptionKey.p8.
  7. Copiez l'ID de la nouvelle clé et définissez-le sur la constante appStoreKeyId dans le fichier lib/constants.dart.
  8. Copiez l'ID d'émetteur en haut de la liste de 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écurisé de suivre vos achats est côté serveur, car le client est difficile à sécuriser. Toutefois, vous devez disposer d'un moyen de lui renvoyer les informations afin que l'application puisse agir en fonction des informations concernant l'état de l'abonnement. En stockant les achats dans Firestore, vous pouvez facilement synchroniser les données avec le client et les mettre à jour automatiquement.

Vous avez déjà inclus IAPRepo dans l'application. Il s'agit du 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 vrai en cas d'achat avec productId storeKeySubscription dont l'état n'a 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((DocumentSnapshot document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

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

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

      notifyListeners();
    });
  }

Toute la logique d'achat se trouve dans la classe DashPurchases, où les abonnements doivent être appliqués ou supprimés. Vous devez donc ajouter iapRepo en tant que propriété dans la classe et attribuer iapRepo au constructeur. Ensuite, ajoutez directement un écouteur dans le constructeur et supprimez-le dans la méthode dispose(). Au départ, l'écouteur peut simplement être une fonction vide. Étant donné que 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

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

Fournissez ensuite le IAPRepo au constructeur dans main.dart.. Vous pouvez obtenir le dépôt à l'aide de 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>(),
          ),
          lazy: false,
        ),

Ensuite, écrivez le code de la fonction purchaseUpdate(). Dans dash_counter.dart,, les méthodes applyPaidMultiplier et removePaidMultiplier définissent respectivement le multiplicateur sur 10 ou sur 1. Vous n'avez donc pas besoin de vérifier si l'abonnement est déjà appliqué. Lorsque l'état de l'abonnement change, vous mettez également à jour l'état du produit pouvant être acheté 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 vous êtes désormais assuré que l'état de l'abonnement et de la mise à niveau est toujours à jour dans le service de backend et synchronisé avec l'application. L'application se comporte en conséquence et applique les fonctionnalités d'abonnement et de mise à niveau à votre jeu Dasher 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.

Pour aller plus loin, suivez les autres ateliers de programmation Flutter.