Aggiungere acquisti in-app all'app Flutter

1. Introduzione

Ultimo aggiornamento: 2023-07-11

L'aggiunta di acquisti in-app a un'app Flutter richiede la corretta configurazione dell'App Store e del Play Store, la verifica dell'acquisto e la concessione delle autorizzazioni necessarie, come i vantaggi dell'abbonamento.

In questo codelab, aggiungerai tre tipi di acquisti in-app a un'app (fornita da te) 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. Aggiungerai le seguenti opzioni di acquisto:

  1. Un'opzione di acquisto ripetibile per 2000 trattini alla volta.
  2. Acquisto di un upgrade una tantum per trasformare il vecchio stile della Dash in uno stile moderno.
  3. Un abbonamento che raddoppia i clic generati automaticamente.

La prima opzione di acquisto offre all'utente un vantaggio diretto di 2000 trattini. Sono direttamente disponibili per l'utente e possono essere acquistate più volte. Questo viene chiamato consumabile in quanto viene consumato direttamente e può essere utilizzato più volte.

La seconda opzione consente di passare a una dashboard più bella. Deve essere acquistato una sola volta ed è disponibile per sempre. Un acquisto del genere è definito "non consumabile" perché non può essere utilizzato dall'app, ma è valido per sempre.

La terza e ultima opzione di acquisto è l'abbonamento. Mentre l'abbonamento è attivo, l'utente riceverà i trattini più rapidamente, ma quando smetterà di pagare l'abbonamento scompaiono anche i relativi vantaggi.

Il servizio di backend (fornito anche per te) viene eseguito come app Dart, verifica che gli acquisti siano stati effettuati e li archivia tramite Firestore. Firestore viene utilizzato per semplificare il processo, ma nella tua app di produzione puoi utilizzare qualsiasi tipo di servizio di backend.

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

Cosa creerai

  • Estenderai un'app per supportare acquisti di consumo e abbonamenti.
  • Estenderai inoltre un'app di backend Dart per verificare e archiviare gli articoli acquistati.

Cosa imparerai

  • Come configurare App Store e Play Store con 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 versione successiva (per sviluppo iOS)
  • SDK Flutter

2. Configurare l'ambiente di sviluppo

Per avviare questo codelab, scarica il codice e modifica l'identificatore pacchetto per iOS e il nome del pacchetto per Android.

Scarica il codice

Per clonare il repository GitHub dalla riga di comando, utilizza questo comando:

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

In alternativa, se hai installato lo strumento interfaccia a riga di comando di GitHub, utilizza il seguente comando:

gh repo clone flutter/codelabs flutter-codelabs

Il codice campione viene clonato in una directory flutter-codelabs che contiene il codice di una raccolta di codelab. Il codice di questo codelab è in flutter-codelabs/in_app_purchases.

La struttura di directory in flutter-codelabs/in_app_purchases contiene una serie di snapshot della posizione in cui ti trovi alla fine di ogni passaggio denominato. Il codice di base si trova al passaggio 0, quindi l'individuazione dei file corrispondenti è semplice quanto:

cd flutter-codelabs/in_app_purchases/step_00

Se vuoi andare avanti o vedere come dovrebbe apparire un elemento 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 nel tuo IDE preferito. Abbiamo usato Android Studio per gli screenshot, ma anche Visual Studio Code è un'ottima opzione. Con entrambi gli editor, assicurati che siano installati i plug-in Dart e Flutter più recenti.

Le app che hai intenzione di creare 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. Nell'App Store iOS è l'identificatore pacchetto, mentre per il Play Store Android è l'ID applicazione. Questi identificatori vengono in genere creati utilizzando una notazione del nome di dominio inversa. Ad esempio, per effettuare un acquisto in-app per flutter.dev, utilizzeremo dev.flutter.inapppurchase. Pensa a un identificatore per la tua app: ora lo imposterai nelle impostazioni progetto.

Innanzitutto, configura l'identificatore pacchetto per iOS.

Con il progetto aperto in Android Studio, fai clic con il tasto destro del mouse sulla cartella iOS, fai clic su Flutter e apri il modulo nell'app Xcode.

942772eb9a73bfaa.png

Nella struttura delle cartelle di Xcode, il progetto Runner è 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, poi fai clic su Firma e Funzionalità. Inserisci l'identificatore pacchetto appena scelto nel campo Team per impostare il team.

812f919d965c649a.jpeg

Ora puoi chiudere Xcode e tornare ad Android Studio per completare la configurazione per Android. Per farlo, apri il file build.gradle in android/app, e modifica applicationId (alla riga 37 nello screenshot di seguito) nell'ID applicazione, che corrisponde all'identificatore pacchetto iOS. Tieni presente che gli ID degli store iOS e Android non devono essere identici, ma mantenerli identici è meno soggetto a errori e pertanto in questo codelab utilizzeremo anche identificatori identici.

5c4733ac560ae8c2.png

3. Installa il plug-in

In questa parte del codelab installerai il plug-in in_app_purchase.

Aggiungi dipendenza in Pubspec

Aggiungi in_app_purchase a pubspec aggiungendo in_app_purchase alle dipendenze nel tuo pubspec:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

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

Fai clic su Pub get per scaricare il pacchetto o esegui flutter pub get dalla 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 prodotti acquistabili lì. Non devi pubblicare nulla o inviare l'app ad Apple per la revisione. Per eseguire questa operazione, devi avere un account sviluppatore. Se non ne hai uno, registrati al programma per sviluppatori Apple.

