Aggiungere acquisti in-app all'app Flutter

1. Introduzione

L'aggiunta di acquisti in-app a un'app Flutter richiede la configurazione corretta degli store App Store e Play Store, 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. Aggiungerai le seguenti opzioni di acquisto:

  1. Un'opzione di acquisto ripetibile per 2000 Dash alla volta.
  2. Un acquisto di upgrade una tantum per trasformare il vecchio stile di 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 Dash. Questi sono disponibili direttamente per l'utente e possono essere acquistati più volte. Questo viene chiamato consumabile perché viene consumato direttamente e può essere consumato più volte.

La seconda opzione esegue l'upgrade di Dash a una versione più bella. Deve essere acquistato una sola volta ed è disponibile per sempre. Un acquisto di questo tipo viene definito non di consumo perché non può essere consumato dall'app, ma è valido per sempre.

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

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

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

Cosa creerai

  • Estenderai un'app per supportare gli acquisti di contenuti consumabili e gli abbonamenti.
  • 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 prodotti acquistabili.
  • Come comunicare con i negozi per verificare gli acquisti e memorizzarli in Firestore.
  • Come gestire gli acquisti nella tua app.

Requisiti

  • Android Studio
  • Xcode (per lo sviluppo di app per iOS)
  • SDK Flutter

2. Configura l'ambiente di sviluppo

Per iniziare 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 interfaccia a riga di comando di GitHub, utilizza questo comando:

gh repo clone flutter/codelabs flutter-codelabs

Il codice di esempio viene clonato in una directory flutter-codelabs che contiene il codice per una raccolta di codelab. Il codice per questo codelab si trova in flutter-codelabs/in_app_purchases.

La struttura delle directory in flutter-codelabs/in_app_purchases contiene una serie di snapshot di dove dovresti trovarti alla fine di ogni passaggio denominato. Il codice iniziale si trova nel passaggio 0, quindi vai al passaggio come segue:

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. Abbiamo utilizzato 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 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 chiama identificatore del bundle, mentre per il Play Store di Android si chiama ID applicazione. Questi identificatori vengono di solito creati utilizzando una notazione del nome di dominio inverso. Ad esempio, quando crei un'app di acquisto in-app per flutter.dev, utilizzerai dev.flutter.inapppurchase. Pensa a un identificatore per la tua app, che ora imposterai nelle impostazioni del progetto.

Innanzitutto, configura l'identificatore bundle per iOS. Per farlo, apri il file Runner.xcworkspace nell'app Xcode.

a9fbac80a31e28e0.png

Nella struttura delle cartelle di Xcode, il progetto Runner si trova in alto, mentre i target Flutter, Runner e Products si trovano sotto il progetto Runner. Fai doppio clic su Runner per modificare le impostazioni del progetto e fai clic su Signing & Capabilities (Firma e funzionalità). Inserisci l'identificatore bundle che hai appena scelto nel campo Team per impostare il tuo 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.kts in android/app, e modifica applicationId (riga 24 nello screenshot riportato di seguito) con l'ID applicazione, lo stesso dell'identificatore bundle iOS. Tieni presente che gli ID per gli store iOS e Android non devono essere identici, ma mantenerli identici è meno soggetto a errori, pertanto in questo codelab utilizzeremo anche identificatori identici.

e320a49ff2068ac2.png

3. Installare il plug-in

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

Aggiungere la dipendenza in pubspec

Aggiungi in_app_purchase a pubspec aggiungendo in_app_purchase alle dipendenze del tuo progetto:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Apri pubspec.yaml e verifica che ora in_app_purchase sia elencato come voce in dependencies e in_app_purchase_platform_interface in dev_dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  in_app_purchase_platform_interface: ^1.4.0

4. 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. Non devi pubblicare nulla né 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.

Per utilizzare gli acquisti in-app, devi anche disporre di un contratto attivo per le app a pagamento in App Store Connect. Vai alla pagina https://appstoreconnect.apple.com/ e fai clic su Accordi, tasse e dati bancari.

