1. Introduction
Pour ajouter des achats via une application à une application Flutter, vous devez configurer correctement les plates-formes de téléchargement d'applications et de jeux, vérifier l'achat et accorder les autorisations nécessaires, telles que les avantages d'abonnement.
Dans cet atelier de programmation, vous allez ajouter trois types d'achats via une application à une application (fournie) et valider ces achats à l'aide d'un backend Dart avec Firebase. L'application fournie, Dash Clicker, contient un jeu qui utilise la mascotte Dash comme monnaie. Vous allez ajouter les options d'achat suivantes:
- Option d'achat répétable pour 2 000 Dashes à la fois.
- Achat unique pour passer de l'ancien Dash au nouveau Dash.
- Abonnement qui double le nombre de clics générés automatiquement.
La première option d'achat offre à l'utilisateur un avantage direct de 2 000 Dashes. 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 consommé directement et peut être consommé plusieurs fois.
La deuxième option permet de passer à une version plus attrayante de Dash. Vous n'avez à l'acheter qu'une seule fois. Il est disponible pour toujours. 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 un abonnement. Tant que l'abonnement est actif, l'utilisateur reçoit des Dashes plus rapidement, mais lorsqu'il cesse de payer l'abonnement, les avantages disparaissent également.
Le service 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 permet de simplifier le processus, mais dans votre application de production, vous pouvez utiliser n'importe quel type de service de backend.
Ce que vous allez faire
- Vous allez étendre une application pour qu'elle prenne en charge 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
- 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 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 l'outil CLI de GitHub est installé, vous pouvez également utiliser la commande suivante:
gh repo clone flutter/codelabs flutter-codelabs
L'exemple de code est cloné dans un répertoire flutter-codelabs
contenant 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 trouver les fichiers correspondants, procédez comme suit:
cd flutter-codelabs/in_app_purchases/step_00
Si vous souhaitez passer à l'étape suivante ou voir à quoi doit ressembler un élément après une étape, recherchez-le dans 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
dans votre IDE préféré. Nous avons utilisé Android Studio pour les captures d'écran, mais Visual Studio Code est également une excellente option. Avec l'un ou l'autre de ces é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 ID unique. Pour l'App Store iOS, il s'agit de l'identifiant du groupe, et pour le Play Store Android, de l'ID de l'application. Ces identifiants sont généralement créés à l'aide d'une notation de nom de domaine inversé. Par exemple, lorsque vous créez une application d'achat via une application pour flutter.dev, vous devez utiliser dev.flutter.inapppurchase
. Trouvez un identifiant pour votre application. Vous allez maintenant le définir dans les paramètres du projet.
Commencez par configurer l'ID de groupe 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.
Dans la structure de dossiers 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 Signature et fonctionnalités. Saisissez l'identifiant de lot que vous venez de choisir dans le champ Équipe pour définir votre équipe.
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 votre applicationId
(à la ligne 37 de la capture d'écran ci-dessous) par l'ID de l'application, qui est identique à l'identifiant du bundle iOS. Notez que les ID des plates-formes iOS et Android ne doivent pas nécessairement être identiques. Toutefois, les conserver identiques est moins sujet à erreur. Par conséquent, dans cet atelier de programmation, nous utiliserons également des identifiants identiques.
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 pubspec en ajoutant in_app_purchase
aux dépendances de votre pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Ouvrez votre fichier pubspec.yaml
et vérifiez que in_app_purchase
est désormais listé en tant qu'entrée sous dependencies
et in_app_purchase_platform_interface
sous dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
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 et tester les achats via une application sur iOS, vous devez créer une application dans l'App Store et y créer des produits achetables. Vous n'avez pas besoin de publier quoi que ce soit ni d'envoyer l'application à Apple pour examen. Pour ce faire, vous devez disposer d'un compte de développeur. Si vous n'en avez pas, inscrivez-vous au programme de développeur Apple.
Contrats pour les applications payantes
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, taxes et services bancaires.
Vous y trouverez les accords pour les applications sans frais et payantes. L'état des applications sans frais doit être "Active", et celui des applications payantes "Nouvelle". Veillez à consulter les conditions, à les accepter et à saisir toutes les informations requises.
Une fois tout configuré correctement, l'état des applications payantes sera défini sur "Actif". Cette étape est très importante, car vous ne pourrez pas tester les achats via l'application sans contrat actif.
Enregistrer l'ID de l'application
Créez un identifiant dans le portail des développeurs Apple.
Choisir des ID d'application
Sélectionner une appli
Fournissez une description et définissez l'ID du bundle sur la même valeur que celle définie précédemment dans XCode.
Pour savoir comment créer 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.
Pour en savoir plus sur la création d'une application et la gestion des contrats, consultez l'aide App Store Connect.
Pour tester les achats via l'application, vous avez besoin d'un utilisateur test dans un bac à sable. Cet utilisateur de test ne doit pas être connecté à iTunes. Il n'est utilisé que pour tester les achats intégrés. 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 Environnement de test pour créer un compte d'environnement de test ou gérer les ID Apple d'environnement de test existants.
Vous pouvez maintenant configurer votre utilisateur de bac à sable sur votre iPhone en accédant à Settings > App Store > Sandbox-account (Paramètres > App Store > Compte bac à sable).
Configurer vos achats via une application
Vous allez maintenant configurer les trois articles disponibles à l'achat:
dash_consumable_2k
: achat consommable pouvant être effectué plusieurs fois, qui accorde à l'utilisateur 2 000 Dashes (monnaie de l'application) par achat.dash_upgrade_3d
: achat de type "mise à niveau" non consommable qui ne peut être effectué qu'une seule fois et qui offre à l'utilisateur un bouton Dash différent.dash_subscription_doubler
: abonnement qui accorde à l'utilisateur deux fois plus de Dashes par clic pendant la durée de l'abonnement.
Accédez à Achats intégrés > Gérer.
Créez vos achats via l'application avec les ID spécifiés:
- 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 sur dash consumable 2k
et ajoutez vos localisations pour l'achat. Appelez l'achat Spring is in the air
avec 2000 dashes fly out
comme description.
- Configurez
dash_upgrade_3d
en tant qu'élément non consommable.
Utilisez dash_upgrade_3d
comme ID de produit. Définissez le nom de référence sur dash upgrade 3d
et ajoutez vos localisations pour l'achat. Appelez l'achat 3D Dash
avec Brings your dash back to the future
comme description.
- Configurez
dash_subscription_doubler
en tant qu'abonnement à renouvellement automatique.
Le processus pour les abonnements est légèrement différent. Vous devez d'abord définir le nom de référence et l'ID produit:
Vous devez ensuite créer un groupe d'abonnements. Lorsqu'un même groupe comprend plusieurs abonnements, un utilisateur ne peut en souscrire qu'un seul à la fois, mais il peut facilement passer à un forfait supérieur ou inférieur. Appelez simplement ce groupe subscriptions
.
Saisissez ensuite la durée de l'abonnement et les localisations. Nommez cet abonnement Jet Engine
et attribuez-lui la description Doubles your clicks
. Cliquez sur Enregistrer.
Après avoir cliqué sur le bouton Enregistrer, ajoutez un prix d'abonnement. Choisissez le prix de votre choix.
Les trois achats doivent maintenant apparaître dans la liste:
5. Configurer le Play Store
Comme pour l'App Store, vous avez é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:
- Ouvrez la Play Console.
- Sélectionnez Toutes les applications > Créer une application.
- 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.
- Indiquez que votre application est un jeu. Vous pourrez modifier ce choix ultérieurement.
- Indiquez si votre application est sans frais ou payante.
- Ajoutez une adresse e-mail à laquelle les utilisateurs du Play Store peuvent vous contacter à propos de cette application.
- Remplissez les déclarations "Consignes relatives au contenu" et "Lois des États-Unis en matière d'exportation".
- Sélectionnez Créer l'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 devez y fournir des informations sur votre application, telles que la classification du contenu et des captures d'écran.
Signer l'application
Pour pouvoir tester les achats via une application, vous devez importer au moins une version sur 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, exécutez 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
keystore
fichier privé ; ne l'enregistrez pas dans un contrôle des sources public !
Référencer le 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
.
Ajoutez les informations du keystore à partir 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 la 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 builds de votre application seront désormais signés 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
Par défaut, cette commande génère un build de version. 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 > Tests > Tests fermés, puis créez une version de test fermé.
Pour cet atelier de programmation, vous allez utiliser la signature de Google pour l'application. Pour l'activer, appuyez sur Continuer sous Signature d'application Play.
Importez ensuite 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 Start rollout to Internal testing (Lancer le déploiement du test interne) pour activer la version de test interne.
Configurer des utilisateurs de test
Pour pouvoir tester les achats via une application, vous devez ajouter les comptes Google de vos testeurs à deux endroits dans la Google Play Console:
- Au canal de test spécifique (tests internes)
- En tant que testeur de licence
Commencez par ajouter le testeur au canal de test interne. Revenez à Publier > Tests > Tests internes, puis cliquez sur l'onglet Testeurs.
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 avoir accès aux tests des achats intégrés.
Cochez ensuite la case correspondant à la liste, puis cliquez sur Enregistrer les modifications.
Ajoutez ensuite les testeurs de licence:
- Revenez à la vue Toutes les applications de la Google Play Console.
- Accédez à Paramètres > Test de licence.
- Ajoutez les mêmes adresses e-mail que celles des testeurs qui doivent pouvoir tester les achats via l'application.
- Définissez Réponse de licence sur
RESPOND_NORMALLY
. - Cliquez sur Enregistrer les modifications.
Configurer vos achats via une application
Vous allez maintenant configurer les éléments disponibles à l'achat dans l'application.
Comme sur l'App Store, vous devez définir trois achats différents:
dash_consumable_2k
: achat consommable pouvant être effectué plusieurs fois, qui accorde à l'utilisateur 2 000 Dashes (monnaie de l'application) par achat.dash_upgrade_3d
: achat de type "mise à niveau" non consommable qui ne peut être effectué qu'une seule fois, ce qui donne à l'utilisateur un bouton Dash différent.dash_subscription_doubler
: abonnement qui accorde à l'utilisateur deux fois plus de Dashes par clic pendant la durée de l'abonnement.
Commencez par ajouter les consommables et les non consommables.
- Accédez à la Google Play Console, puis sélectionnez votre application.
- Accédez à Monétiser > Produits > Produits intégrés.
- Cliquez sur Créer un produit.
- Saisissez toutes les informations requises pour votre produit. Assurez-vous que l'ID du produit correspond exactement à celui que vous comptez utiliser.
- Cliquez sur Enregistrer.
- Cliquez sur Activer.
- Répétez la procédure pour l'achat de la mise à niveau non consommable.
Ajoutez ensuite l'abonnement:
- Accédez à la Google Play Console, puis sélectionnez votre application.
- Accédez à Monétiser > Produits > Abonnements.
- Cliquez sur Créer un abonnement.
- Saisissez toutes les informations requises pour votre abonnement. Assurez-vous que l'ID du produit correspond exactement à celui que vous prévoyez d'utiliser.
- 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 à partir 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 duper votre application en lui faisant croire qu'elle propose des fonctionnalités premium en faisant reculer l'horloge système.
Il existe de nombreuses façons de configurer un service backend. Vous allez le faire à l'aide de Cloud Functions et de Firestore, en utilisant Firebase, le service de Google.
L'écriture du backend n'entre pas dans le champ d'application de 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 à vous lancer.
Les plug-ins Firebase sont également inclus avec 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, puis 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.
- Dans le tableau de bord Firebase, accédez à Authentification et activez-la si nécessaire.
- Accédez à l'onglet Mode de connexion, puis activez le fournisseur de connexion Google.
Étant donné que vous utiliserez également la base de données Firestore de Firebase, activez-la également.
Définissez des 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 décrites sur 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>
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 "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, sélectionnez Paramètres, puis l'onglet Général.
Faites défiler la page jusqu'à Vos applications, puis sélectionnez l'application dashclicker (android).
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 de votre certificat de signature de débogage
Dans le répertoire 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 keystore se trouve dans votre dossier de base, 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, puis 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
, puis sur l'option open iOS module in Xcode
.
Pour autoriser la connexion Google sur iOS, ajoutez l'option de configuration CFBundleURLTypes
à vos fichiers plist
de compilation. (Pour en savoir plus, consultez la documentation du package google_sign_in
.) Dans ce cas, 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:
- Obtenez la valeur de
REVERSED_CLIENT_ID
à partir du fichierGoogleService-Info.plist
, sans l'élément<string>..</string>
qui l'entoure. - Remplacez la valeur dans les fichiers
ios/Runner/Info-Debug.plist
etios/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>
Vous avez terminé la configuration de Firebase.
7. Écouter les informations sur les achats
Dans cette partie de l'atelier de programmation, vous allez préparer l'application à l'achat des produits. Ce processus inclut l'écoute des mises à jour et des erreurs d'achat après le démarrage de l'application.
Écouter les actualités sur les achats
Dans main.dart,
, recherchez le widget MyHomePage
qui contient 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 licences que vous pouvez acheter avec des barres obliques. Cet atelier de programmation porte sur DashPurchases
.
Par défaut, l'objet d'un fournisseur est défini lors de la première requête de cet objet. Cet objet écoute les mises à jour des achats directement au démarrage de l'application. Désactivez donc le chargement paresseux 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 devez pouvoir 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 modifier légèrement le test pour qu'il continue de fonctionner.
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 DashPurchases ChangeNotifier
. Pour le moment, vous ne pouvez ajouter qu'un seul DashCounter
à vos Dashes achetés.
Ajoutez une propriété d'abonnement au flux, _subscription
(de type StreamSubscription<List<PurchaseDetails>> _subscription;
), le 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 nullables doivent avoir une valeur non nulle. Le qualificatif late
vous permet de différer 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
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 maintenant les mises à jour des achats. Dans la section suivante, vous allez 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 des produits réels disponibles à l'achat. Ces produits sont chargés à partir des magasins, affichés dans une liste et achetés lorsque vous appuyez dessus.
Adapter PurchasableProduct
PurchasableProduct
affiche un modèle de produit. Modifiez-la 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 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 à partir du magasin. Commencez par vérifier si le magasin est disponible. Lorsque le magasin n'est pas disponible, définir 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 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 les envoyer au service backend.
La méthode await iapConnection.queryProductDetails(ids)
renvoie à la fois les ID introuvables et les produits disponibles à l'achat. Utilisez 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();
}
Enfin, remplacez la valeur StoreState.available
du champ storeState
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 sont utilisés à l'étape suivante.
Le widget _PurchaseList
affiche la liste des produits disponibles à l'achat 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 plates-formes sont correctement configurées, vous devriez pouvoir voir les produits disponibles sur les plates-formes Android et iOS. Notez que les achats peuvent mettre un certain temps à être disponibles lorsqu'ils sont saisis dans les consoles respectives.
Revenez à dash_purchases.dart
et implémentez la fonction permettant d'acheter un produit. Vous devez seulement 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 y faire 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 traité l'achat afin que le marchand sache qu'il est correctement géré.
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 validation des achats, configurez un backend Dart pour ce faire.
Dans cette section, utilisez le dossier dart-backend/
comme racine.
Assurez-vous que les outils suivants sont installés:
- Dart
- CLI Firebase
Présentation du projet de base
Certaines parties de ce projet étant considérées comme hors du champ d'application de cet atelier de programmation, elles sont incluses dans le code de démarrage. Avant de commencer, il est judicieux de passer en revue ce qui est déjà présent dans le code de démarrage 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. 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 routage. Vous créerez ensuite un parcours pour gérer le processus de validation des achats.
Une partie déjà incluse dans le code de démarrage est 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 qui vous permettent 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 de compte de service. Générez-en une en ouvrant les paramètres du projet Firebase, en accédant à la section Comptes de service, puis en sélectionnant Générer une nouvelle clé privée.
Copiez le fichier JSON téléchargé dans le dossier assets/
, puis renommez-le service-account-firebase.json
.
Configurer l'accès à Google Play
Pour accéder au Play Store afin de valider les achats, vous devez générer un compte de service avec ces autorisations et télécharger les identifiants JSON correspondants.
- Accédez à la Google Play Console, puis commencez par la page Toutes les applications.
- Accédez à Configuration > Accès à l'API.
Si la Google Play Console vous demande de créer ou d'associer un projet existant, faites-le d'abord, puis revenez sur cette page.
- Recherchez la section permettant de définir des comptes de service, puis cliquez sur Créer un compte de service.
- Cliquez sur le lien Google Cloud Platform dans la boîte de dialogue qui s'affiche.
- Sélectionnez votre projet. Si ce n'est pas le cas, vérifiez que vous êtes connecté au bon compte Google dans la liste déroulante Compte en haut à droite.
- Après avoir sélectionné votre projet, cliquez sur + Créer un compte de service dans la barre de menu supérieure.
- Attribuez un nom au compte de service, et éventuellement une description pour vous souvenir de son objectif, puis passez à l'étape suivante.
- Attribuez au compte de service le rôle Éditeur.
- Terminez l'assistant, revenez à la page Accès aux API dans la console de développement, puis cliquez sur Actualiser les comptes de service. Le compte que vous venez de créer doit apparaître dans la liste.
- Cliquez sur Accorder l'accès pour votre nouveau compte de service.
- 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.
- Cliquez sur Inviter un utilisateur.
- Maintenant que le compte est configuré, il vous suffit de générer des identifiants. Dans la console Cloud, recherchez votre compte de service dans la liste, cliquez sur les trois points verticaux, puis sélectionnez Gérer les clés.
- Créez une clé JSON et téléchargez-la.
- Renommez le fichier téléchargé en
service-account-google-play.json,
et déplacez-le dans le répertoireassets/
.
Nous devons également 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 afin de valider les achats, vous devez configurer une clé secrète partagée:
- Ouvrez App Store Connect.
- Accédez à Mes applications,puis sélectionnez votre application.
- Dans la barre latérale de navigation, accédez à Achats via une application > Gérer.
- En haut à droite de la liste, cliquez sur Secret partagé spécifique à l'application.
- Générez un secret et copiez-le.
- Ouvrez
lib/constants.dart,
et remplacez la valeur deappStoreSharedSecret
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 de package utilisé sur Android (par exemple,com.example.dashclicker
)appStoreSharedSecret
: code secret partagé pour accéder à App Store Connect afin de valider les achats.bundleId
: ID de bundle utilisé sur iOS (par exemple,com.example.dashclicker
)
Vous pouvez ignorer le reste des constantes pour le moment.
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 backend, qui valide ensuite l'achat auprès des serveurs de la boutique concernée à l'aide du jeton fourni.
Le service de backend peut ensuite choisir de stocker l'achat et de répondre à l'application si l'achat était valide ou non.
En demandant au service backend d'effectuer la validation auprès des plates-formes de téléchargement plutôt que 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, faisant reculer l'horloge système.
Configurer le côté Flutter
Configurez l'authentification.
Comme vous allez envoyer les achats à votre service backend, vous devez vous assurer que l'utilisateur est authentifié lors de l'achat. La majeure partie de la logique d'authentification est déjà 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({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
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 POST HTTP.
Envoyez la plate-forme sélectionnée (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 ordinateur local.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
Ajoutez le firebaseNotifier
avec 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
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) {
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 plus en détail, par exemple, l'application d'un abonnement d'essai lorsque la plate-forme est temporairement indisponible. Toutefois, pour cet exemple, simplifiez-le et n'appliquez l'achat que lorsqu'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();
case storeKeyConsumable:
counter.addBoughtDashes(1000);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
Dans l'application, tout est maintenant prêt pour valider les achats.
Configurer le service de backend
Configurez ensuite la fonction Cloud pour valider les achats dans le backend.
Créer des gestionnaires d'achats
Étant donné que le flux de validation des deux magasins est presque identique, configurez une classe PurchaseHandler
abstraite avec des implémentations distinctes pour chaque magasin.
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 permettant de valider 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 constater, chaque méthode nécessite trois paramètres:
userId:
ID de l'utilisateur connecté afin de pouvoir associer les achats à l'utilisateur.productData:
Données sur le produit. Vous allez le définir 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()
pouvant être utilisée à la fois 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,
);
}
}
Vous pouvez maintenant appeler verifyPurchase
dans les deux cas, tout en conservant des implémentations distinctes.
La classe ProductData
contient des informations de base sur les différents produits disponibles à l'achat, y compris l'ID 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 non.
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 des implémentations d'espaces réservés 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 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 le moment, 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 l'AndroidPublisherApi
fournie.
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
, puis 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'achats. Créons ensuite le point de terminaison de l'API de validation des achats.
Utiliser des gestionnaires d'achat
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.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 ci-dessus effectue les opérations suivantes:
- Définissez un point de terminaison POST qui sera appelé à partir de l'application que vous avez créée précédemment.
- Décodez la charge utile JSON et extrayez les informations suivantes:
userId
: ID de l'utilisateur actuellement connectésource
: magasin utilisé (app_store
ougoogle_play
).productData
: obtenu à partir de l'productDataMap
que vous avez créé précédemment.token
: contient les données de validation à envoyer aux plates-formes de téléchargement.- Appel de la méthode
verifyPurchase
, pourGooglePlayPurchaseHandler
ouAppStorePurchaseHandler
, en fonction de la source. - Si la validation réussit, la méthode renvoie un
Response.ok
au client. - 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 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'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 gestionnaire d'achats
Ensuite, continuez à implémenter le gestionnaire d'achat 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ées dans le fichier server.dart
et vous les utilisez maintenant dans la classe GooglePlayPurchaseHandler
.
Implémentez le gestionnaire pour les achats autres que des abonnements:
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 modifier 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 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
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 via l'App Store, un package Dart tiers appelé app_store_server_sdk
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 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) {
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 désormais ê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 est le service 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 de magasin sur le backend avec le backend Dart que vous avez créé.
Traiter les événements du magasin dans le backend
Les magasins peuvent informer votre backend de tous les événements de facturation qui se produisent, par exemple lorsque les abonnements sont renouvelés. Vous pouvez traiter ces événements dans le backend pour tenir à jour les achats dans votre base de données. Dans cette section, configurez cette fonctionnalité pour le Google Play Store et l'App Store d'Apple.
Traiter les événements de facturation Google Play
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 sur lesquelles des 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 devez l'inclure dans le 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 la 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 dix secondes. Vous pouvez ajuster la durée selon vos préférences.
Créez ensuite 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 10 secondes et demande de nouveaux messages. Ensuite, il traite 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 non, en appelant les handleSubscription
ou handleNonSubscription
existants 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
]);
Créez ensuite l'instance PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Enfin, transmettez-la 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 de consommer des é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:
- Accédez à la page Cloud Pub/Sub de la Google Cloud Console.
- Assurez-vous d'être sur votre projet Firebase, puis cliquez sur + Créer un sujet.
- Attribuez un nom identique à la valeur définie pour
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
dansconstants.ts
à la nouvelle rubrique. Dans ce cas, nommez-leplay_billing
. Si vous choisissez une autre option, veillez à mettre à jourconstants.ts
. Créez le sujet. - 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.
- Dans la barre latérale de droite, sélectionnez Ajouter un compte principal.
- Ajoutez
google-play-developer-notifications@system.gserviceaccount.com
et accordez-lui le rôle d'Éditeur Pub/Sub. - Enregistrez les modifications apportées aux autorisations.
- Copiez le nom du sujet que vous venez de créer.
- Ouvrez à nouveau la Play Console, puis sélectionnez votre application dans la liste Toutes les applications.
- Faites défiler la page vers le bas, puis accédez à Monétiser > Configuration de la monétisation.
- Complétez l'intégralité du sujet, puis enregistrez vos modifications.
Tous les événements de facturation Google Play seront désormais publiés sur le sujet.
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 méthodes efficaces pour gérer les mises à jour des achats sur l'App Store. L'une d'elles consiste à implémenter un webhook que vous fournissez à Apple et qu'il utilise pour communiquer avec votre serveur. La deuxième méthode, qui est celle que vous trouverez dans cet atelier de programmation, consiste à vous connecter à l'API du serveur App Store et à obtenir manuellement les informations d'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, vous devez idéalement disposer des deux. Le webhook pour obtenir des événements depuis l'App Store et l'API Server au cas où vous auriez manqué un événement ou auriez besoin de 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;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
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 en fonction de 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:
- Obtient la liste des abonnements actifs à partir de Firestore à l'aide de IapRepository.
- Pour chaque commande, il demande l'état de l'abonnement à l'API du serveur App Store.
- Récupère la dernière transaction pour cet achat d'abonnement.
- Vérifie la date d'expiration.
- Met à jour l'état de l'abonnement dans 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:
- Connectez-vous à App Store Connect, puis sélectionnez Utilisateurs et accès.
- Accédez à Type de clé > Achat intégré.
- Appuyez sur l'icône Plus pour en ajouter un.
- Attribuez-lui un nom, par exemple "Clé du cours de programmation".
- Téléchargez le fichier P8 contenant la clé.
- Copiez-le dans le dossier "assets", en le nommant
SubscriptionKey.p8
. - Copiez l'ID de clé de la clé nouvellement créée et définissez-le sur la constante
appStoreKeyId
dans le fichierlib/constants.dart
. - Copiez l'ID de l'émetteur en haut de la liste des clés, puis définissez-le sur la constante
appStoreIssuerId
dans le fichierlib/constants.dart
.
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 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 facilement synchroniser les données avec le client et les mettre à 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'un achat avec productId storeKeySubscription
est associé à un état non 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
. C'est là que les abonnements doivent être appliqués ou supprimés. Ajoutez donc iapRepo
en tant que propriété dans la classe et attribuez-lui iapRepo
dans le constructeur. Ajoutez ensuite directement un écouteur dans le constructeur, puis supprimez-le dans la méthode dispose()
. Au début, l'écouteur peut simplement ê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
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
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,
),
É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 modifier 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
en fonction de l'achat de la mise à niveau.
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 maintenant assuré 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 de clics Dash.
12. Terminé !
Félicitations ! Vous avez terminé l'atelier de programmation. Vous trouverez le code final de cet atelier de programmation dans le dossier complete.
Pour aller plus loin, suivez les autres ateliers de programmation Flutter.