Per utilizzare gli acquisti in-app, devi inoltre 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, imposte e banche.

6e373780e5e24a6f.png

Qui troverai 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 è nuovo. Assicurati di visualizzare i termini, accettarli e inserire tutte le informazioni richieste.

74c73197472c9aec.png

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.

4a100bbb8cafdbbf.jpeg

Registra ID app

Crea un nuovo identificatore nel Portale per gli sviluppatori Apple.

55d7e592d9a3fc7b.png

Scegli gli ID app

13f125598b72ca77.png

Scegli app

41ac4c13404e2526.png

Fornisci una descrizione e imposta l'ID pacchetto in modo che corrisponda all'ID pacchetto sullo stesso valore impostato in precedenza in XCode.

9d2c940ad80deeef.png

Per ulteriori indicazioni su come creare un nuovo ID app, consulta la Guida dell'Account sviluppatore .

Creazione di una nuova app

Crea una nuova app in App Store Connect con il tuo identificatore pacchetto univoco.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

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, è necessario un utente di test sandbox. Questo utente di test non deve essere connesso a iTunes, viene utilizzato solo per testare gli acquisti in-app. Non puoi usare un indirizzo email già utilizzato per un account Apple. In Utenti e accesso, vai a Tester in Sandbox per creare un nuovo account sandbox o per gestire gli ID Apple sandbox esistenti.

3ca2b26d4e391a4c.jpeg

Puoi configurare l'utente della sandbox sull'iPhone selezionando Impostazioni > App Store > Sandbox.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

Configurare gli acquisti in-app

Ora devi configurare i tre articoli acquistabili:

  • dash_consumable_2k: un acquisto consumabile che può essere acquistato più volte, offrendo all'utente 2000 trattini (la valuta in-app) per acquisto.
  • dash_upgrade_3d: un "upgrade" non fruibile un prodotto che può essere acquistato una sola volta e offre all'utente un trattino per fare clic esteticamente diverso.
  • dash_subscription_doubler: un abbonamento che concede all'utente un numero doppio di trattini per clic per l'intera durata dell'abbonamento.

d156b2f5bac43ca8.png

Vai ad Acquisti in-app > Gestisci.

Crea i tuoi acquisti in-app con gli ID specificati:

  1. Configura dash_consumable_2k come di consumo.

Utilizza dash_consumable_2k come ID prodotto. Il nome di riferimento viene utilizzato solo in App Store Connect; basta impostarlo su dash consumable 2k e aggiungere le localizzazioni per l'acquisto. Chiama l'acquisto Spring is in the air con 2000 dashes fly out come descrizione.

ec1701834fd8527.png

  1. Configura dash_upgrade_3d come Non consumabile.

Utilizza dash_upgrade_3d come ID prodotto. Imposta il nome di riferimento su dash upgrade 3d e aggiungi le tue localizzazioni per l'acquisto. Chiama l'acquisto 3D Dash con Brings your dash back to the future come descrizione.

6765d4b711764c30.png

  1. Configura dash_subscription_doubler come abbonamento con rinnovo automatico.

Il flusso per gli abbonamenti è un po' diverso. Innanzitutto devi impostare il Nome di riferimento e l'ID prodotto:

6d29e08dae26a0c4.png

Il passaggio successivo consiste nel creare un gruppo di abbonamenti. Quando più abbonamenti fanno parte dello stesso gruppo, un utente può iscriversi solo a uno di questi contemporaneamente, ma può facilmente eseguire l'upgrade o il downgrade da un abbonamento all'altro. Chiama questo gruppo subscriptions.

5bd0da17a85ac076.png

Quindi, inserisci la durata dell'abbonamento e le localizzazioni. Assegna a questo abbonamento il nome Jet Engine con la descrizione Doubles your clicks. Fai clic su Save (Salva).

bd1b1d82eeee4cb3.png

Dopo aver fatto clic sul pulsante Salva, aggiungi il prezzo dell'abbonamento. Scegli il prezzo che preferisci.

d0bf39680ef0aa2e.png

A questo punto dovresti vedere i tre acquisti nell'elenco degli acquisti:

99d5c4b446e8fecf.png

5. Configurare il Play Store

Come avviene per l'App Store, devi disporre anche di un account sviluppatore per il Play Store. Se non ne hai ancora uno, registralo.

Creare una nuova app

Crea una nuova app in Google Play Console:

  1. Apri Play Console.
  2. Seleziona Tutte le app > Crea app.
  3. Seleziona una lingua predefinita e aggiungi un titolo per la tua app. Digita il nome dell'app così come vuoi che venga visualizzato su Google Play. Puoi modificare il nome in un secondo momento.
  4. Specifica che la tua applicazione è un gioco. Puoi modificarlo successivamente.
  5. Specifica se la tua applicazione è senza costi o a pagamento.
  6. Aggiungi un indirizzo email che gli utenti del Play Store possono utilizzare per contattarti in merito a questa applicazione.
  7. Completa le dichiarazioni relative alle Linee guida per i contenuti e alle leggi di esportazione degli Stati Uniti.
  8. Seleziona Crea app.

Dopo aver creato l'app, vai alla dashboard e completa tutte le attività riportate nella sezione Configurare l'app. Qui puoi fornire alcune informazioni sulla tua app, ad esempio classificazioni dei contenuti e screenshot. 13845badcf9bc1db.png