11db9fca823e7608.png

Qui vedrai i contratti per le app senza costi e a pagamento. Lo stato delle app senza costi deve essere attivo, mentre quello delle app a pagamento è nuovo. Assicurati di visualizzare i termini, accettarli e inserire tutte le informazioni richieste.

74c73197472c9aec.png

Quando tutto è configurato 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 sviluppatori Apple. Visita la pagina developer.apple.com/account/resources/identifiers/list e fai clic sull'icona "Più" accanto all'intestazione Identificatori.

55d7e592d9a3fc7b.png

Scegli ID app

13f125598b72ca77.png

Scegli app

41ac4c13404e2526.png

Fornisci una descrizione e imposta l'ID bundle in modo che corrisponda allo 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.

Creare una nuova app

Crea una nuova app in App Store Connect con il tuo identificatore bundle 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, 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.

2ba0f599bcac9b36.png

Ora puoi configurare l'utente sandbox sul tuo iPhone andando su Impostazioni > Sviluppatore > Account Apple sandbox.

74a545210b282ad8.png eaa67752f2350f74.png

Configurare gli acquisti in-app

Ora configurerai i tre articoli acquistabili:

  • dash_consumable_2k: un acquisto di consumo che può essere effettuato più volte e che concede all'utente 2000 Dash (la valuta in-app) per acquisto.
  • dash_upgrade_3d: Un acquisto "upgrade" non consumabile che può essere acquistato una sola volta e che offre all'utente un Dash cosmetico diverso su cui fare clic.
  • dash_subscription_doubler: un abbonamento che concede all'utente il doppio dei trattini per clic per la durata dell'abbonamento.

a118161fac83815a.png

Vai ad Acquisti in-app.

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

  1. Configura dash_consumable_2k come consumabile. Utilizza dash_consumable_2k come ID prodotto. Il nome di riferimento viene utilizzato solo in App Store Connect, impostalo su dash consumable 2k. 1f8527fc03902099.png Configura la disponibilità. Il prodotto deve essere disponibile nel paese dell'utente sandbox. bd6b2ce2d9314e6e.png Aggiungi i prezzi e imposta il prezzo su $1.99 o l'equivalente in un'altra valuta. 926b03544ae044c4.png Aggiungi le localizzazioni per l'acquisto. Chiama l'acquisto Spring is in the air con 2000 dashes fly out come descrizione. e26dd4f966dcfece.png Aggiungi uno screenshot della recensione. I contenuti non sono importanti a meno che il prodotto non venga inviato per la revisione, ma sono necessari affinché il prodotto si trovi nello stato "Pronto per l'invio", necessario quando l'app recupera i prodotti dall'App Store. 25171bfd6f3a033a.png
  2. Configura dash_upgrade_3d come non consumabile. Utilizza dash_upgrade_3d come ID prodotto. Imposta il nome di riferimento su dash upgrade 3d. Chiama l'acquisto 3D Dash con Brings 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 prodotto dash_consumable_2k. 83878759f32a7d4a.png
  3. Configura dash_subscription_doubler come abbonamento con rinnovo automatico. Il flusso per gli abbonamenti è leggermente diverso. Innanzitutto, devi creare un gruppo di abbonati. Quando più abbonamenti fanno parte dello stesso gruppo, un utente può abbonarsi a uno solo di questi contemporaneamente, ma può eseguire l'upgrade o il downgrade tra questi abbonamenti. Chiama questo gruppo subscriptions. 393a44b09f3cd8bf.png e aggiungi la localizzazione per il gruppo di abbonamenti. 595aa910776349bd.png A questo punto, crea l'abbonamento. Imposta il nome di riferimento su dash subscription doubler e l'ID prodotto su dash_subscription_doubler. 7bfff7bbe11c8eec.png Dopodiché, seleziona la durata dell'abbonamento di 1 settimana e le localizzazioni. Assegna a questo abbonamento il nome Jet Engine con la descrizione Doubles 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 prodotto dash_consumable_2k. 44d18e02b926a334.png

