Informazioni su questo codelab
1. Introduzione
L'aggiunta di acquisti in-app a un'app Flutter richiede la configurazione corretta degli store App e Play, la verifica dell'acquisto e la concessione delle autorizzazioni necessarie, ad esempio i vantaggi dell'abbonamento.
In questo codelab aggiungerai tre tipi di acquisti in-app a un'app (fornita) e verificherai questi acquisti utilizzando un backend Dart con Firebase. L'app fornita, Dash Clicker, contiene un gioco che utilizza la mascotte Dash come valuta. Aggiungi le seguenti opzioni di acquisto:
- Un'opzione di acquisto ripetibile per 2000 Dash alla volta.
- Un acquisto di upgrade una tantum per trasformare il vecchio Dash in un Dash di stile moderno.
- Un abbonamento che raddoppia i clic generati automaticamente.
La prima opzione di acquisto offre all'utente un vantaggio diretto di 2000 Dash. Sono disponibili direttamente per l'utente e possono essere acquistati più volte. Si tratta di un prodotto consumabile perché viene consumato direttamente e può essere consumato più volte.
La seconda opzione esegue l'upgrade del riquadro a uno più bello. Deve essere acquistato una sola volta ed è disponibile per sempre. Questo tipo di acquisto è definito non di consumo perché non può essere utilizzato dall'app, ma è valido a vita.
La terza e ultima opzione di acquisto è un abbonamento. Mentre l'abbonamento è attivo, l'utente riceverà i Dash più rapidamente, ma quando smette di pagare l'abbonamento, anche i vantaggi scompaiono.
Il servizio di backend (fornito anche per te) viene eseguito come app Dart, verifica che gli acquisti vengano effettuati e li memorizza utilizzando Firestore. Firestore viene utilizzato per semplificare la procedura, ma nell'app di produzione puoi utilizzare qualsiasi tipo di servizio di backend.
Cosa creerai
- Estenderai un'app per supportare gli acquisti e gli abbonamenti con consumi.
- Estenderai anche un'app di backend Dart per verificare e archiviare gli articoli acquistati.
Cosa imparerai
- Come configurare l'App Store e il Play Store con i prodotti acquistabili.
- Come comunicare con i negozi per verificare gli acquisti e archiviarli in Firestore.
- Come gestire gli acquisti nella tua app.
Che cosa ti serve
- Android Studio 4.1 o versioni successive
- Xcode 12 o versioni successive (per lo sviluppo per iOS)
- SDK Flutter
2. Configura l'ambiente di sviluppo
Per avviare questo codelab, scarica il codice e modifica l'identificatore del bundle per iOS e il nome del pacchetto per Android.
Scarica il codice
Per clonare il repository GitHub dalla riga di comando, utilizza il seguente comando:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
In alternativa, se hai installato lo strumento cli di GitHub, utilizza il seguente comando:
gh repo clone flutter/codelabs flutter-codelabs
Il codice di esempio viene clonato in una directory flutter-codelabs
contenente il codice di una raccolta di codelab. Il codice di questo codelab è in flutter-codelabs/in_app_purchases
.
La struttura della directory in flutter-codelabs/in_app_purchases
contiene una serie di istantanee della posizione in cui dovresti trovarti alla fine di ogni passaggio denominato. Il codice di avvio è nel passaggio 0, quindi individuare i file corrispondenti è semplicissimo:
cd flutter-codelabs/in_app_purchases/step_00
Se vuoi andare avanti o vedere come dovrebbe apparire qualcosa dopo un passaggio, cerca nella directory denominata in base al passaggio che ti interessa. Il codice dell'ultimo passaggio si trova nella cartella complete
.
Configurare il progetto iniziale
Apri il progetto iniziale da step_00/app
nel tuo IDE preferito. Per gli screenshot abbiamo utilizzato Android Studio, ma Visual Studio Code è un'ottima alternativa. Con entrambi gli editor, assicurati che siano installati i plug-in Dart e Flutter più recenti.
Le app che creerai devono comunicare con l'App Store e il Play Store per sapere quali prodotti sono disponibili e a quale prezzo. Ogni app è identificata da un ID univoco. Per l'App Store di iOS si tratta dell'identificatore del pacchetto, mentre per il Play Store di Android si tratta dell'ID applicazione. Questi identificatori vengono solitamente creati utilizzando una notazione del nome di dominio inverso. Ad esempio, quando crei un'app di acquisto in-app per flutter.dev, devi utilizzare dev.flutter.inapppurchase
. Pensa a un identificatore per la tua app, che ora imposterai nelle impostazioni del progetto.
Innanzitutto, configura l'identificatore del bundle per iOS. Per farlo, apri il file Runner.xcworkspace
nell'app Xcode.
Nella struttura delle cartelle di Xcode, il progetto Runner si trova in alto e i target Flutter, Runner e Products si trovano sotto il progetto Runner. Fai doppio clic su Runner per modificare le impostazioni del progetto e poi fai clic su Firma e funzionalità. Inserisci l'identificatore del pacchetto che hai appena scelto nel campo Team per impostare il team.
Ora puoi chiudere Xcode e tornare ad Android Studio per completare la configurazione per Android. Per farlo, apri il file build.gradle.kts
in android/app,
e modifica applicationId
(nella riga 24 dello screenshot di seguito) con l'ID applicazione, lo stesso dell'identificatore del bundle per iOS. Tieni presente che gli ID per gli store iOS e Android non devono essere identici, ma mantenerli identici riduce la possibilità di errori e, pertanto, in questo codelab utilizzeremo anche identificatori identici.
3. Installare il plug-in
In questa parte del codelab installerai il plug-in in_app_purchase.
Aggiungere una dipendenza in pubspec
Aggiungi in_app_purchase
al pubspec aggiungendo in_app_purchase
alle dipendenze nel pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Apri pubspec.yaml
e verifica che in_app_purchase
sia ora elencato come voce in dependencies
e in_app_purchase_platform_interface
in dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.6.3
cupertino_icons: ^1.0.8
firebase_auth: ^5.4.2
firebase_core: ^3.11.0
google_sign_in: ^6.2.2
http: ^1.3.0
intl: ^0.20.2
provider: ^6.1.2
in_app_purchase: ^3.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
Fai clic su pub get per scaricare il pacchetto o esegui flutter pub get
nella riga di comando.
4. Configurare l'App Store
Per configurare gli acquisti in-app e testarli su iOS, devi creare una nuova app nell'App Store e creare i prodotti acquistabili al suo interno. Non devi pubblicare nulla o inviare l'app ad Apple per la revisione. Per farlo, devi disporre di un account sviluppatore. Se non ne hai uno, registrati al programma per sviluppatori Apple.
Contratti per le app a pagamento
Per utilizzare gli acquisti in-app, devi anche avere un contratto attivo per le app a pagamento in App Store Connect. Vai all'indirizzo https://appstoreconnect.apple.com/ e fai clic su Contratti, tasse e banche.
Qui vedrai i contratti per le app senza costi e a pagamento. Lo stato delle app senza costi deve essere attivo e quello delle app a pagamento deve essere nuovo. Assicurati di visualizzare i termini, accettarli e inserire tutte le informazioni richieste.
Se tutto è impostato correttamente, lo stato delle app a pagamento sarà attivo. Questo è molto importante perché non potrai provare gli acquisti in-app senza un contratto attivo.
Registra l'ID app
Crea un nuovo identificatore nel portale per sviluppatori Apple. Visita la pagina https://developer.apple.com/account/resources/identifiers/list e fai clic sull'icona "Più" accanto all'intestazione Identificatori.
Scegli gli ID app
Scegli app
Fornisci una descrizione e imposta l'ID pacchetto in modo che corrisponda allo stesso valore impostato in precedenza in Xcode.
Per ulteriori indicazioni su come creare un nuovo ID app, consulta la guida dell'account sviluppatore .
Creare una nuova app
Crea una nuova app in App Store Connect con il tuo identificatore del bundle univoco.
Per ulteriori indicazioni su come creare una nuova app e gestire i contratti, consulta la guida di App Store Connect.
Per testare gli acquisti in-app, devi avere un utente di test della sandbox. Questo utente di test non deve essere collegato a iTunes, ma viene utilizzato solo per testare gli acquisti in-app. Non puoi utilizzare un indirizzo email già utilizzato per un account Apple. In Utenti e accesso, vai a Sandbox per creare un nuovo account sandbox o per gestire gli ID Apple sandbox esistenti.
Ora puoi configurare l'utente della sandbox sul tuo iPhone andando a Impostazioni > Sviluppatore > Account Apple sandbox.
Configurare gli acquisti in-app
Ora configura i tre articoli acquistabili:
dash_consumable_2k
: un acquisto di consumabili che può essere acquistato più volte e che concede all'utente 2000 Dash (la valuta in-app) per acquisto.dash_upgrade_3d
: un acquisto di "upgrade" non consumabile che può essere acquistato una sola volta e offre all'utente un Dash to click esteticamente diverso.dash_subscription_doubler
: un abbonamento che concede all'utente il doppio dei trattini per clic per la durata dell'abbonamento.
Vai ad Acquisti in-app.
Crea gli acquisti in-app con gli ID specificati:
- Configura
dash_consumable_2k
come consumabile. Utilizzadash_consumable_2k
come ID prodotto. Il nome di riferimento viene utilizzato solo in App Store Connect, impostalo sudash consumable 2k
.Configura la disponibilità. Il prodotto deve essere disponibile nel paese dell'utente della sandbox.
Aggiungi i prezzi e imposta il prezzo su
$1.99
o sull'equivalente in un'altra valuta.Aggiungi le localizzazioni per l'acquisto. Chiama l'acquisto
Spring is in the air
con2000 dashes fly out
come descrizione.Aggiungi uno screenshot della recensione. I contenuti non sono importanti a meno che il prodotto non venga inviato per la revisione, ma è necessario che il prodotto sia nello stato "Pronto per l'invio", che è necessario quando l'app recupera i prodotti dall'App Store.
- Configura
dash_upgrade_3d
come Non consumabile. Utilizzadash_upgrade_3d
come ID prodotto. Imposta il nome del riferimento sudash upgrade 3d
. Chiama l'acquisto3D Dash
conBrings your dash back to the future
come descrizione. Imposta il prezzo su$0.99
. Configura la disponibilità e carica lo screenshot della recensione nello stesso modo in cui hai fatto per il prodottodash_consumable_2k
. - Configura
dash_subscription_doubler
come abbonamento con rinnovo automatico. Il flusso per gli abbonamenti è leggermente diverso. Innanzitutto, devi creare un gruppo di abbonamenti. Quando più abbonamenti fanno parte dello stesso gruppo, un utente può abbonarsi a uno solo alla volta, ma può eseguire facilmente l'upgrade o il downgrade tra questi abbonamenti. Basta chiamare questo grupposubscriptions
.Aggiungi la localizzazione per il gruppo di abbonamenti.
A questo punto, crea l'abbonamento. Imposta il nome di riferimento su
dash subscription doubler
e l'ID prodotto sudash_subscription_doubler
.Poi seleziona la durata dell'abbonamento di 1 settimana e le localizzazioni. Assegna a questa sottoscrizione il nome
Jet Engine
con la descrizioneDoubles your clicks
. Imposta il prezzo su$0.49
. Configura la disponibilità e carica lo screenshot della recensione nello stesso modo in cui hai fatto per il prodottodash_consumable_2k
.
Ora dovresti vedere i prodotti negli elenchi:
5. Configurare il Play Store
Come per l'App Store, avrai bisogno anche di un account sviluppatore per il Play Store. Se non ne hai ancora uno, registra un account.
Creare una nuova app
Crea una nuova app in Google Play Console:
- Apri Play Console.
- Seleziona Tutte le app > Crea app.
- Seleziona una lingua predefinita e aggiungi un titolo per l'app. Inserisci il nome dell'app così come vuoi che venga visualizzato su Google Play. Puoi modificare il nome in un secondo momento.
- Specifica che la tua applicazione è un gioco. Puoi modificarle successivamente.
- Specifica se la tua applicazione è senza costi o a pagamento.
- Completa le dichiarazioni relative alle linee guida per i contenuti e alle leggi di esportazione degli Stati Uniti.
- Seleziona Crea app.
Dopo aver creato l'app, vai alla dashboard e completa tutte le attività nella sezione Configura l'app. Qui fornisci alcune informazioni sulla tua app, ad esempio le classificazioni dei contenuti e gli screenshot.
Firmare la richiesta
Per poter testare gli acquisti in-app, devi avere caricato almeno una build su Google Play.
Per farlo, è necessario che la build di release sia firmata con qualcosa di diverso dalle chiavi di debug.
Crea un archivio chiavi
Se hai già un keystore, vai al passaggio successivo. In caso contrario, creane uno eseguendo quanto segue nella riga di comando.
Su Mac/Linux, utilizza il seguente comando:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Su Windows, utilizza il seguente comando:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Questo comando archivia il file key.jks
nella home directory. Se vuoi memorizzare il file altrove, modifica l'argomento che passi al parametro -keystore
. Tieni il
keystore
file privato; non eseguirlo nel controllo del codice sorgente pubblico
Fare riferimento all'archivio chiavi dall'app
Crea un file denominato <your app dir>/android/key.properties
contenente un riferimento al tuo 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>
Configurare la firma in Gradle
Configura la firma per la tua app modificando il file <your app dir>/android/app/build.gradle.kts
.
Aggiungi le informazioni del file del keystore dal file delle proprietà prima del blocco android
:
import java.util.Properties
import java.io.FileInputStream
plugins {
// omitted
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Carica il file key.properties
nell'oggetto keystoreProperties
.
Aggiorna il blocco buildTypes
in modo che sia impostato su:
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Configura il blocco signingConfigs
nel file build.gradle.kts
del modulo con le informazioni di configurazione della firma:
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Le build di release della tua app verranno ora firmate automaticamente.
Per ulteriori informazioni sulla firma dell'app, consulta Firmare l'app su developer.android.com.
Caricare la prima build
Dopo aver configurato l'app per la firma, dovresti essere in grado di compilare l'applicazione eseguendo:
flutter build appbundle
Per impostazione predefinita, questo comando genera una build di release e l'output è disponibile all'indirizzo <your app dir>/build/app/outputs/bundle/release/
Dalla dashboard di Google Play Console, vai a Test e release > Test > Test chiusi e crea una nuova release di test chiusi.
Quindi, carica l'app bundle app-release.aab
generato dal comando di compilazione.
Fai clic su Salva e poi su Controlla la release.
Infine, fai clic su Avvia l'implementazione per i test chiusi per attivare la release di test chiuso.
Configurare gli utenti di test
Per poter testare gli acquisti in-app, gli Account Google dei tuoi tester devono essere aggiunti in due posizioni in Google Play Console:
- Al canale di test specifico (Test interni)
- Come tester delle licenze
Per prima cosa, aggiungi il tester al canale di test interno. Torna a Test e rilascio > Test > Test interni e fai clic sulla scheda Tester.
Crea una nuova mailing list facendo clic su Crea mailing list. Assegna un nome all'elenco e aggiungi gli indirizzi email degli Account Google che devono avere accesso per testare gli acquisti in-app.
Quindi, seleziona la casella di controllo per l'elenco e fai clic su Salva modifiche.
Aggiungi quindi i tester delle licenze:
- Torna alla visualizzazione Tutte le app di Google Play Console.
- Vai a Impostazioni > Test licenza.
- Aggiungi gli stessi indirizzi email dei tester che devono poter testare gli acquisti in-app.
- Imposta Risposta licenza su
RESPOND_NORMALLY
. - Fai clic su Salva modifiche.
Configurare gli acquisti in-app
Ora dovrai configurare gli articoli acquistabili all'interno dell'app.
Come nell'App Store, devi definire tre diversi acquisti:
dash_consumable_2k
: un acquisto di consumabili che può essere acquistato più volte e che concede all'utente 2000 Dash (la valuta in-app) per acquisto.dash_upgrade_3d
: un acquisto di "upgrade" non consumabile che può essere acquistato una sola volta e che offre all'utente un Dash diverso da fare clic.dash_subscription_doubler
: un abbonamento che concede all'utente il doppio dei trattini per clic per la durata dell'abbonamento.
Innanzitutto, aggiungi i prodotti di consumo e non di consumo.
- Vai a Google Play Console e seleziona la tua applicazione.
- Vai a Monetizza > Prodotti > Prodotti in-app.
- Fai clic su Crea prodotto
.
- Inserisci tutte le informazioni richieste per il prodotto. Assicurati che l'ID prodotto corrisponda esattamente all'ID che intendi utilizzare.
- Fai clic su Salva.
- Fai clic su Attiva.
- Ripeti la procedura per l'acquisto di un "upgrade" non consumabile.
Poi aggiungi l'abbonamento:
- Vai a Google Play Console e seleziona la tua applicazione.
- Vai a Monetizza > Prodotti > Abbonamenti.
- Fai clic su Crea abbonamento
- Inserisci tutte le informazioni richieste per l'abbonamento. Assicurati che l'ID prodotto corrisponda esattamente a quello che intendi utilizzare.
- Fai clic su Salva.
A questo punto, i tuoi acquisti dovrebbero essere configurati in Play Console.
6. Configura Firebase
In questo codelab utilizzerai un servizio di backend per verificare e monitorare gli acquisti degli utenti.
L'utilizzo di un servizio di backend presenta diversi vantaggi:
- Puoi verificare in modo sicuro le transazioni.
- Puoi reagire agli eventi di fatturazione dagli store.
- Puoi tenere traccia degli acquisti in un database.
- Gli utenti non potranno ingannare la tua app facendo in modo che fornisca funzionalità premium facendo retrocedere l'orologio di sistema.
Sebbene esistano molti modi per configurare un servizio di backend, lo farai utilizzando le funzioni cloud e Firestore, con Firebase di Google.
La scrittura del backend non rientra nell'ambito di questo codelab, pertanto il codice di avvio include già un progetto Firebase che gestisce gli acquisti di base per iniziare.
I plug-in Firebase sono inclusi anche nell'app di avvio.
Non ti resta che creare il tuo progetto Firebase, configurare l'app e il backend per Firebase e infine eseguire il deployment del backend.
Creare un progetto Firebase
Vai alla Console Firebase e crea un nuovo progetto Firebase. Per questo esempio, chiama il progetto Dash Clicker.
Nell'app di backend, colleghi gli acquisti a un utente specifico, quindi devi autenticarti. A tale scopo, utilizza il modulo di autenticazione di Firebase con Accedi con Google.
- Nella dashboard di Firebase, vai ad Autenticazione e attivala, se necessario.
- Vai alla scheda Metodo di accesso e attiva il provider di accesso Google.
Poiché utilizzerai anche il database Firestore di Firebase, abilita anche questo.
Imposta le regole di Cloud Firestore come segue:
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
}
}
}
Configurare Firebase per Flutter
Il modo consigliato per installare Firebase nell'app Flutter è utilizzare l'interfaccia a riga di comando FlutterFire. Segui le istruzioni riportate nella pagina di configurazione.
Quando esegui flutterfire configure, seleziona il progetto appena creato nel passaggio precedente.
$ 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>
Successivamente, abilita iOS e Android selezionando le due piattaforme.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
Quando ti viene chiesto di eseguire l'override di firebase_options.dart, seleziona Sì.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Configurare Firebase per Android: passaggi successivi
Nella dashboard di Firebase, vai a Panoramica progetto,scegli Impostazioni e seleziona la scheda Generali.
Scorri verso il basso fino a Le tue app e seleziona l'app dashclicker (android).
Per consentire l'accesso con Google in modalità di debug, devi fornire l'impronta dell'hash SHA-1 del tuo certificato di debug.
Ottieni l'hash del certificato di firma di debug
Nella directory principale del progetto dell'app Flutter, cambia directory nella cartella android/
e genera un report sulla firma.
cd android ./gradlew :app:signingReport
Viene visualizzato un ampio elenco di chiavi di firma. Poiché stai cercando l'hash per il certificato di debug, cerca il certificato con le proprietà Variant
e Config
impostate su debug
. È probabile che il keystore si trovi nella home directory in .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
Copia l'hash SHA-1 e compila l'ultimo campo nella finestra di dialogo modale di invio dell'app.
Infine, esegui di nuovo il comando flutterfire configure
per aggiornare l'app in modo da includere la configurazione della firma.
$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
Configurare Firebase per iOS: passaggi successivi
Apri ios/Runner.xcworkspace
con Xcode
. In alternativa, puoi utilizzare l'IDE che preferisci.
In VSCode, fai clic con il tasto destro del mouse sulla cartella ios/
e poi su open in xcode
.
In Android Studio, fai clic con il tasto destro del mouse sulla cartella ios/
, poi fai clic su flutter
e poi sull'opzione open iOS module in Xcode
.
Per consentire l'accesso con Google su iOS, aggiungi l'opzione di configurazione CFBundleURLTypes
ai file plist
di compilazione. Per ulteriori informazioni, consulta la documentazione del pacchetto google_sign_in
. In questo caso, i file sono ios/Runner/Info.plist
e ios/Runner/Info.plist
.
La coppia chiave-valore è già stata aggiunta, ma i relativi valori devono essere sostituiti:
- Recupera il valore di
REVERSED_CLIENT_ID
dal fileGoogleService-Info.plist
, senza l'elemento<string>..</string>
che lo circonda. - Sostituisci il valore nel file
ios/Runner/Info.plist
sotto la chiaveCFBundleURLTypes
.
<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 configurazione di Firebase è stata completata.
7. Ascoltare gli aggiornamenti sugli acquisti
In questa parte del codelab preparerai l'app per l'acquisto dei prodotti. Questa procedura include l'ascolto di aggiornamenti e errori relativi agli acquisti dopo l'avvio dell'app.
Ascoltare gli aggiornamenti sugli acquisti
In main.dart,
trova il widget MyHomePage
che ha un Scaffold
con un BottomNavigationBar
contenente due pagine. Questa pagina crea anche tre Provider
per DashCounter
, DashUpgrades,
e DashPurchases
. DashCounter
tiene traccia del conteggio corrente dei trattini e li incrementa automaticamente. DashUpgrades
gestisce gli upgrade che puoi acquistare con Dash. Questo codelab è incentrato su DashPurchases
.
Per impostazione predefinita, l'oggetto di un fornitore viene definito alla prima richiesta. Questo oggetto ascolta gli aggiornamenti degli acquisti direttamente all'avvio dell'app, quindi disattiva il caricamento lento su questo oggetto con lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
È necessaria anche un'istanza di InAppPurchaseConnection
. Tuttavia, per mantenere l'app testabile, devi trovare un modo per simulare la connessione. Per farlo, crea un metodo di istanza che può essere sostituito nel test e aggiungilo a main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
Devi aggiornare leggermente il test affinché continui a funzionare.
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.
In lib/logic/dash_purchases.dart
, vai al codice di DashPurchases ChangeNotifier
. Al momento, puoi aggiungere solo un DashCounter
ai tuoi Dash acquistati.
Aggiungi una proprietà di abbonamento allo stream, _subscription
(di tipo StreamSubscription<List<PurchaseDetails>> _subscription;
), IAPConnection.instance,
e le importazioni. Il codice risultante dovrebbe avere il seguente aspetto:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
La parola chiave late
viene aggiunta a _subscription
perché _subscription
viene inizializzato nel costruttore. Questo progetto è configurato per essere non nullo per impostazione predefinita (NNBD), il che significa che le proprietà non dichiarate come nulle devono avere un valore non nullo. Il qualificatore late
ti consente di posticipare la definizione di questo valore.
Nel costruttore, recupera lo stream purchaseUpdated
e inizia ad ascoltarlo. Nel metodo dispose()
, annulla l'abbonamento allo stream.
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.
}
Ora l'app riceve gli aggiornamenti sugli acquisti, quindi nella prossima sezione effettuerai un acquisto.
Prima di procedere, esegui i test con "flutter test"
per verificare che tutto sia configurato correttamente.
$ flutter test
00:01 +1: All tests passed!
8. Effettuare acquisti.
In questa parte del codelab, sostituirai i prodotti simulati esistenti con prodotti acquistabili reali. Questi prodotti vengono caricati dai negozi, mostrati in un elenco e acquistati quando vengono toccati.
Adattare PurchasableProduct
PurchasableProduct
mostra un prodotto simulato. Aggiornalo per mostrare i contenuti effettivi sostituendo la classe PurchasableProduct
in purchasable_product.dart
con il seguente codice:
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;
}
In dash_purchases.dart,
rimuovi gli acquisti simulati e sostituiscili con un elenco vuoto, List<PurchasableProduct> products = [];
Carica gli acquisti disponibili
Per consentire a un utente di effettuare un acquisto, carica gli acquisti dal negozio. Innanzitutto, controlla se il negozio è disponibile. Quando il negozio non è disponibile, l'impostazione di storeState
su notAvailable
mostra un messaggio di errore all'utente.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Quando lo store è disponibile, carica gli acquisti disponibili. Data la configurazione precedente di Google Play e App Store, dovresti vedere storeKeyConsumable
, storeKeySubscription,
e storeKeyUpgrade
. Quando un acquisto previsto non è disponibile, stampa queste informazioni nella console. Potresti anche inviarle al servizio di backend.
Il metodo await iapConnection.queryProductDetails(ids)
restituisce sia gli ID non trovati sia i prodotti acquistabili trovati. Utilizza productDetails
dalla risposta per aggiornare l'interfaccia utente e imposta StoreState
su 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();
}
Chiama la funzione loadPurchases()
nel costruttore:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Infine, modifica il valore del campo storeState
da StoreState.available
a StoreState.loading:
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Mostrare i prodotti acquistabili
Prendi in considerazione il file purchase_page.dart
. Il widget PurchasePage
mostra _PurchasesLoading
, _PurchaseList,
o _PurchasesNotAvailable,
a seconda del StoreState
. Il widget mostra anche gli acquisti passati dell'utente, che vengono utilizzati nel passaggio successivo.
Il widget _PurchaseList
mostra l'elenco dei prodotti acquistabili e invia una richiesta di acquisto all'oggetto 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(),
);
}
}
Se sono configurati correttamente, dovresti riuscire a vedere i prodotti disponibili negli store Android e iOS. Tieni presente che potrebbe essere necessario del tempo prima che gli acquisti siano disponibili quando vengono inseriti nelle rispettive console.
Torna a dash_purchases.dart
e implementa la funzione per acquistare un prodotto. Devi solo separare i materiali di consumo da quelli non di consumo. I prodotti di upgrade e di abbonamento non sono consumabili.
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');
}
}
Prima di continuare, crea la variabile _beautifiedDashUpgrade
e aggiorna il getter beautifiedDash
per farvi riferimento.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
Il metodo _onPurchaseUpdate
riceve gli aggiornamenti degli acquisti, aggiorna lo stato del prodotto mostrato nella pagina di acquisto e applica l'acquisto alla logica del contatore. È importante chiamare completePurchase
dopo aver gestito l'acquisto in modo che il negozio sappia che l'acquisto è stato gestito correttamente.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. Configura il backend
Prima di passare al monitoraggio e alla verifica degli acquisti, configura un backend Dart per supportare questa operazione.
In questa sezione, lavora dalla cartella dart-backend/
come cartella principale.
Assicurati di avere installato i seguenti strumenti:
- Dart
- Firebase CLI
Panoramica del progetto di base
Poiché alcune parti di questo progetto sono considerate non pertinenti per questo codelab, sono incluse nel codice di avvio. Ti consigliamo di esaminare cosa è già presente nel codice di avvio prima di iniziare, per avere un'idea di come strutturare il tutto.
Questo codice di backend può essere eseguito localmente sulla tua macchina e non è necessario eseguirlo per utilizzarlo. Tuttavia, devi essere in grado di connetterti dal tuo dispositivo di sviluppo (Android o iPhone) alla macchina su cui verrà eseguito il server. Per farlo, devono essere nella stessa rete e devi conoscere l'indirizzo IP del tuo computer.
Prova a eseguire il server utilizzando il seguente comando:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
Il backend Dart utilizza shelf
e shelf_router
per pubblicare gli endpoint API. Per impostazione predefinita, il server non fornisce percorsi. In un secondo momento, creerai un percorso per gestire la procedura di verifica dell'acquisto.
Una parte già inclusa nel codice di avvio è IapRepository
in lib/iap_repository.dart
. Poiché l'apprendimento di come interagire con Firestore o con i database in generale non è considerato pertinente per questo codelab, il codice di avvio contiene funzioni per creare o aggiornare gli acquisti in Firestore, nonché tutte le classi per questi acquisti.
Configurare l'accesso a Firebase
Per accedere a Firebase Firestore, devi disporre di una chiave di accesso all'account di servizio. Generane uno aprendo le impostazioni del progetto Firebase e vai alla sezione Account di servizio, quindi seleziona Genera nuova chiave privata.
Copia il file JSON scaricato nella cartella assets/
e rinominalo in service-account-firebase.json
.
Configurare l'accesso a Google Play
Per accedere al Play Store per la verifica degli acquisti, devi generare un account di servizio con queste autorizzazioni e scaricare le relative credenziali JSON.
- Visita la pagina dell'API Google Play Android for Developers nella console Google Cloud.
Se Google Play Console ti chiede di creare o collegarti a un progetto esistente, esegui prima questa operazione e poi torna a questa pagina.
- Poi vai alla pagina Account di servizio e fai clic su + Crea account di servizio.
- Inserisci il nome dell'account di servizio e fai clic su Crea e continua.
- Seleziona il ruolo Sottoscrittore Pub/Sub e fai clic su Fine.
- Una volta creato l'account, vai a Gestisci chiavi.
- Seleziona Aggiungi chiave > Crea nuova chiave.
- Crea e scarica una chiave JSON.
- Rinomina il file scaricato in
service-account-google-play.json,
e spostalo nella directoryassets/
. - Poi, vai alla pagina Utenti e autorizzazioni in Play Console
- Fai clic su Invita nuovi utenti e inserisci l'indirizzo email dell'account di servizio creato in precedenza. Puoi trovare l'email nella tabella della pagina Account di servizio
- Concedi le autorizzazioni Visualizzazione di dati finanziari e Gestisci ordini e abbonamenti per l'applicazione.
- Fai clic su Invita utente.
Un'altra cosa che dobbiamo fare è aprire lib/constants.dart,
e sostituire il valore di lib/constants.dart,
con l'ID pacchetto che hai scelto per la tua app per Android.androidPackageId
Configurare l'accesso all'Apple App Store
Per accedere all'App Store per la verifica degli acquisti, devi configurare un segreto condiviso:
- Apri App Store Connect.
- Vai a Le mie app e seleziona la tua app.
- Nel riquadro di navigazione della barra laterale, vai a Generale > Informazioni sull'app.
- Fai clic su Gestisci sotto l'intestazione Segreto condiviso specifico per l'app.
- Genera un nuovo secret e copialo.
- Apri
lib/constants.dart,
e sostituisci il valore diappStoreSharedSecret
con il secret condiviso appena generato.
File di configurazione delle costanti
Prima di procedere, assicurati che le seguenti costanti siano configurate nel file lib/constants.dart
:
androidPackageId
: ID pacchetto utilizzato su Android, ad esempiocom.example.dashclicker
appStoreSharedSecret
: segreto condiviso per accedere ad App Store Connect ed eseguire la verifica dell'acquisto.bundleId
: ID bundle utilizzato su iOS, ad es.com.example.dashclicker
Per il momento puoi ignorare il resto delle costanti.
10. Verificare gli acquisti
La procedura generale per la verifica degli acquisti è simile per iOS e Android.
Per entrambi i negozi, la tua applicazione riceve un token quando viene effettuato un acquisto.
Questo token viene inviato dall'app al tuo servizio di backend, che a sua volta verifica l'acquisto con i server del rispettivo negozio utilizzando il token fornito.
Il servizio di backend può quindi scegliere di memorizzare l'acquisto e rispondere all'applicazione se l'acquisto è valido o meno.
Se la convalida con i negozi viene eseguita dal servizio di backend anziché dall'applicazione in esecuzione sul dispositivo dell'utente, puoi impedire all'utente di accedere alle funzionalità premium, ad esempio facendo retrocedere l'orologio di sistema.
Configurare il lato Flutter
Configurare l'autenticazione
Poiché invierai gli acquisti al tuo servizio di backend, devi assicurarti che l'utente sia autenticato durante l'acquisto. La maggior parte della logica di autenticazione è già stata aggiunta nel progetto iniziale, devi solo assicurarti che PurchasePage
mostri il pulsante di accesso quando l'utente non ha ancora eseguito l'accesso. Aggiungi il codice seguente all'inizio del metodo di compilazione di 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
Chiamare l'endpoint di verifica dall'app
Nell'app, crea la funzione _verifyPurchase(PurchaseDetails purchaseDetails)
che chiama l'endpoint /verifypurchase
nel tuo backend Dart utilizzando una chiamata post HTTP.
Invia il negozio selezionato (google_play
per il Play Store o app_store
per l'App Store), il serverVerificationData
e il productID
. Il server restituisce il codice di stato che indica se l'acquisto è stato verificato.
Nelle costanti dell'app, configura l'IP del server con l'indirizzo IP della tua macchina locale.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
Aggiungi firebaseNotifier
con la creazione di DashPurchases
in main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Aggiungi un getter per l'utente in FirebaseNotifier, in modo da poter passare l'ID utente alla funzione di verifica dell'acquisto.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
Aggiungi la funzione _verifyPurchase
alla classe DashPurchases
. Questa funzione async
restituisce un valore booleano che indica se l'acquisto è stato convalidato.
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;
}
}
Chiama la funzione _verifyPurchase
in _handlePurchase
appena prima di applicare l'acquisto. Dovresti applicare l'acquisto solo quando è stato verificato. In un'app di produzione, puoi specificare ulteriori dettagli, ad esempio applicare un abbonamento di prova quando lo store non è temporaneamente disponibile. Tuttavia, per questo esempio, mantieni la semplicità e applica l'acquisto solo quando viene verificato correttamente.
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);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
Nell'app è tutto pronto per convalidare gli acquisti.
Configura il servizio di backend
Successivamente, configura il backend per la verifica degli acquisti.
Creare gestori degli acquisti
Poiché il flusso di verifica per entrambi i negozi è quasi identico, configura una classe PurchaseHandler
astratta con implementazioni separate per ogni negozio.
Inizia aggiungendo un file purchase_handler.dart
alla cartella lib/
, in cui definisci una classe PurchaseHandler
astratta con due metodi astratti per la verifica di due diversi tipi di acquisti: abbonamenti e non abbonamenti.
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,
});
Come puoi vedere, ogni metodo richiede tre parametri:
userId:
L'ID dell'utente che ha eseguito l'accesso, in modo da poter associare gli acquisti all'utente.productData:
Dati sul prodotto. Lo definirai tra un minuto.token:
Il token fornito all'utente dal negozio.
Inoltre, per semplificare l'utilizzo di questi gestori degli acquisti, aggiungi un metodo verifyPurchase()
che può essere utilizzato sia per gli abbonamenti che per i non abbonamenti:
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,
);
}
}
Ora puoi semplicemente chiamare verifyPurchase
per entrambi i casi, ma avere comunque implementazioni separate.
La classe ProductData
contiene informazioni di base sui diversi prodotti acquistabili, tra cui l'ID prodotto (a volte indicato anche come SKU) e ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
ProductType
può essere un abbonamento o meno.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
Infine, l'elenco dei prodotti è definito come una mappa nello stesso file.
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,
),
};
Successivamente, definisci alcune implementazioni di segnaposto per il Google Play Store e l'Apple App Store. Inizia a utilizzare Google Play:
Crea lib/google_play_purchase_handler.dart
e aggiungi una classe che estenda PurchaseHandler
che hai appena scritto:
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;
}
}
Per il momento restituisce true
per i metodi di gestore, che esamineremo in seguito.
Come avrai notato, il costruttore prende un'istanza di IapRepository
. Il gestore degli acquisti utilizza questa istanza per archiviare in un secondo momento le informazioni sugli acquisti in Firestore. Per comunicare con Google Play, utilizzi il AndroidPublisherApi
fornito.
Ripeti la stessa operazione per l'handler dello store. Crea lib/app_store_purchase_handler.dart
e aggiungi un'altra classe che espanda PurchaseHandler
:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Bene. Ora hai due gestori degli acquisti. Ora creiamo l'endpoint dell'API di verifica dell'acquisto.
Utilizzare i gestori degli acquisti
Apri bin/server.dart
e crea un endpoint API utilizzando shelf_route
:
bin/server.dart
import 'dart:convert'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart'; // new
import 'package:shelf/shelf.dart'; // new
import 'package:shelf_router/shelf_router.dart';
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
Il codice riportato sopra esegue le seguenti operazioni:
- Definisci un endpoint POST che verrà chiamato dall'app creata in precedenza.
- Decodifica il payload JSON ed estrai le seguenti informazioni:
userId
: ID utente che ha eseguito l'accessosource
: negozio utilizzato,app_store
ogoogle_play
.productData
: ottenuto daproductDataMap
creato in precedenza.token
: contiene i dati di verifica da inviare ai negozi.- Chiama il metodo
verifyPurchase
perGooglePlayPurchaseHandler
oAppStorePurchaseHandler
, a seconda dell'origine. - Se la verifica è andata a buon fine, il metodo restituisce un
Response.ok
al client. - Se la verifica non va a buon fine, il metodo restituisce un
Response.internalServerError
al client.
Dopo aver creato l'endpoint API, devi configurare i due gestori degli acquisti. Per farlo, devi caricare le chiavi dell'account di servizio ottenute nel passaggio precedente e configurare l'accesso ai diversi servizi, tra cui l'API Android Publisher e l'API Firebase Firestore. Quindi, crea i due gestori degli acquisti con le diverse dipendenze:
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Verificare gli acquisti su Android: implementare il gestore degli acquisti
Continua a implementare il gestore degli acquisti di Google Play.
Google fornisce già pacchetti Dart per interagire con le API necessarie per verificare gli acquisti. Le hai inizializzate nel file server.dart
e ora le utilizzi nella classe GooglePlayPurchaseHandler
.
Implementa il gestore per gli acquisti di tipo diverso dall'abbonamento:
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;
}
Puoi aggiornare il gestore dell'acquisto dell'abbonamento in modo simile:
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;
}
}
Aggiungi il seguente metodo per facilitare l'analisi degli ID ordine, nonché due metodi per analizzare lo stato dell'acquisto.
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;
}
A questo punto, i tuoi acquisti su Google Play dovrebbero essere stati verificati e archiviati nel database.
A questo punto, passa agli acquisti effettuati sull'App Store per iOS.
Verificare gli acquisti su iOS: implementare il gestore degli acquisti
Per verificare gli acquisti con l'App Store, esiste un pacchetto Dart di terze parti denominato app_store_server_sdk
che semplifica la procedura.
Per iniziare, crea l'istanza ITunesApi
. Utilizza la configurazione della sandbox e attiva il logging per facilitare il debug degli errori.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
Ora, a differenza delle API di Google Play, l'App Store utilizza gli stessi endpoint API sia per gli abbonamenti sia per i non abbonamenti. Ciò significa che puoi utilizzare la stessa logica per entrambi i gestori. Uniscili in modo che chiamino la stessa implementazione:
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 {
//..
}
Ora implementa handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
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;
}
}
A questo punto, i tuoi acquisti effettuati sull'App Store dovrebbero essere stati verificati e archiviati nel database.
Esegui il backend
A questo punto, puoi eseguire dart bin/server.dart
per pubblicare l'endpoint /verifypurchase
.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. Monitorare gli acquisti
Il modo consigliato per monitorare gli acquisti degli utenti è nel servizio di backend. Questo perché il backend può rispondere agli eventi del negozio ed è quindi meno soggetto a informazioni obsolete a causa della memorizzazione nella cache, oltre a essere meno soggetto a manomissioni.
Innanzitutto, configura l'elaborazione degli eventi in negozio sul backend con il backend Dart che hai creato.
Elaborare gli eventi negozio nel backend
I negozi hanno la possibilità di informare il tuo backend di eventuali eventi di fatturazione, ad esempio quando gli abbonamenti vengono rinnovati. Puoi elaborare questi eventi nel tuo backend per mantenere aggiornati gli acquisti nel tuo database. In questa sezione, configura questa opzione sia per il Google Play Store sia per l'Apple App Store.
Elaborare gli eventi di fatturazione di Google Play
Google Play fornisce eventi di fatturazione tramite un argomento Cloud Pub/Sub. Si tratta essenzialmente di code di messaggi su cui è possibile pubblicare messaggi e da cui è possibile consumarli.
Poiché si tratta di una funzionalità specifica di Google Play, devi includerla in GooglePlayPurchaseHandler
.
Per iniziare, apri lib/google_play_purchase_handler.dart
e aggiungi l'importazione di PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Quindi, passa il PubsubApi
al GooglePlayPurchaseHandler
e modifica il costruttore della classe per creare un Timer
come segue:
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
è configurato per chiamare il metodo _pullMessageFromPubSub
ogni dieci secondi. Puoi regolare la durata in base alle tue preferenze.
Poi, crea il _pullMessageFromPubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$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,
);
}
Il codice che hai appena aggiunto comunica con l'argomento Pub/Sub di Google Cloud ogni dieci secondi e richiede nuovi messaggi. Quindi, elabora ogni messaggio nel metodo _processMessage
.
Questo metodo decodifica i messaggi in arrivo e ottiene le informazioni aggiornate su ogni acquisto, sia con abbonamento che senza, chiamando handleSubscription
o handleNonSubscription
esistenti, se necessario.
Ogni messaggio deve essere confermato con il metodo _askMessage
.
Aggiungi le dipendenze richieste al file server.dart
. Aggiungi PubsubApi.cloudPlatformScope alla configurazione delle credenziali:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
Quindi, crea l'istanza PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Infine, passalo al costruttore GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Configurazione di Google Play
Hai scritto il codice per utilizzare gli eventi di fatturazione dall'argomento Pub/Sub, ma non hai creato l'argomento Pub/Sub né stai pubblicando eventi di fatturazione. È il momento di configurare il servizio.
Innanzitutto, crea un argomento Pub/Sub:
- Imposta il valore di
googleCloudProjectId
inconstants.dart
sull'ID del tuo progetto Google Cloud. - Visita la pagina Cloud Pub/Sub nella console Google Cloud.
- Assicurati di essere nel progetto Firebase e fai clic su + Crea argomento.
- Assegna un nome al nuovo argomento, identico al valore impostato per
googlePlayPubsubBillingTopic
inconstants.dart
. In questo caso, chiamaloplay_billing
. Se scegli qualcos'altro, assicurati di aggiornareconstants.dart
. Crea l'argomento. - Nell'elenco degli argomenti Pub/Sub, fai clic sui tre puntini verticali dell'argomento che hai appena creato e poi su Visualizza autorizzazioni.
- Nella barra laterale a destra, scegli Aggiungi entità.
- Aggiungi
google-play-developer-notifications@system.gserviceaccount.com
e concedi il ruolo di Publisher Pub/Sub. - Salva le modifiche alle autorizzazioni.
- Copia il nome dell'argomento appena creato.
- Apri di nuovo Play Console e scegli la tua app dall'elenco Tutte le app.
- Scorri verso il basso e vai a Monetizzazione > Configurazione della monetizzazione.
- Compila l'argomento completo e salva le modifiche.
Ora tutti gli eventi di fatturazione di Google Play verranno pubblicati nell'argomento.
Elaborare gli eventi di fatturazione dell'App Store
Ripeti la stessa operazione per gli eventi di fatturazione dell'App Store. Esistono due modi efficaci per implementare gli aggiornamenti della gestione degli acquisti per l'App Store. Una consiste nell'implementare un webhook che fornisci ad Apple e che viene utilizzato per comunicare con il tuo server. Il secondo metodo, che troverai in questo codelab, consiste nel connetterti all'API App Store Server e ottenere manualmente le informazioni sull'abbonamento.
Il motivo per cui questo codelab si concentra sulla seconda soluzione è che per implementare l'webhook devi esporre il tuo server a internet.
In un ambiente di produzione, l'ideale sarebbe avere entrambi. Il webhook per ottenere gli eventi dall'App Store e l'API di Server nel caso in cui tu abbia perso un evento o debba verificare lo stato di un abbonamento.
Per iniziare, apri lib/app_store_purchase_handler.dart
e aggiungi la dipendenza AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
Modifica il costruttore per aggiungere un timer che chiamerà il metodo _pullStatus
. Questo timer chiamerà il metodo _pullStatus
ogni 10 secondi. Puoi regolare la durata del timer in base alle tue esigenze.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Poi, crea il metodo _pullStatus come segue:
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,
));
}
}
}
}
Questo metodo funziona nel seguente modo:
- Recupera l'elenco degli abbonamenti attivi da Firestore utilizzando IapRepository.
- Per ogni ordine, richiede lo stato dell'abbonamento all'API App Store Server.
- Recupera l'ultima transazione per l'acquisto dell'abbonamento.
- Controlla la data di scadenza.
- Aggiorna lo stato dell'abbonamento su Firestore. Se è scaduto, verrà contrassegnato come tale.
Infine, aggiungi tutto il codice necessario per configurare l'accesso all'API App Store Server:
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
),
};
Configurazione dell'App Store
Poi, configura l'App Store:
- Accedi ad App Store Connect e seleziona Utenti e accesso.
- Vai ad Integrazioni > Chiavi > Acquisto in-app.
- Tocca l'icona "Più" per aggiungerne uno nuovo.
- Assegna un nome, ad esempio "Chiave Codelab".
- Scarica il file p8 contenente la chiave.
- Copialo nella cartella degli asset con il nome
SubscriptionKey.p8
. - Copia l'ID chiave dalla chiave appena creata e impostalo come costante
appStoreKeyId
nel filelib/constants.dart
. - Copia l'ID emittente in alto nell'elenco delle chiavi e impostalo come costante
appStoreIssuerId
nel filelib/constants.dart
.
Monitorare gli acquisti sul dispositivo
Il modo più sicuro per monitorare gli acquisti è lato server perché il client è difficile da proteggere, ma devi avere un modo per recapitare le informazioni al client in modo che l'app possa intervenire in base alle informazioni sullo stato dell'abbonamento. Se archivi gli acquisti in Firestore, puoi sincronizzare facilmente i dati con il client e mantenerli aggiornati automaticamente.
Hai già incluso nell'app IAPRepo, il repository Firestore che contiene tutti i dati di acquisto dell'utente in List<PastPurchase> purchases
. Il repository contiene anche hasActiveSubscription,
, che è true quando esiste un acquisto con productId storeKeySubscription
con uno stato non scaduto. Quando l'utente non ha eseguito l'accesso, l'elenco è vuoto.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
Tutta la logica di acquisto si trova nella classe DashPurchases
ed è qui che devono essere applicati o rimossi gli abbonamenti. Aggiungi quindi iapRepo
come proprietà del corso e assegnalo al costruttore.iapRepo
Aggiungi un ascoltatore direttamente nel costruttore e rimuovilo nel metodo dispose()
. All'inizio, l'ascoltatore può essere solo una funzione vuota. Poiché IAPRepo
è un ChangeNotifier
e chiami notifyListeners()
ogni volta che gli acquisti in Firestore cambiano, il metodo purchasesUpdate()
viene sempre chiamato quando i prodotti acquistati cambiano.
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
}
Poi, fornisci IAPRepo
al costruttore in main.dart.
. Puoi ottenere il repository utilizzando context.read
perché è già stato creato in un Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
Poi, scrivi il codice per la funzione purchaseUpdate()
. In dash_counter.dart,
, i metodi applyPaidMultiplier
e removePaidMultiplier
impostano il moltiplicatore rispettivamente su 10 o 1, quindi non devi controllare se l'abbonamento è già applicato. Quando lo stato dell'abbonamento cambia, aggiorna anche lo stato del prodotto acquistabile in modo da poter mostrare nella pagina di acquisto che è già attivo. Imposta la proprietà _beautifiedDashUpgrade
in base all'acquisto o meno dell'upgrade.
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();
}
}
Ora hai verificato che lo stato dell'abbonamento e dell'upgrade sia sempre aggiornato nel servizio di backend e sincronizzato con l'app. L'app agisce di conseguenza e applica le funzionalità di abbonamento e upgrade al tuo gioco di clicker Dash.
12. Operazione completata.
Complimenti!!! Hai completato il codelab. Puoi trovare il codice completo di questo codelab nella cartella completa.
Per scoprire di più, prova gli altri codelab Flutter.