Firma la richiesta

Per poter testare gli acquisti in-app, devi avere almeno una build caricata su Google Play.

A questo scopo, è necessario che la build della release sia firmata con qualcosa di diverso dalle chiavi di debug.

Crea un archivio chiavi

Se hai già un archivio chiavi, vai al passaggio successivo. In caso contrario, creane uno eseguendo il comando seguente 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 tua directory home. Se vuoi archiviare il file altrove, modifica l'argomento che passi nel parametro -keystore. Non perdere le

keystore

file privato; non devi inserirlo nel controllo del codice sorgente pubblico.

Fare riferimento all'archivio chiavi dall'app

Crea un file denominato <your app dir>/android/key.properties che contenga un riferimento all'archivio chiavi:

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>

Configura l'accesso in Gradle

Configura la firma dell'app modificando il file <your app dir>/android/app/build.gradle.

Aggiungi le informazioni sull'archivio chiavi dal file delle proprietà prima del blocco android:

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

   android {
         // omitted
   }

Carica il file key.properties nell'oggetto keystoreProperties.

Aggiungi il seguente codice prima del blocco buildTypes:

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

Configura il blocco signingConfigs nel file build.gradle del tuo modulo con le informazioni di configurazione della firma:

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

Le build di release della tua app verranno firmate automaticamente.

Per maggiori informazioni sulla firma dell'app, vedi Firmare l'app su developer.android.com.

Carica la tua prima build

Dopo aver configurato l'app per la firma, dovresti riuscire a creare l'applicazione eseguendo:

flutter build appbundle

Questo comando genera una build di release per impostazione predefinita e l'output è disponibile all'indirizzo <your app dir>/build/app/outputs/bundle/release/

Dalla dashboard in Google Play Console, vai a Release > Test > Test chiusi e crea una nuova release di test chiuso.

Per questo codelab, dovrai firmare l'app da parte di Google, quindi premi Continua nella sezione Firma dell'app di Google Play per attivarla.

ba98446d9c5c40e0.png

Successivamente, carica l'app bundle app-release.aab generato dal comando build.

Fai clic su Salva e poi su Controllo della release.

Infine, fai clic su Avvia implementazione per test interni per attivare la release di test interno.

Configurare gli utenti di test

Per poter testare gli acquisti in-app, gli Account Google dei tester devono essere aggiunti in Google Play Console in due posizioni:

  1. Al canale di test specifico (test interni)
  2. Come tester delle licenze

Innanzitutto, aggiungi il tester al canale di test interno. Torna a Release > Test > Test interni e fai clic sulla scheda Tester.

a0d0394e85128f84.png

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 accedere agli acquisti in-app di prova.

Seleziona la casella di controllo dell'elenco e fai clic su Salva modifiche.

Quindi, aggiungi i tester delle licenze:

  1. Torna alla visualizzazione Tutte le app di Google Play Console.
  2. Vai a Impostazioni > Test delle licenze.
  3. Aggiungi gli stessi indirizzi email dei tester che devono poter testare gli acquisti in-app.
  4. Imposta Risposta licenza su RESPOND_NORMALLY.
  5. Fai clic su Salva modifiche.

a1a0f9d3e55ea8da.png

Configurare gli acquisti in-app

Ora devi configurare gli elementi acquistabili all'interno dell'app.

Come nell'App Store, devi definire tre diversi acquisti:

  • dash_consumable_2k: un acquisto consumabile che può essere acquistato più volte, offrendo all'utente 2000 trattini (la valuta in-app) per acquisto.
  • dash_upgrade_3d: un "upgrade" non fruibile un acquisto che può essere acquistato una sola volta, offrendo all'utente un trattino per fare clic esteticamente diverso.
  • dash_subscription_doubler: un abbonamento che concede all'utente un numero doppio di trattini per clic per l'intera durata dell'abbonamento.

Per prima cosa, aggiungi il materiale di consumo e quello non consumabile.

  1. Vai a Google Play Console e seleziona la tua applicazione.
  2. Vai a Monetizza > Prodotti > Prodotti in-app.
  3. Fai clic su Crea prodottoc8d66e32f57dee21.png.
  4. Inserisci tutte le informazioni richieste per il prodotto. Assicurati che l'ID prodotto corrisponda esattamente all'ID che intendi utilizzare.
  5. Fai clic su Salva.
  6. Fai clic su Attiva.
  7. Ripeti la procedura per l'"upgrade" non fruibile acquisto.

A questo punto, aggiungi la sottoscrizione:

  1. Vai a Google Play Console e seleziona la tua applicazione.
  2. Vai a Monetizza > Prodotti > Abbonamenti.
  3. Fai clic su Crea abbonamento32a6a9eefdb71dd0.png.
  4. Inserisci tutte le informazioni richieste per l'abbonamento. Assicurati che l'ID prodotto corrisponda esattamente all'ID che intendi utilizzare.
  5. Fai clic su Salva.

Ora 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 utenti acquisti.

L'utilizzo di un servizio di backend presenta diversi vantaggi:

  • Puoi verificare le transazioni in modo sicuro.
  • Puoi reagire agli eventi di fatturazione dagli store.
  • Puoi tenere traccia degli acquisti in un database.
  • Gli utenti non potranno indurre con l'inganno la tua app a fornire funzionalità premium riavvolgendo l'orologio di sistema.