Ora dovresti vedere i prodotti negli elenchi:

17f242b5c1426b79.png d71da951f595054a.png

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:

  1. Apri Play Console.
  2. Seleziona Tutte le app > Crea app.
  3. 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.
  4. Specifica che la tua applicazione è un gioco. Puoi modificarle successivamente.
  5. Specifica se la tua applicazione è senza costi o a pagamento.
  6. Completa le dichiarazioni relative alle linee guida per i contenuti e alle leggi di esportazione degli Stati Uniti.
  7. Seleziona Crea app.

Dopo aver creato l'app, vai alla dashboard e completa tutte le attività nella sezione Configura la tua app. Qui fornisci alcune informazioni sulla tua app, come le classificazioni dei contenuti e gli screenshot. 13845badcf9bc1db.png

Firmare la richiesta

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

Per questo, devi firmare la build di rilascio con un elemento diverso dalle chiavi di debug.

Creare un keystore

Se disponi di un keystore esistente, vai al passaggio successivo. In caso contrario, creane uno eseguendo il seguente comando 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 questo 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 home directory. Se vuoi archiviare il file altrove, modifica l'argomento che passi al parametro -keystore. Mantieni il

keystore

file privato; non eseguirne il check-in nel controllo del codice sorgente pubblico.

Fai riferimento all'archivio chiavi dall'app

Crea un file denominato <your app dir>/android/key.properties che contenga un riferimento al 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 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 a:

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

Ora le build di rilascio della tua app verranno firmate automaticamente.

Per ulteriori informazioni sulla firma dell'app, consulta l'articolo Firma dell'app su developer.android.com.

Caricare la prima build

Dopo aver configurato la tua app per la firma, dovresti essere in grado di creare l'applicazione eseguendo:

flutter build appbundle

Questo comando genera una build di release per impostazione predefinita e l'output si trova in <your app dir>/build/app/outputs/bundle/release/

Dalla dashboard di Google Play Console, vai a Testa e rilascia > Test > Test chiusi e crea una nuova release di test chiusi.

A questo punto, carica l'app bundle app-release.aab generato dal comando di build.

Fai clic su Salva e poi su Controlla release.

Infine, fai clic su Avvia implementazione per il test chiuso per attivare la release di test chiuso.

Configurare gli utenti di test

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

  1. Al canale di test specifico (test interno)
  2. In qualità di tester delle licenze

Per prima cosa, aggiungi il tester al canale di test interno. Torna a Testa e rilascia > 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 ai test degli acquisti in-app.

Poi, seleziona la casella di controllo per l'elenco e fai clic su Salva modifiche.

Poi aggiungi i tester delle licenze:

  1. Torna alla visualizzazione Tutte le app di Google Play Console.
  2. Vai a Impostazioni > Test licenza.
  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 configurerai gli articoli acquistabili all'interno dell'app.

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

  • dash_consumable_2k: un acquisto di consumo che può essere effettuato più volte e che concede all'utente 2000 Dash (la valuta in-app) per acquisto.
  • dash_upgrade_3d: un acquisto "upgrade" non consumabile che può essere acquistato una sola volta e che offre all'utente un Dash con un aspetto diverso su cui fare clic.
  • dash_subscription_doubler: un abbonamento che concede all'utente il doppio dei trattini per clic per la durata dell'abbonamento.

Innanzitutto, aggiungi il prodotto di consumo e non di consumo.

  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 tuo 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'acquisto "upgrade" non consumabile.

Poi aggiungi l'abbonamento:

  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.

Gli acquisti dovrebbero ora 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 le transazioni in modo sicuro.
  • Puoi reagire agli eventi di fatturazione degli store.
  • Puoi tenere traccia degli acquisti in un database.
  • Gli utenti non potranno ingannare la tua app per ottenere funzionalità premium riavvolgendo l'orologio di sistema.

Esistono molti modi per configurare un servizio di backend, ma lo farai utilizzando le funzioni cloud e Firestore, utilizzando Firebase di Google.

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