Anche se ci sono molti modi per configurare un servizio di backend, puoi farlo utilizzando Cloud Functions e Firestore, utilizzando Firebase di Google.

La scrittura del backend è considerata fuori ambito per questo codelab, quindi il codice iniziale include già un progetto Firebase che gestisce gli acquisti di base per aiutarti a iniziare.

L'app iniziale include anche i plug-in Firebase.

Devi solo 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 è necessaria l'autenticazione. Per farlo, utilizza il modulo di autenticazione di Firebase con Accedi con Google.

  1. Nella dashboard di Firebase, vai ad Autenticazione e attivala, se necessario.
  2. Vai alla scheda Metodo di accesso e attiva il provider di accesso Google.

7babb48832fbef29.png

Poiché utilizzerai anche il database Firestore di Firebase, abilita anche questa.

e20553e0de5ac331.png

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 la configurazione di Flutterfire, seleziona il progetto che hai 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, attiva iOS e Android selezionando le due piattaforme.

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

Quando viene richiesto se eseguire l'override di firebase_options.ARROW, seleziona Sì.

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

Configurare Firebase per Android: ulteriori passaggi

Nella dashboard di Firebase, vai a Panoramica del progetto,scegli Impostazioni e seleziona la scheda Generale.

Scorri verso il basso fino a Le tue app e seleziona l'app dashclicker (android).

b22d46a759c0c834.png

Per consentire l'accesso a Google in modalità di debug, devi fornire l'impronta hash SHA-1 del certificato di debug.

Recuperare l'hash del certificato di firma di debug

Nella directory principale del progetto dell'app Flutter, passa alla cartella android/, quindi genera un report sulla firma.

cd android
./gradlew :app:signingReport

Ti verrà presentato un lungo 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 l'archivio chiavi si trovi nella cartella Home 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.

Configurazione di Firebase per iOS: ulteriori passaggi

Apri ios/Runnder.xcworkspace con Xcode. Oppure con l'IDE che preferisci.

In VSCode fai clic con il tasto destro del mouse sulla cartella ios/, quindi fai clic su open in xcode.

In Android Studio, fai clic con il pulsante destro del mouse sulla cartella ios/, quindi fai clic su flutter e sull'opzione open iOS module in Xcode.

Per consentire l'accesso con Google su iOS, aggiungi l'opzione di configurazione CFBundleURLTypes ai file della build plist. Per ulteriori informazioni, consulta la documentazione relativa al pacchetto google_sign_in. In questo caso, i file sono ios/Runner/Info-Debug.plist e ios/Runner/Info-Release.plist.

La coppia chiave-valore è già stata aggiunta, ma i relativi valori devono essere sostituiti:

  1. Recupera il valore per REVERSED_CLIENT_ID dal file GoogleService-Info.plist, senza l'elemento <string>..</string> circostante.
  2. Sostituisci il valore nei file ios/Runner/Info-Debug.plist e ios/Runner/Info-Release.plist nella chiave 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>

A questo punto, hai terminato la configurazione di Firebase.

7. Ascolta gli aggiornamenti sugli acquisti

In questa parte del codelab preparerai l'app per l'acquisto dei prodotti. Questa procedura include l'ascolto di aggiornamenti ed errori sugli 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 monitora il conteggio attuale dei trattini e li incrementa automaticamente. DashUpgrades gestisce gli upgrade che puoi acquistare con Dashes. Questo codelab è incentrato su DashPurchases.

Per impostazione predefinita, l'oggetto di un provider viene definito quando viene richiesto per la prima volta. Questo oggetto rimane in ascolto degli aggiornamenti sugli 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,
),

È necessaria anche un'istanza di InAppPurchaseConnection. Tuttavia, per fare in modo che l'app possa essere testata, hai bisogno di un modo per simulare la connessione. Per farlo, crea un metodo di istanza di cui sia possibile eseguire l'override 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!;
  }
}

Se vuoi che continui a funzionare, devi aggiornare leggermente il test. Visita la pagina widget_test.dart su GitHub per leggere il codice completo di TestIAPConnection.

test/widget_test.dart

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

In lib/logic/dash_purchases.dart, vai al codice di DashPurchases ChangeNotifier. Al momento, puoi aggiungere solo un DashCounter ai trattini acquistati.

Aggiungi una proprietà di iscrizione allo streaming, _subscription (di tipo StreamSubscription<List<PurchaseDetails>> _subscription;), IAPConnection.instance, e le importazioni. Il codice risultante dovrebbe avere i seguenti aspetti:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

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

  DashPurchases(this.counter);
}

La parola chiave late viene aggiunta a _subscription perché _subscription è inizializzato nel costruttore. Questo progetto è configurato per impostazione predefinita in modo che non sia possibile aggiungere valori null (NNBD), il che significa che le proprietà che non vengono dichiarate come null devono avere un valore diverso da null. Il qualificatore late ti consente di posticipare la definizione di questo valore.

Nel costruttore, recupera purchaseUpdatedStream e inizia ad ascoltare lo stream. Con il metodo dispose(), annulla l'iscrizione allo stream.

lib/logic/dash_purchases.dart

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

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

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

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

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

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

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

Ora l'app riceve gli aggiornamenti relativi agli acquisti, quindi nella sezione successiva effettuerai l'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 fittizi attualmente esistenti con prodotti reali acquistabili. Questi prodotti vengono caricati dai negozi, mostrati in un elenco e vengono acquistati quando si tocca il prodotto.

Adapt PurchasableProduct

PurchasableProduct mostra un prodotto fittizio. 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 fittizi e sostituiscili con un elenco vuoto, List<PurchasableProduct> products = [];

Carica acquisti disponibili

Per dare a un utente la possibilità di effettuare un acquisto, carica gli acquisti dallo store. 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 di Firebase precedente, sono previsti storeKeyConsumable, storeKeySubscription, e storeKeyUpgrade. Quando un acquisto previsto non è disponibile, stampa queste informazioni sulla console; potresti anche voler inviare queste informazioni al servizio di backend.

Il metodo await iapConnection.queryProductDetails(ids) restituisce sia gli ID non trovati sia i prodotti acquistabili trovati. Usa productDetails della 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);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

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

Mostra i prodotti acquistabili

Considera il file purchase_page.dart. Il widget PurchasePage mostra _PurchasesLoading, _PurchaseList, o _PurchasesNotAvailable, in base al StoreState. Il widget mostra anche gli acquisti passati dell'utente, che verranno 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(),
    );
  }
}

Dovresti riuscire a vedere i prodotti disponibili sugli store Android e iOS se sono configurati correttamente. Tieni presente che potrebbe essere necessario un po' di tempo prima che gli acquisti siano disponibili dopo aver eseguito l'accesso nelle rispettive console.

ca1a9f97c21e552d.png

Torna a dash_purchases.dart e implementa la funzione per acquistare un prodotto. Devi solo separare i consumabili da quelli non consumabili. I prodotti per l'upgrade e l'abbonamento sono inutilizzabili.

lib/logic/dash_purchases.dart

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

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 di acquisto, aggiorna lo stato del prodotto mostrato nella pagina di acquisto e applica l'acquisto alla logica del contatore. È importante chiamare completePurchase dopo aver effettuato l'acquisto, in modo che il negozio sappia che è 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();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    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 che supporti questa operazione.

In questa sezione, lavora dalla cartella dart-backend/ come root.

Assicurati di aver installato i seguenti utensili:

Panoramica del progetto di base

Poiché alcune parti di questo progetto sono considerate fuori dall'ambito di questo codelab, sono incluse nel codice iniziale. È una buona idea esaminare ciò che è già presente nel codice di base prima di iniziare, per avere un'idea di come strutturare le cose.

Questo codice di backend può essere eseguito localmente sulla tua macchina, quindi non è necessario eseguirne il deployment per utilizzarlo. Tuttavia, devi poterti connettere dal dispositivo di sviluppo (Android o iPhone) al computer su cui verrà eseguito il server. Per farlo, devono essere sulla stessa rete e devi conoscere l'indirizzo IP della tua macchina.

Prova a eseguire il server utilizzando questo comando:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Il backend Dart utilizza shelf e shelf_router per gestire gli endpoint API. Per impostazione predefinita, il server non fornisce alcuna route. In seguito creerai un percorso per gestire la procedura di verifica dell'acquisto.

Una parte già inclusa nel codice iniziale è IapRepository in lib/iap_repository.dart. Poiché l'apprendimento su come interagire con Firestore o con i database in generale non è considerato pertinente per questo codelab, il codice iniziale contiene funzioni per creare o aggiornare gli acquisti in Firestore, nonché tutte le classi per questi acquisti.

Configura l'accesso a Firebase

Per accedere a Firebase Firestore, è necessaria 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.

27590fc77ae94ad4.png

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.

  1. Vai a Google Play Console e inizia dalla pagina Tutte le app.
  2. Vai a Configurazione > Accesso API. 317fdfb54921f50e.png Nel caso in cui Google Play Console ti chieda di creare un progetto o di collegarlo a un progetto esistente, devi prima farlo e poi tornare in questa pagina.
  3. Individua la sezione in cui puoi definire gli account di servizio e fai clic su Crea nuovo account di servizio.1e70d3f8d794bebb.png
  4. Fai clic sul link Piattaforma Google Cloud nella finestra di dialogo visualizzata. 7c9536336dd9e9b4.png
  5. Seleziona il progetto. Se non lo vedi, assicurati di aver eseguito l'accesso all'Account Google corretto nell'elenco a discesa Account in alto a destra. 3fb3a25bad803063.png
  6. Dopo aver selezionato il progetto, fai clic su + Crea account di servizio nella barra dei menu in alto. 62fe4c3f8644acd8.png
  7. Specifica un nome per l'account di servizio e, facoltativamente, fornisci una descrizione in modo da ricordare a cosa serve e vai al passaggio successivo. 8a92d5d6a3dff48c.png
  8. Assegna all'account di servizio il ruolo Editor. 6052b7753667ed1a.png
  9. Completa la procedura guidata, torna alla pagina Accesso API all'interno della console per gli sviluppatori e fai clic su Aggiorna account di servizio. Il nuovo account dovrebbe essere visualizzato nell'elenco. 5895a7db8b4c7659.png
  10. Fai clic su Concedi l'accesso per il tuo nuovo account di servizio.
  11. Nella pagina successiva, scorri verso il basso, fino al blocco Dati finanziari. Seleziona Visualizzazione di dati finanziari, ordini e risposte del sondaggio sull'annullamento e Gestisci ordini e abbonamenti. 75b22d0201cf67e.png
  12. Fai clic su Invita utente. 70ea0b1288c62a59.png
  13. Ora che l'account è configurato, devi solo generare alcune credenziali. Torna nella console Cloud, individua il tuo account di servizio nell'elenco degli account di servizio, fai clic sui tre puntini verticali e scegli Gestisci chiavi. 853ee186b0e9954e.png
  14. Crea una nuova chiave JSON e scaricala. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Rinomina il file scaricato in service-account-google-play.json, e spostalo nella directory assets/.