I plug-in Firebase sono inclusi anche nell'app iniziale.

Non ti resta che creare il tuo progetto Firebase, configurare l'app e il backend per Firebase e infine eseguire il deployment del backend.

Crea un progetto Firebase

Vai alla console Firebase e crea un nuovo progetto Firebase. Per questo esempio, chiamalo Dash Clicker.

Nell'app di backend, colleghi gli acquisti a un utente specifico, pertanto è necessaria l'autenticazione. Per farlo, utilizza il modulo di autenticazione di Firebase con l'accesso con Google.

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

fe2e0933d6810888.png

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

d02d641821c71e2c.png

Imposta le regole Cloud Firestore nel seguente modo:

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

Configura Firebase per Android: passaggi successivi

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

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

b22d46a759c0c834.png

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

Ottenere l'hash del certificato di firma di debug

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

cd android
./gradlew :app:signingReport

Verrà visualizzato un lungo elenco di chiavi di firma. Poiché stai cercando l'hash del certificato di debug, cerca il certificato con le proprietà Variant e Config impostate su debug. È probabile che il keystore si trovi nella cartella principale 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. oppure con 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 sull'opzione open iOS module in Xcode.

Per consentire l'accesso con Google su iOS, aggiungi l'opzione di configurazione CFBundleURLTypes ai file plist della build. Per ulteriori informazioni, consulta la documentazione del pacchetto google_sign_in. In questo caso, il file è ios/Runner/Info.plist.

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

  1. Ottieni il valore di REVERSED_CLIENT_ID dal file GoogleService-Info.plist, senza l'elemento <string>..</string> che lo circonda.
  2. Sostituisci il valore nel file ios/Runner/Info.plist con la 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>

La configurazione di Firebase è terminata.

7. Ascoltare gli aggiornamenti sugli acquisti

In questa parte del codelab preparerai l'app per l'acquisto dei prodotti. Questo processo include l'ascolto degli aggiornamenti e degli errori di acquisto dopo l'avvio dell'app.

Ascoltare gli aggiornamenti sugli acquisti

In main.dart, trova il widget MyHomePage con un Scaffold con un BottomNavigationBar contenente due pagine. Questa pagina crea anche tre Provider per DashCounter, DashUpgrades, e DashPurchases. DashCounter tiene traccia del conteggio attuale dei trattini e li incrementa automaticamente. DashUpgrades gestisce gli upgrade che puoi acquistare con i Dash. Questo codelab si concentra su DashPurchases.

Per impostazione predefinita, l'oggetto di un fornitore viene definito quando viene richiesto per la prima volta. Questo oggetto ascolta gli aggiornamenti degli acquisti direttamente all'avvio dell'app, quindi disattiva il caricamento differito su questo oggetto con lazy: false:

lib/main.dart

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

Devi anche avere un'istanza di InAppPurchaseConnection. Tuttavia, per mantenere l'app testabile, è necessario un modo per simulare la connessione. Per farlo, crea un metodo di istanza che possa 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!;
  }
}

Aggiorna il test come segue:

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 per DashPurchasesChangeNotifier. A questo punto, 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 l'aspetto seguente:

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 annullabile per impostazione predefinita (NNBD), il che significa che le proprietà non dichiarate annullabili devono avere un valore non nullo. Il qualificatore late ti consente di ritardare la definizione di questo valore.

Nel costruttore, ottieni lo stream purchaseUpdated e inizia ad ascoltarlo. Nel metodo dispose(), annulla l'abbonamento allo stream.

lib/logic/dash_purchases.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  StoreState storeState = StoreState.notAvailable;         // Modify this line
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [
    PurchasableProduct(
      'Spring is in the air',
      'Many dashes flying out from their nests',
      '\$0.99',
    ),
    PurchasableProduct(
      'Jet engine',
      'Doubles you clicks per second for a day',
      '\$1.99',
    ),
  ];

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

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

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

  Future<void> buy(PurchasableProduct product) async {
    product.status = ProductStatus.pending;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchased;
    notifyListeners();
    await Future<void>.delayed(const Duration(seconds: 5));
    product.status = ProductStatus.purchasable;
    notifyListeners();
  }
                                                           // Add from here
  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

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

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

Ora l'app riceve gli aggiornamenti degli acquisti, quindi nella sezione successiva 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 dagli store, visualizzati in un elenco e acquistati quando tocchi il prodotto.

Adatta 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 gli acquisti disponibili

Per consentire a un utente di effettuare un acquisto, carica gli acquisti dallo store. Innanzitutto, controlla se lo store è disponibile. Quando lo store 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. In base alla configurazione precedente di Google Play e dell'App Store, dovresti visualizzare storeKeyConsumable, storeKeySubscription, e storeKeyUpgrade. Quando un acquisto previsto non è disponibile, stampa queste informazioni nella console. Potresti anche voler 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 la UI 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();                                       // Add this line
  }

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

Considera 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 una volta inseriti nelle rispettive console.

ca1a9f97c21e552d.png

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. L'upgrade e i prodotti in 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 in modo che faccia riferimento a questa variabile.

lib/logic/dash_purchases.dart

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

Il metodo _onPurchaseUpdate riceve gli aggiornamenti dell'acquisto, aggiorna lo stato del prodotto visualizzato nella pagina di acquisto e applica l'acquisto alla logica del contatore. È importante chiamare completePurchase dopo aver gestito l'acquisto, in modo che lo store 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. Configurare 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 radice.

Assicurati di aver installato i seguenti strumenti:

Panoramica del progetto di base

Poiché alcune parti di questo progetto sono considerate al di fuori dell'ambito di questo codelab, sono incluse nel codice iniziale. Prima di iniziare, ti consigliamo di esaminare il codice iniziale per farti un'idea di come strutturare il progetto.

Questo codice di backend può essere eseguito localmente sulla tua macchina, non è necessario eseguirne il deployment per utilizzarlo. Tuttavia, devi essere in grado di connetterti dal tuo dispositivo di sviluppo (Android o iPhone) alla macchina in cui verrà eseguito il server. Per farlo, devono trovarsi nella 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 pubblicare gli endpoint API. Per impostazione predefinita, il server non fornisce route. In un secondo momento creerai una route per gestire la procedura di verifica dell'acquisto.

Una parte già inclusa nel codice iniziale è IapRepository in lib/iap_repository.dart. Poiché imparare a interagire con Firestore o con i database in generale non è considerato pertinente a questo codelab, il codice iniziale 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 al service account. Generane uno aprendo le impostazioni del progetto Firebase e vai alla sezione Service account, poi 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 service account con queste autorizzazioni e scaricare le credenziali JSON.

  1. Visita la pagina dell'API Google Play Android Developer in Google Cloud Console. 629f0bd8e6b50be8.png Se Google Play Console ti chiede di creare un progetto o di collegarti a uno esistente, fallo prima di tornare a questa pagina.
  2. Poi, vai alla pagina Service account e fai clic su + Crea service account. 8dc97e3b1262328a.png
  3. Inserisci il Nome service account e fai clic su Crea e continua. 4fe8106af85ce75f.png
  4. Seleziona il ruolo Sottoscrittore Pub/Sub e fai clic su Fine. a5b6fa6ea8ee22d.png
  5. Una volta creato l'account, vai a Gestisci chiavi. eb36da2c1ad6dd06.png
  6. Seleziona Aggiungi chiave > Crea nuova chiave. e92db9557a28a479.png
  7. Crea e scarica una chiave JSON. 711d04f2f4176333.png
  8. Rinomina il file scaricato in service-account-google-play.json, e spostalo nella directory assets/.
  9. Poi, vai alla pagina Utenti e autorizzazioni in Play Console28fffbfc35b45f97.png
  10. Fai clic su Invita nuovi utenti e inserisci l'indirizzo email del service account creato in precedenza. Puoi trovare l'email nella tabella della pagina Account di servizioe3310cc077f397d.png
  11. Concedi le autorizzazioni Visualizzazione di dati finanziari e Gestione di ordini e abbonamenti per l'applicazione. a3b8cf2b660d1900.png
  12. Fai clic su Invita utente.