Un'altra cosa che dobbiamo fare è aprire lib/constants.dart, e sostituire il valore di androidPackageId con l'ID pacchetto che hai scelto per la tua app per Android.

Configurare l'accesso all'Apple App Store

Per accedere all'App Store per la verifica degli acquisti, devi impostare un secret condiviso:

  1. Apri App Store Connect.
  2. Vai a Le mie app e seleziona la tua app.
  3. Nella barra di navigazione laterale, vai su Acquisti in-app > Gestisci.
  4. In alto a destra nell'elenco, fai clic su Segreto condiviso specifico per l'app.
  5. Genera un nuovo secret e copialo.
  6. Apri lib/constants.dart, e sostituisci il valore di appStoreSharedSecret con il secret condiviso che hai appena generato.

d8b8042470aaeff.png

b72f4565750e2f40.png

File di configurazione costanti

Prima di procedere, assicurati che nel file lib/constants.dart siano configurate le seguenti costanti:

  • androidPackageId: ID pacchetto utilizzato su Android. ad es. com.example.dashclicker
  • appStoreSharedSecret: secret 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 le altre costanti.

10. Verifica 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 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 archiviare l'acquisto e rispondere all'applicazione se quest'ultimo è valido o meno.

Se il servizio di backend esegue la convalida con gli archivi anziché con l'applicazione in esecuzione sul dispositivo dell'utente, puoi impedire all'utente di ottenere l'accesso alle funzionalità premium, ad esempio riavvolgendo l'orologio di sistema.

Configurare il lato Flutter

Configurare l'autenticazione

Poiché intendi inviare gli acquisti al tuo servizio di backend, devi assicurarti che l'utente sia autenticato mentre effettua un acquisto. La maggior parte della logica di autenticazione è già stata aggiunta al progetto iniziale; devi solo assicurarti che PurchasePage mostri il pulsante di accesso quando l'utente non ha ancora eseguito l'accesso. Aggiungi il seguente codice all'inizio del metodo di build 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({Key? key}) : super(key: key);

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

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

Endpoint di verifica delle chiamate dall'app

Nell'app, crea la funzione _verifyPurchase(PurchaseDetails purchaseDetails) che chiama l'endpoint /verifypurchase sul backend Dart utilizzando una chiamata post HTTP.

Invia lo store selezionato (google_play per il Play Store o app_store per l'App Store), serverVerificationData e productID. Il server restituisce un codice di stato che indica se l'acquisto è stato verificato.

Nelle costanti dell'app, configura l'IP del server in base all'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 è 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) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

Chiama la funzione _verifyPurchase in _handlePurchase poco prima di applicare l'acquisto. Dovresti applicare l'acquisto solo dopo averlo verificato. In un'app di produzione, puoi specificarlo ulteriormente per, ad esempio, applicare un abbonamento di prova quando lo store non è temporaneamente disponibile. Tuttavia, per questo esempio, devi semplificare le cose e applicare l'acquisto solo quando l'acquisto è stato verificato.

lib/logic/dash_purchases.dart

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

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

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

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

Nell'app ora è tutto pronto per convalidare gli acquisti.

Configura il servizio di backend

Poi, configura la funzione Cloud Functions per la verifica degli acquisti sul backend.

Creare gestori degli acquisti

Poiché il flusso di verifica per entrambi i negozi è quasi identico, configura una classe PurchaseHandler astratta con implementazioni separate per ciascun negozio.

be50c207c5a2a519.png

Per iniziare, aggiungi un file purchase_handler.dart alla cartella lib/, dove definisci una classe PurchaseHandler astratta con due metodi astratti per verificare 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, ciascun metodo richiede tre parametri:

  • userId: L'ID dell'utente che ha eseguito l'accesso, in modo da collegare gli acquisti all'utente.
  • productData: Dati sul prodotto. Lo definirai in un minuto.
  • token: Il token fornito all'utente dallo store.

Inoltre, per semplificare l'uso di questi gestori degli acquisti, aggiungi un metodo verifyPurchase() che possa essere utilizzato sia per gli abbonamenti sia 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 in entrambi i casi, ma avere implementazioni distinte.

La classe ProductData contiene informazioni di base sui diversi prodotti acquistabili, che includono 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 un non abbonamento.

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 segnaposto per il Google Play Store e l'Apple App Store. Inizia con Google Play:

Crea lib/google_play_purchase_handler.dart e aggiungi un corso che espanda la 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 ora, restituisce true per i metodi del gestore; le troverai in un secondo momento.

Come avrai notato, il costruttore prende un'istanza di IapRepository. Il gestore degli acquisti utilizza questa istanza per archiviare le informazioni sugli acquisti in Firestore in un secondo momento. Per comunicare con Google Play, usa il AndroidPublisherApi fornito.

Ripeti la stessa operazione per il gestore dello store. Crea lib/app_store_purchase_handler.dart e aggiungi un corso che estende di nuovo le 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;
  }
}