Un'altra cosa da 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 configurare un segreto condiviso:

  1. Apri App Store Connect.
  2. Vai a Le mie app e seleziona la tua app.
  3. Nella navigazione della barra laterale, vai a Generale > Informazioni sull'app.
  4. Fai clic su Gestisci sotto l'intestazione Segreto condiviso specifico per l'app. ad419782c5fbacb2.png
  5. Genera un nuovo secret e copialo. b5b72a357459b0e5.png
  6. Apri lib/constants.dart, e sostituisci il valore di appStoreSharedSecret con il segreto 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 esempio com.example.dashclicker
  • appStoreSharedSecret: segreto condiviso per accedere ad App Store Connect ed eseguire la verifica degli acquisti.
  • bundleId: ID bundle utilizzato su iOS, ad esempio com.example.dashclicker

Per il momento, puoi ignorare le altre costanti.

10. Verificare gli acquisti

Il flusso generale per la verifica degli acquisti è simile per iOS e Android.

Per entrambi gli store, 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 indicando se l'acquisto è valido o meno.

Se il servizio di backend esegue la convalida con gli store anziché l'applicazione in esecuzione sul dispositivo dell'utente, puoi impedire all'utente di accedere alle funzionalità premium, ad esempio riavvolgendo 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 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 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

class PurchasePage extends StatelessWidget {
  const PurchasePage({super.key});

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

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

    // ...

Chiamare l'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 http post.

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

Nelle costanti dell'app, configura l'IP del server con l'indirizzo IP della tua macchina locale.

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart';                           // And this one

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

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

  final iapConnection = IAPConnection.instance;

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

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

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

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. Devi applicare l'acquisto solo quando è verificato. In un'app di produzione, puoi specificare ulteriormente questa impostazione, ad esempio per applicare un abbonamento di prova quando lo store non è temporaneamente disponibile. Tuttavia, per questo esempio, applica l'acquisto 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(2000);
          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 gli store è quasi identico, configura una classe astratta PurchaseHandler con implementazioni separate per ogni store.

be50c207c5a2a519.png

Inizia aggiungendo un file purchase_handler.dart alla cartella lib/, in cui definisci una classe astratta PurchaseHandler 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, ogni metodo richiede tre parametri:

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

Inoltre, per semplificare l'utilizzo di questi gestori degli acquisti, aggiungi un metodo verifyPurchase() che può essere utilizzato sia per gli abbonamenti che per gli acquisti non in abbonamento:

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 chiamare verifyPurchase in 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);
}

L'ProductType può essere un abbonamento o un abbonamento non sottoscritto.

lib/products.dart

enum ProductType { subscription, nonSubscription }

Infine, l'elenco dei prodotti viene 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 dei segnaposto per il Google Play Store e l'Apple App Store. Inizia da 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 ora, restituisce true per i metodi del gestore, che vedremo in un secondo momento.

Come avrai notato, il costruttore accetta 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, utilizza AndroidPublisherApi fornito.

Dopodiché, fai lo stesso per il gestore dello store. Crea lib/app_store_purchase_handler.dart e aggiungi di nuovo una classe che estende 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. Successivamente, crea l'endpoint API di verifica degli acquisti.

Utilizzare i gestori degli acquisti

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

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router.call);
}

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

Il codice esegue le seguenti operazioni:

  1. Definisci un endpoint POST che verrà chiamato dall'app che hai creato in precedenza.
  2. Decodifica il payload JSON ed estrai le seguenti informazioni:
    1. userId: ID utente che ha eseguito l'accesso
    2. source: negozio utilizzato, app_store o google_play.
    3. productData: ottenuto dal productDataMap che hai creato in precedenza.
    4. token: contiene i dati di verifica da inviare agli store.
  3. Chiamata al metodo verifyPurchase, per GooglePlayPurchaseHandler o AppStorePurchaseHandler, a seconda dell'origine.
  4. Se la verifica ha esito positivo, il metodo restituisce un Response.ok al client.
  5. 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 del service account che hai ottenuto 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,
    ),
  };
}

Verifica gli acquisti su Android: implementa il gestore degli acquisti

Dopodiché, continua a implementare il gestore degli acquisti 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 utilizzi nella classe GooglePlayPurchaseHandler.

Implementa il gestore per gli acquisti non di tipo abbonamento:

lib/google_play_purchase_handler.dart

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

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

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

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

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

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

Puoi aggiornare il gestore degli acquisti di abbonamenti 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 don't know the user ID, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

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

I tuoi acquisti su Google Play dovrebbero ora essere verificati e archiviati nel database.

Dopodiché, passa agli acquisti sull'App Store per iOS.

Verifica degli 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.

Inizia creando l'istanza ITunesApi. Utilizza la configurazione della sandbox e attiva la registrazione 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 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 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 {

    // See next step
  }

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

Gli acquisti sull'App Store ora dovrebbero essere 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 dello store ed è quindi meno soggetto a informazioni obsolete a causa della memorizzazione nella cache, oltre a essere meno suscettibile a manomissioni.

Innanzitutto, configura l'elaborazione degli eventi del negozio nel backend con il backend Dart che hai creato.

Elaborare gli eventi negozio nel backend

Gli store hanno la possibilità di comunicare al backend eventuali eventi di fatturazione, ad esempio il rinnovo degli abbonamenti. Puoi elaborare questi eventi nel backend per mantenere aggiornati gli acquisti nel tuo database. In questa sezione, configura l'impostazione 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 chiama argomento Cloud Pub/Sub. Si tratta essenzialmente di code di messaggi in cui i messaggi possono essere pubblicati e da cui possono essere consumati.

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

Inizia aprendo lib/google_play_purchase_handler.dart e aggiungendo 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 un Timer nel seguente modo:

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 10 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/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

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

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

Il codice che hai appena aggiunto comunica con l'argomento Pub/Sub di Google Cloud ogni 10 secondi e chiede 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 in abbonamento che non, chiamando le handleSubscription o handleNonSubscription esistenti, se necessario.

Ogni messaggio deve essere riconosciuto con il metodo _askMessage.

Poi, aggiungi le dipendenze richieste al file server.dart. Aggiungi PubsubApi.cloudPlatformScope alla configurazione delle credenziali:

bin/server.dart

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

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

Poi, 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,                                           // Add this line
    ),
    '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. Imposta il valore di googleCloudProjectId in constants.dart sull'ID del tuo progetto Google Cloud.
  2. Visita la pagina Cloud Pub/Sub nella console Google Cloud.
  3. Assicurati di essere nel progetto Firebase e fai clic su + Crea argomento. d5ebf6897a0a8bf5.png
  4. Assegna al nuovo argomento un nome identico al valore impostato per googlePlayPubsubBillingTopic in constants.dart. In questo caso, chiamalo play_billing. Se scegli un'altra opzione, assicurati di aggiornare constants.dart. Crea l'argomento. 20d690fc543c4212.png
  5. Nell'elenco degli argomenti Pub/Sub, fai clic sui tre puntini verticali per l'argomento che hai appena creato e poi su Visualizza autorizzazioni. ea03308190609fb.png
  6. Nella barra laterale a destra, scegli Aggiungi entità.
  7. Qui, aggiungi google-play-developer-notifications@system.gserviceaccount.com e concedigli il ruolo di Publisher Pub/Sub. 55631ec0549215bc.png
  8. Salva le modifiche alle autorizzazioni.
  9. Copia il nome dell'argomento che hai appena creato.
  10. Apri di nuovo Play Console e scegli la tua app dall'elenco Tutte le app.
  11. Scorri verso il basso e vai a Monetizzazione > Configurazione della monetizzazione.
  12. 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