Bene. Ora hai due gestori degli acquisti. Ora creiamo l'endpoint dell'API purchase Verification.

Utilizzare i gestori degli acquisti

Apri bin/server.dart e crea un endpoint API utilizzando shelf_route:

bin/server.dart

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router);
}

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

Il codice riportato sopra sta nel seguente modo:

  1. Definisci un endpoint POST che verrà chiamato dall'app creata in precedenza.
  2. Decodifica il payload JSON ed estrai le informazioni seguenti:
  3. userId: ID utente attualmente connesso
  4. source: negozio utilizzato, app_store o google_play.
  5. productData: ottenuto dalla productDataMap che hai creato in precedenza.
  6. token: contiene i dati di verifica da inviare ai negozi.
  7. Chiamata al metodo verifyPurchase, per GooglePlayPurchaseHandler o AppStorePurchaseHandler, a seconda dell'origine.
  8. Se la verifica è andata a buon fine, il metodo restituisce un Response.ok al client.
  9. 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. Devi caricare le chiavi dell'account di servizio che hai ottenuto nel passaggio precedente e configurare l'accesso ai diversi servizi, incluse l'API Android Publisher e l'API Firebase Firestore. Quindi, crea i due gestori degli acquisti con le diverse dipendenze:

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

Verificare gli acquisti Android: implementare un strumenti per l'acquisto

Continua quindi a implementare il gestore degli acquisti di Google Play.

Google fornisce già pacchetti Dart per interagire con le API necessarie per verificare gli acquisti. Li hai inizializzati nel file server.dart e ora li hai utilizzati nella classe GooglePlayPurchaseHandler.

Implementa il gestore per gli acquisti di tipo non 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 degli abbonamenti in un 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 di acquisto.

lib/google_play_purchase_handler.dart

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

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

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

Ora i tuoi acquisti su Google Play dovrebbero essere verificati e memorizzati nel database.

Successivamente, passa agli acquisti nell'App Store per iOS.

Verificare gli acquisti su iOS: implementa il gestore degli acquisti

Per la verifica degli 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,
    ),
  );

A differenza delle API di Google Play, l'App Store utilizza gli stessi endpoint API sia per gli abbonamenti che per i non abbonamenti. Ciò significa che puoi utilizzare la stessa logica per entrambi i gestori. Uniscili in modo che utilizzino 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) {
      print('Successfully verified purchase');
      final receipts = response.latestReceiptInfo ?? [];
      for (final receipt in receipts) {
        final product = productDataMap[receipt.productId];
        if (product == null) {
          print('Error: Unknown product: ${receipt.productId}');
          continue;
        }
        switch (product.type) {
          case ProductType.nonSubscription:
            await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              status: NonSubscriptionStatus.completed,
            ));
            break;
          case ProductType.subscription:
            await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
              userId: userId,
              productId: receipt.productId ?? '',
              iapSource: IAPSource.appstore,
              orderId: receipt.originalTransactionId ?? '',
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.originalPurchaseDateMs ?? '0')),
              type: product.type,
              expiryDate: DateTime.fromMillisecondsSinceEpoch(
                  int.parse(receipt.expiresDateMs ?? '0')),
              status: SubscriptionStatus.active,
            ));
            break;
        }
      }
      return true;
    } else {
      print('Error: Status: ${response.status}');
      return false;
    }
  }

Ora i tuoi acquisti sull'App Store dovrebbero essere verificati e memorizzati nel database.

Esegui il backend

A questo punto, puoi eseguire dart bin/server.dart per gestire l'endpoint /verifypurchase.

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

11. Monitora gli acquisti

Il metodo consigliato per monitorare i tuoi utenti acquisti è nel servizio di backend. Questo perché il backend può rispondere agli eventi dello store e, di conseguenza, è meno soggetto a incorrere in informazioni obsolete a causa della memorizzazione nella cache, oltre a essere meno suscettibile alla manomissione.

Per prima cosa, configura l'elaborazione degli eventi del negozio sul backend con il backend Dart che stai creando.

Eventi del datastore di elaborazione sul backend

I negozi possono informare il tuo backend di qualsiasi evento di fatturazione che si verifica, ad esempio il rinnovo degli abbonamenti. Puoi elaborare questi eventi nel backend per mantenere aggiornati gli acquisti nel tuo database. In questa sezione, esegui la configurazione 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 quello che definiscono un argomento Cloud Pub/Sub. Si tratta essenzialmente di code di messaggi nelle quali i messaggi possono essere pubblicati e da cui possono essere consumati.

Poiché si tratta di una funzionalità specifica di Google Play, puoi includerla in GooglePlayPurchaseHandler.

Per prima cosa, apri lib/google_play_purchase_handler.dart e aggiungi l'importazione PubsubApi:

lib/google_play_purchase_handler.dart

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

Quindi, passa PubsubApi a GooglePlayPurchaseHandler e modifica il costruttore della classe per creare 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();
    });
  }

Il Timer è configurato in modo da chiamare il metodo _pullMessageFromSubSub ogni dieci secondi. Puoi regolare la durata in base alle tue preferenze.

Poi, crea l'_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,
    );
  }

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 abbonamenti che non, chiamando l'elemento handleSubscription o handleNonSubscription esistente, se necessario.

Ogni messaggio deve essere confermato con il metodo _askMessage.

Poi, 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 configurarlo.