Dopodiché, fai lo stesso per gli eventi di fatturazione dell'App Store. Esistono due modi efficaci per implementare la gestione degli aggiornamenti negli acquisti per l'App Store. Uno è l'implementazione di un webhook che fornisci ad Apple e che viene utilizzato per comunicare con il tuo server. Il secondo modo, che è quello che troverai in questo codelab, consiste nel connettersi 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 a internet per implementare il webhook.

In un ambiente di produzione, l'ideale sarebbe avere entrambi. Il webhook per ottenere eventi dall'App Store e l'API Server nel caso in cui tu abbia perso un evento o debba ricontrollare lo stato di un abbonamento.

Inizia aprendo lib/app_store_purchase_handler.dart e aggiungendo la dipendenza AppStoreServerAPI:

lib/app_store_purchase_handler.dart

  final AppStoreServerAPI appStoreServerAPI;                 // Add this member

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

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 di questo 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 nel seguente modo:

lib/app_store_purchase_handler.dart

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where(
      (element) =>
          element.type == ProductType.subscription &&
          element.iapSource == IAPSource.appstore,
    );
    for (final purchase in appStoreSubscriptions) {
      final status = await appStoreServerAPI.getAllSubscriptionStatuses(
        purchase.orderId,
      );
      // Obtain all subscriptions for the order ID.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
            transaction.transactionInfo.expiresDate ?? 0,
          );
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(
            SubscriptionPurchase(
              userId: null,
              productId: transaction.transactionInfo.productId,
              iapSource: IAPSource.appstore,
              orderId: transaction.originalTransactionId,
              purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate,
              ),
              type: ProductType.subscription,
              expiryDate: expirationDate,
              status: isExpired
                  ? SubscriptionStatus.expired
                  : SubscriptionStatus.active,
            ),
          );
        }
      }
    }
  }

Questo metodo funziona nel seguente modo:

  1. Recupera l'elenco degli abbonamenti attivi da Firestore utilizzando IapRepository.
  2. Per ogni ordine, richiede lo stato dell'abbonamento all'API Server App Store.
  3. Ottiene l'ultima transazione per l'acquisto dell'abbonamento.
  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 Server App Store:

bin/server.dart

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


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

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

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

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

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

Configurazione dell'App Store

Dopodiché, configura l'App Store:

  1. Accedi ad App Store Connect e seleziona Utenti e accesso.
  2. Vai ad Integrazioni > Chiavi > Acquisto in-app.
  3. Tocca l'icona "Più" per aggiungerne uno nuovo.
  4. Assegna un nome, ad esempio "Chiave del 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 in cima all'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 gli acquisti è lato server, perché il client è difficile da proteggere, ma devi avere un modo per restituire le informazioni al client in modo che l'app possa agire in base alle informazioni sullo stato dell'abbonamento. Se memorizzi gli acquisti in Firestore, puoi sincronizzare i dati con il client e mantenerli aggiornati automaticamente.

Hai già incluso IAPRepo nell'app, ovvero il repository Firestore che contiene tutti i dati di acquisto dell'utente in List<PastPurchase> purchases. Il repository contiene anche hasActiveSubscription,, che è true quando è presente 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((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, in cui devono essere applicati o rimossi gli abbonamenti. Pertanto, aggiungi iapRepo come proprietà nella classe e assegna iapRepo nel costruttore. Successivamente, aggiungi direttamente un listener nel costruttore e rimuovilo nel metodo dispose(). All'inizio, il listener può essere 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

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

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

  Future<void> loadPurchases() async {
    // Elided.
  }

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

  void purchasesUpdate() {
    //TODO manage updates
  }

Successivamente, 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>(),                         // Add this line
          ),
          lazy: false,
        ),

Successivamente, 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à stato applicato. Quando lo stato dell'abbonamento cambia, aggiorna anche lo stato del prodotto acquistabile in modo da 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 la certezza 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 clicker Dash.

12. Operazione completata.

Complimenti!!! Hai completato il codelab. Puoi trovare il codice completato per questo codelab nella cartella android_studio_folder.png complete.

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