Innanzitutto, crea un argomento Pub/Sub:

  1. Visita la pagina Cloud Pub/Sub nella console Google Cloud.
  2. Assicurati di essere nel tuo progetto Firebase e fai clic su + Crea argomento. d5ebf6897a0a8bf5.png
  3. Assegna un nome al nuovo argomento, identico al valore impostato per GOOGLE_PLAY_PUBSUB_BILLING_TOPIC in constants.ts. In questo caso, assegnagli il nome play_billing. Se scegli qualcos'altro, assicurati di aggiornare constants.ts. Crea l'argomento. 20d690fc543c4212.png
  4. Nell'elenco degli argomenti Pub/Sub, fai clic sui tre puntini verticali per l'argomento appena creato, quindi su Visualizza autorizzazioni. ea03308190609fb.png
  5. Nella barra laterale a destra, scegli Aggiungi entità.
  6. Qui aggiungi google-play-developer-notifications@system.gserviceaccount.com e concedigli il ruolo di Publisher Pub/Sub. 55631ec0549215bc.png
  7. Salva le modifiche alle autorizzazioni.
  8. Copia il Nome argomento dell'argomento appena creato.
  9. Apri di nuovo Play Console e scegli l'app dall'elenco Tutte le app.
  10. Scorri verso il basso e vai a Monetizza > Configurazione della monetizzazione.
  11. Compila l'argomento completo e salva le modifiche. 7e5e875dc6ce5d54.png

Tutti gli eventi di fatturazione di Google Play verranno ora pubblicati nell'argomento.

Elaborare gli eventi di fatturazione dell'App Store

Esegui la stessa operazione per gli eventi di fatturazione dell'App Store. Esistono due modi efficaci per implementare la gestione degli aggiornamenti negli acquisti effettuati sull'App Store. Uno è implementare un webhook che fornisci ad Apple, che utilizza per comunicare con il tuo server. Il secondo modo, che è quello 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 dovresti esporre il tuo server su internet per implementare il webhook.

In un ambiente di produzione, idealmente dovresti avere entrambi. Il webhook per ottenere gli eventi dall'App Store e dall'API server nel caso in cui tu abbia perso un evento o devi verificare lo stato di un abbonamento.

Per prima cosa, 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();
    });
  }

Quindi, 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:

  1. Ottiene l'elenco degli abbonamenti attivi da Firestore utilizzando IapRepository.
  2. Per ogni ordine, lo stato dell'abbonamento viene richiesto all'API App Store Server.
  3. Consente di ottenere l'ultima transazione per l'acquisto dell'abbonamento in questione.
  4. Controlla la data di scadenza.
  5. 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 dello store

Dopodiché, configura l'App Store:

  1. Accedi ad App Store Connect e seleziona Utenti e accesso.
  2. Vai a Tipo di chiave > Acquisto in-app.
  3. Tocca il pulsante "Più". per aggiungerne una nuova.
  4. Scegli un nome, ad esempio "Chiave codelab".
  5. Scarica il file p8 contenente la chiave.
  6. Copialo nella cartella degli asset, con il nome SubscriptionKey.p8.
  7. Copia l'ID chiave dalla chiave appena creata e impostalo sulla costante appStoreKeyId nel file lib/constants.dart.
  8. Copia l'ID emittente a destra nella parte superiore dell'elenco delle chiavi e impostalo sulla costante appStoreIssuerId nel file lib/constants.dart.

9540ea9ada3da151.png

Monitorare gli acquisti sul dispositivo

Il modo più sicuro per monitorare i tuoi acquisti è sul lato server perché il client è difficile da proteggere, ma devi disporre di un modo per recuperarle le informazioni in modo che l'app possa agire in base alle informazioni sullo stato dell'abbonamento. Archiviando gli acquisti in Firestore, puoi sincronizzare facilmente i dati con il client e mantenerli aggiornati automaticamente.

Hai già incluso nell'app IAPRepo, ovvero il repository Firestore che contiene tutti i dati di acquisto dell'utente in List<PastPurchase> purchases. Il repository contiene anche hasActiveSubscription,, che si verifica quando viene effettuato un acquisto con productId storeKeySubscription con uno stato non scaduto. Se 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((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();
    });
  }

Tutta la logica di acquisto è nella classe DashPurchases ed è qui che devono essere applicati o rimossi gli abbonamenti. Aggiungi quindi iapRepo come proprietà nella classe e assegna iapRepo nel costruttore. In seguito, aggiungi direttamente un listener nel costruttore e rimuovi il listener nel metodo dispose(). All'inizio, il listener può essere semplicemente 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() {
    iapRepo.removeListener(purchasesUpdate);
    _subscription.cancel();
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

Quindi, fornisci IAPRepo al costruttore in main.dart.. Puoi ottenere il repository utilizzando context.read perché è già stato creato in Provider.

lib/main.dart

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

Quindi, 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 è necessario controllare se l'abbonamento è già applicato. Quando lo stato dell'abbonamento cambia, aggiorni anche lo stato del prodotto acquistabile in modo da poter mostrare nella pagina di acquisto che è già attivo. Imposta la proprietà _beautifiedDashUpgrade a seconda che venga acquistato o meno l'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();
    }
  }

Hai ora 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 Dash clicker.

12. Operazione completata.

Complimenti!!! Hai completato il codelab. Puoi trovare il codice completato per questo codelab nella android_studio_folder.pngcartella di completamento.

Per scoprire di più, prova gli altri codelab di Flutter.