In-App-Käufe zur Flutter App hinzufügen

1. Einführung

Zuletzt aktualisiert:11.07.2023

Damit Sie einer Flutter-App In-App-Käufe hinzufügen können, müssen die App und der Play Store korrekt eingerichtet, der Kauf bestätigt und die erforderlichen Berechtigungen, z. B. Abovorteile, gewährt werden.

In diesem Codelab fügen Sie einer App drei Arten von In-App-Käufen hinzu und bestätigen diese Käufe mithilfe eines Dart-Back-Ends mit Firebase. Die App „Dash Clicker“ enthält ein Spiel, in dem das Maskottchen „Dash“ als Währung verwendet wird. Sie fügen die folgenden Kaufoptionen hinzu:

  1. Eine wiederholbare Kaufoption für 2.000 Striche auf einmal.
  2. Ein einmaliges Upgrade, um aus dem alten Design einen modernen Dash zu machen.
  3. Ein Abo, bei dem sich die Anzahl der automatisch generierten Klicks verdoppelt.

Die erste Kaufoption bietet dem Nutzer einen direkten Vorteil von 2000 Dashes. Diese sind für den Nutzer direkt verfügbar und können mehrfach gekauft werden. Dies wird als Verbrauchsmaterial bezeichnet, da es direkt verbraucht wird und mehrmals konsumiert werden kann.

Die zweite Option erhöht den Dash auf einen schöneren Dash. Dies muss nur einmal gekauft werden und ist für immer verfügbar. Ein solcher Kauf wird als nicht konsumierbar bezeichnet, weil er von der App nicht verarbeitet werden kann, aber für immer gültig ist.

Die dritte und letzte Kaufoption ist ein Abo. Während das Abo aktiv ist, kann der Nutzer Dashes schneller erhalten, aber wenn er nicht mehr für das Abo bezahlt, stehen ihm auch die Vorteile nicht mehr zur Verfügung.

Der Back-End-Dienst (auch für Sie bereitgestellt) wird als Dart-App ausgeführt, überprüft, ob die Käufe getätigt wurden, und speichert sie mit Firestore. Firestore wird verwendet, um den Prozess zu vereinfachen. In Ihrer Produktionsanwendung können Sie jedoch jede Art von Back-End-Dienst verwenden.

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

Aufgaben

  • Du erweiterst eine App, um kurzfristig nutzbare Käufe und Abos zu unterstützen.
  • Außerdem erweitern Sie eine Dart-Backend-App, um die gekauften Artikel zu verifizieren und zu speichern.

Lerninhalte

  • App Store und Play Store mit käuflichen Produkten konfigurieren
  • Mit den Geschäften kommunizieren, um Käufe zu bestätigen und in Firestore zu speichern
  • Käufe in Ihrer App verwalten

Voraussetzungen

  • Android Studio 4.1 oder höher
  • Xcode 12 oder höher (für iOS-Entwicklung)
  • Flutter-SDK

2. Entwicklungsumgebung einrichten

Laden Sie den Code herunter und ändern Sie den Bundle-Identifikator für iOS und den Paketnamen für Android, um dieses Codelab zu starten.

Code herunterladen

Verwenden Sie den folgenden Befehl, um das GitHub-Repository über die Befehlszeile zu klonen:

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

Wenn Sie das GitHub-Cli-Tool installiert haben, verwenden Sie den folgenden Befehl:

gh repo clone flutter/codelabs flutter-codelabs

Der Beispielcode wird in ein flutter-codelabs-Verzeichnis geklont, das den Code für eine Sammlung von Codelabs enthält. Der Code für dieses Codelab befindet sich in flutter-codelabs/in_app_purchases.

Die Verzeichnisstruktur unter flutter-codelabs/in_app_purchases enthält eine Reihe von Snapshots davon, wo Sie sich am Ende jedes benannten Schritts befinden sollten. Der Startcode befindet sich in Schritt 0, sodass die Suche nach den übereinstimmenden Dateien so einfach ist wie:

cd flutter-codelabs/in_app_purchases/step_00

Wenn Sie vorwärts springen oder sehen möchten, wie nach einem Schritt etwas aussehen sollte, suchen Sie in dem Verzeichnis, das nach dem gewünschten Schritt benannt ist. Der Code des letzten Schritts befindet sich im Ordner complete.

Startprojekt einrichten

Öffnen Sie das Startprojekt aus step_00 in Ihrer bevorzugten IDE. Für die Screenshots haben wir Android Studio verwendet, aber auch Visual Studio Code ist eine gute Option. In beiden Editoren müssen die neuesten Dart- und Flutter-Plug-ins installiert sein.

Die von dir erstellten Apps müssen mit dem App Store und dem Play Store kommunizieren, damit sie wissen, welche Produkte zu welchem Preis verfügbar sind. Jede Anwendung wird durch eine eindeutige ID identifiziert. Im iOS App Store wird dies als Paket-ID bezeichnet und für den Android Play Store als App-ID. Diese IDs werden in der Regel mit der umgekehrten Domainnamen-Notation erstellt. Beim Kauf einer App für In-App-Käufe für flutter.dev würden wir beispielsweise dev.flutter.inapppurchase verwenden. Überlegen Sie sich eine Kennung für Ihre App, die Sie nun in den Projekteinstellungen festlegen.

Richten Sie zuerst den Paket-Identifikator für iOS ein.

Öffnen Sie das Projekt in Android Studio, klicken Sie mit der rechten Maustaste auf den iOS-Ordner, klicken Sie auf Flutter und öffnen Sie das Modul in der Xcode App.

942772eb9a73bfaa.png

In der Ordnerstruktur von Xcode befindet sich das Runner-Projekt oben und die Ziele Flutter, Runner und Products unterhalb des Runner-Projekts. Doppelklicken Sie auf Runner, um Ihre Projekteinstellungen zu bearbeiten, und klicken Sie auf Signing & Funktionen. Geben Sie im Feld Team die Bundle-ID ein, die Sie gerade ausgewählt haben, um Ihr Team festzulegen.

812f919d965c649a.jpeg

Sie können jetzt Xcode schließen und zu Android Studio zurückkehren, um die Konfiguration für Android abzuschließen. Öffnen Sie dazu die Datei build.gradle unter android/app, und ändern Sie Ihre applicationId (in Zeile 37 im Screenshot unten) in die App-ID, die mit dem iOS-Paket-Identifikator identisch ist. Die IDs für den iOS- und Android-Shop müssen nicht identisch sein. Wenn Sie sie identisch halten, ist das weniger fehleranfällig. Deshalb verwenden wir in diesem Codelab auch identische IDs.

5c4733ac560ae8c2.png

3. Plug-in installieren

In diesem Teil des Codelabs installieren Sie das Plug-in „in_app_purchase“.

Abhängigkeit in pubspec hinzufügen

Fügen Sie in_app_purchase zur Pubspec. Fügen Sie dazu in_app_purchase den Abhängigkeiten in Ihrer Pubspec hinzu:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

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

Klicken Sie auf pub get, um das Paket herunterzuladen, oder führen Sie flutter pub get in der Befehlszeile aus.

4. App Store einrichten

Wenn Sie In-App-Käufe einrichten und auf iOS-Geräten testen möchten, müssen Sie im App Store eine neue App mit kaufbaren Produkten erstellen. Du musst nichts veröffentlichen oder die App zur Überprüfung an Apple senden. Dafür benötigst du ein Entwicklerkonto. Wenn Sie keines haben, registrieren Sie sich für das Apple-Entwicklerprogramm.

Um In-App-Käufe nutzen zu können, benötigen Sie außerdem eine aktive Vereinbarung für kostenpflichtige Apps in App Store Connect. Rufe https://appstoreconnect.apple.com/ auf und klicke auf Vereinbarungen, Steuern und Bankwesen.

6e373780e5e24a6f.png

Hier finden Sie Vereinbarungen für kostenlose und kostenpflichtige Apps. Kostenlose Apps sollten „Aktiv“ und kostenpflichtige Apps einen neuen Status haben. Lesen Sie die Nutzungsbedingungen, akzeptieren Sie sie und geben Sie alle erforderlichen Informationen ein.

74c73197472c9aec.png

Wenn alles korrekt eingerichtet ist, ist der Status für kostenpflichtige Apps „Aktiv“. Dies ist sehr wichtig, da Sie In-App-Käufe ohne aktive Vereinbarung nicht testen können.

4a100bbb8cafdbbf.jpeg

App-ID registrieren

Erstellen Sie im Apple-Entwicklerportal eine neue ID.

55d7e592d9a3fc7b.png

App-IDs auswählen

13f125598b72ca77.png

App auswählen

41ac4c13404e2526.png

Geben Sie eine Beschreibung ein und legen Sie die Bundle-ID so fest, dass sie der Bundle-ID mit dem zuvor in XCode festgelegten Wert entspricht.

9d2c940ad80deeef.png

Weitere Informationen zum Erstellen einer neuen App-ID finden Sie in der Entwicklerkonto-Hilfe .

Neue App erstellen

Erstellen Sie im App Store Connect eine neue App mit Ihrer eindeutigen Paket-ID.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Weitere Informationen zum Erstellen einer neuen App und zum Verwalten von Vereinbarungen finden Sie in der App Store Connect-Hilfe.

Zum Testen der In-App-Käufe benötigen Sie einen Sandbox-Testnutzer. Dieser Testnutzer sollte nicht mit iTunes verbunden sein. Er wird nur zum Testen von In-App-Käufen verwendet. Sie können keine E-Mail-Adresse verwenden, die bereits für ein Apple-Konto verwendet wird. Gehen Sie in Nutzer und Zugriff zu Tester unter Sandbox, um ein neues Sandbox-Konto zu erstellen oder die vorhandenen Sandbox-Apple-IDs zu verwalten.

3ca2b26d4e391a4c.jpeg

Jetzt können Sie Ihren Sandbox-Nutzer auf Ihrem iPhone einrichten. Rufen Sie dazu Einstellungen > App Store > Sandbox-Konto.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

In-App-Käufe konfigurieren

Konfigurieren Sie nun die drei käuflichen Artikel:

  • dash_consumable_2k: Ein kurzfristig nutzbarer Kauf, der mehrmals gekauft werden kann und dem Nutzer 2.000 Dashes (die In-App-Währung) pro Kauf gewährt.
  • dash_upgrade_3d: Ein nicht nutzbares „Upgrade“ Kauf, der nur einmal gekauft werden kann, und gibt dem Nutzer einen optisch anderen Dash, auf den er klicken kann.
  • dash_subscription_doubler: Ein Abo, das dem Nutzer während der gesamten Laufzeit doppelt so viele Striche pro Klick gewährt.

d156b2f5bac43ca8.png

Rufen Sie In-App-Käufe > Verwalten.

Erstellen Sie Ihre In-App-Käufe mit den angegebenen IDs:

  1. Richte dash_consumable_2k als Consumable ein.

Verwenden Sie dash_consumable_2k als Produkt-ID. Der Referenzname wird nur in App Store Connect verwendet. Setzen Sie ihn einfach auf dash consumable 2k und fügen Sie Ihre Lokalisierungen für den Kauf hinzu. Rufen Sie den Kauf Spring is in the air mit 2000 dashes fly out als Beschreibung auf.

ec1701834fd8527.png

  1. Richte dash_upgrade_3d als Nicht verbraucht ein.

Verwenden Sie dash_upgrade_3d als Produkt-ID. Lege den Referenznamen auf „dash upgrade 3d“ fest und füge deine Lokalisierungen für den Kauf hinzu. Rufen Sie den Kauf 3D Dash mit Brings your dash back to the future als Beschreibung auf.

6765d4b711764c30.png

  1. Richte dash_subscription_doubler als Abo mit automatischer Verlängerung ein.

Der Ablauf für Abos ist etwas anders. Zuerst musst du den Referenznamen und die Produkt-ID festlegen:

6d29e08dae26a0c4.png

Als Nächstes musst du eine Abogruppe erstellen. Wenn mehrere Abos Teil derselben Gruppe sind, kann ein Nutzer nur eines dieser Abos gleichzeitig abonnieren. Zwischen diesen Abos kann jedoch problemlos ein Upgrade oder Downgrade ausgeführt werden. Nennen Sie diese Gruppe einfach subscriptions.

5bd0da17a85ac076.png

Geben Sie als Nächstes die Abodauer und die Lokalisierungen ein. Geben Sie dem Abo den Namen „Jet Engine“ und die Beschreibung „Doubles your clicks“. Klicken Sie auf Speichern.

bd1b1d82eeee4cb3.png

Nachdem Sie auf die Schaltfläche Speichern geklickt haben, fügen Sie einen Abopreis hinzu. Wählen Sie einen beliebigen Preis aus.

d0bf39680ef0aa2e.png

Sie sollten jetzt die drei Käufe in der Liste der Käufe sehen:

99d5c4b446e8fecf.png

5. Play Store einrichten

Wie für den App Store benötigen Sie auch für den Play Store ein Entwicklerkonto. Wenn Sie noch kein Konto haben, registrieren Sie ein Konto.

Neue App erstellen

So erstellen Sie eine neue App in der Google Play Console:

  1. Öffnen Sie die Play Console.
  2. Wählen Sie Alle Apps > App erstellen.
  3. Wählen Sie eine Standardsprache und einen Titel für die App aus. Geben Sie den Namen der App so ein, wie er bei Google Play erscheinen soll. Sie können den Namen bei Bedarf später ändern.
  4. Geben Sie an, dass es sich bei Ihrer App um ein Spiel handelt. Sie können dies später ändern.
  5. Geben Sie an, ob Ihre Anwendung kostenlos oder kostenpflichtig ist.
  6. Füge eine E-Mail-Adresse hinzu, über die Play Store-Nutzer dich bezüglich dieser App kontaktieren können.
  7. Füllen Sie die Erklärungen zu Inhaltsrichtlinien und Exportbestimmungen der USA aus.
  8. Wählen Sie App erstellen aus.

Rufen Sie nach dem Erstellen Ihrer App das Dashboard auf und führen Sie alle Aufgaben im Abschnitt App einrichten aus. Hier geben Sie einige Informationen zu Ihrer App an, z. B. Altersfreigaben und Screenshots. 13845badcf9bc1db.png

Anwendung signieren

Um In-App-Käufe testen zu können, muss mindestens ein Build bei Google Play hochgeladen sein.

Dazu muss der Release-Build nicht mit den Schlüsseln zur Fehlerbehebung signiert werden.

Schlüsselspeicher erstellen

Wenn bereits ein Schlüsselspeicher vorhanden ist, fahren Sie mit dem nächsten Schritt fort. Falls nicht, erstellen Sie eines, indem Sie in der Befehlszeile Folgendes ausführen.

Verwenden Sie unter Mac/Linux den folgenden Befehl:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

Verwenden Sie unter Windows den folgenden Befehl:

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

Mit diesem Befehl wird die Datei key.jks in Ihrem Basisverzeichnis gespeichert. Wenn Sie die Datei an einem anderen Ort speichern möchten, ändern Sie das Argument, das Sie an den Parameter -keystore übergeben. Die

keystore

file private; nicht in die öffentliche Versionsverwaltung einchecken!

Schlüsselspeicher über die Anwendung referenzieren

Erstellen Sie eine Datei mit dem Namen <your app dir>/android/key.properties, die einen Verweis auf Ihren Schlüsselspeicher enthält:

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>

Gradle für die Anmeldung konfigurieren

Konfigurieren Sie die Signatur für Ihre App, indem Sie die Datei <your app dir>/android/app/build.gradle bearbeiten.

Fügen Sie die Schlüsselspeicherinformationen aus Ihrer Property-Datei vor dem android-Block hinzu:

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

   android {
         // omitted
   }

Laden Sie die Datei key.properties in das keystoreProperties-Objekt.

Fügen Sie den folgenden Code vor dem Block buildTypes ein:

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

Konfigurieren Sie den Block signingConfigs in der Datei build.gradle Ihres Moduls mit den Informationen zur Signaturkonfiguration:

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

Release-Builds deiner App werden jetzt automatisch signiert.

Weitere Informationen zum Signieren Ihrer App finden Sie auf developer.android.com unter App signieren.

Ersten Build hochladen

Nachdem Ihre App für das Signieren konfiguriert wurde, sollten Sie sie mit folgendem Befehl erstellen können:

flutter build appbundle

Dieser Befehl generiert standardmäßig einen Release-Build. Die Ausgabe finden Sie unter <your app dir>/build/app/outputs/bundle/release/.

Gehen Sie im Dashboard der Google Play Console zu Release > Testen > Geschlossener Test und erstellen Sie einen neuen, geschlossenen Testrelease.

In diesem Codelab bleiben Sie beim Signieren der App durch Google. Drücken Sie also unter Play App-Signatur auf Weiter, um sich anzumelden.

ba98446d9c5c40e0.png

Laden Sie als Nächstes das App Bundle app-release.aab hoch, das mit dem Build-Befehl generiert wurde.

Klicken Sie auf Speichern und dann auf Release überprüfen.

Klicken Sie abschließend auf Für interne Tests einführen, um den internen Testrelease zu aktivieren.

Testnutzer einrichten

Um In-App-Käufe testen zu können, müssen die Google-Konten Ihrer Tester in der Google Play Console an zwei Orten hinzugefügt werden:

  1. An den spezifischen Test-Track (interner Test)
  2. Als Lizenztester

Fügen Sie den Tester zuerst dem internen Test-Track hinzu. Gehen Sie zurück zu Release > Testen > Interne Tests und klicken Sie auf den Tab Tester.

a0d0394e85128f84.png

Erstellen Sie eine neue E-Mail-Liste, indem Sie auf E-Mail-Liste erstellen klicken. Benennen Sie die Liste und fügen Sie die E-Mail-Adressen der Google-Konten hinzu, die Zugriff zum Testen von In-App-Käufen benötigen.

Klicken Sie auf das Kästchen für die Liste und dann auf Änderungen speichern.

Fügen Sie dann die Lizenztester hinzu:

  1. Kehren Sie zur Ansicht Alle Apps in der Google Play Console zurück.
  2. Gehen Sie zu Einstellungen > Lizenztests.
  3. Füge dieselben E-Mail-Adressen der Tester hinzu, die In-App-Käufe testen können müssen.
  4. Setze Lizenzantwort auf RESPOND_NORMALLY.
  5. Klicken Sie auf ROLLE ERSTELLEN.

a1a0f9d3e55ea8da.png

In-App-Käufe konfigurieren

Nun konfigurieren Sie die Artikel, die in der App gekauft werden können.

Genau wie im App Store müssen Sie drei verschiedene Käufe definieren:

  • dash_consumable_2k: Ein kurzfristig nutzbarer Kauf, der mehrmals gekauft werden kann und dem Nutzer 2.000 Dashes (die In-App-Währung) pro Kauf gewährt.
  • dash_upgrade_3d: Ein nicht nutzbares „Upgrade“ -Kauf, der nur einmal gekauft werden kann. Dadurch erhält der Nutzer einen optisch anderen Dash, auf den er klicken muss.
  • dash_subscription_doubler: Ein Abo, das dem Nutzer während der gesamten Laufzeit doppelt so viele Striche pro Klick gewährt.

Füge zuerst das Verbrauchsmaterial und das Nicht-Verbrauchsmaterial hinzu.

  1. Rufen Sie die Google Play Console auf und wählen Sie Ihre App aus.
  2. Rufe Monetarisieren > Produkte > In-App-Produkte
  3. Klicken Sie auf Produkt erstellenc8d66e32f57dee21.png
  4. Geben Sie alle erforderlichen Informationen für das Produkt ein. Achten Sie darauf, dass die Produkt-ID genau mit der ID übereinstimmt, die Sie verwenden möchten.
  5. Klicken Sie auf Speichern.
  6. Klicken Sie auf Aktivieren.
  7. Wiederholen Sie den Vorgang für das nicht nutzbare „Upgrade“. kaufen.

Fügen Sie als Nächstes das Abo hinzu:

  1. Rufen Sie die Google Play Console auf und wählen Sie Ihre App aus.
  2. Rufe Monetarisieren > Produkte > Abos.
  3. Klicke auf Abo erstellen32a6a9eefdb71dd0.png
  4. Geben Sie alle erforderlichen Informationen für Ihr Abo ein. Achten Sie darauf, dass die Produkt-ID genau mit der ID übereinstimmt, die Sie verwenden möchten.
  5. Klicken Sie auf Speichern.

Deine Käufe sollten jetzt in der Play Console eingerichtet sein.

6. Firebase einrichten

In diesem Codelab nutzen Sie einen Back-End-Dienst, um die Käufe.

Die Verwendung eines Back-End-Dienstes bietet mehrere Vorteile:

  • Sie können Transaktionen sicher bestätigen.
  • Sie können auf Abrechnungsereignisse aus den App-Shops reagieren.
  • Sie können die Käufe in einer Datenbank verfolgen.
  • Nutzer können Ihre App nicht dazu bringen, Premium-Funktionen bereitzustellen, indem sie ihre Systemuhr zurückspulen.

Es gibt viele Möglichkeiten, einen Back-End-Dienst einzurichten. Verwenden Sie dazu Cloud Functions und Firestore und verwenden Sie dazu Firebase von Google.

Das Schreiben des Backends ist in diesem Codelab nicht enthalten. Daher enthält der Startcode bereits ein Firebase-Projekt, das grundlegende Käufe verarbeitet, um Ihnen den Einstieg zu erleichtern.

In der Start-App sind auch Firebase-Plug-ins enthalten.

Jetzt müssen Sie ein eigenes Firebase-Projekt erstellen, die App und das Backend für Firebase konfigurieren und schließlich das Backend bereitstellen.

Firebase-Projekt erstellen

Rufen Sie die Firebase Console auf und erstellen Sie ein neues Firebase-Projekt. In diesem Beispiel nennen wir das Projekt „Dash Clicker“.

In der Back-End-App verknüpfen Sie Käufe mit einem bestimmten Nutzer. Daher ist eine Authentifizierung erforderlich. Nutzen Sie dazu das Authentifizierungsmodul von Firebase mit Google Log-in.

  1. Gehen Sie im Firebase-Dashboard zu Authentifizierung und aktivieren Sie die Authentifizierung bei Bedarf.
  2. Gehen Sie zum Tab Anmeldemethode und aktivieren Sie den Google-Anmeldeanbieter.

7babb48832fbef29.png

Da Sie auch die Firestore-Datenbank von Firebase verwenden, sollten Sie diese ebenfalls aktivieren.

e20553e0de5ac331.png

Legen Sie die Cloud Firestore-Regeln so fest:

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

Firebase for Flutter einrichten

Die empfohlene Methode zum Installieren von Firebase in der Flutter-App ist die Verwendung der FlutterFire CLI. Folgen Sie der Anleitung auf der Einrichtungsseite.

Wenn Sie Flutterfire konfigurieren, wählen Sie das Projekt aus, das Sie im vorherigen Schritt erstellt haben.

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

Aktivieren Sie als Nächstes iOS und Android, indem Sie die beiden Plattformen auswählen.

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

Wenn Sie aufgefordert werden, „firebase_options.dart“ zu überschreiben, wählen Sie „yes“ (Ja).

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

Firebase für Android einrichten: Weitere Schritte

Gehen Sie im Firebase-Dashboard zu Projektübersicht, wählen Sie Einstellungen und dann den Tab Allgemein aus.

Scrollen Sie nach unten zu Meine Apps und wählen Sie die App Dashclicker (Android) aus.

b22d46a759c0c834.png

Wenn Sie die Google-Anmeldung im Debug-Modus zulassen möchten, müssen Sie den SHA-1-Hash-Fingerabdruck Ihres Fehlerbehebungszertifikats angeben.

Hash des Signaturzertifikats für die Fehlerbehebung abrufen

Wechseln Sie im Stammverzeichnis Ihres Flutter-App-Projekts in den Ordner android/ und generieren Sie einen Signaturbericht.

cd android
./gradlew :app:signingReport

Es wird eine umfangreiche Liste von Signaturschlüsseln angezeigt. Da Sie nach dem Hash für das Debug-Zertifikat suchen, suchen Sie nach dem Zertifikat, bei dem die Attribute Variant und Config auf debug festgelegt sind. Der Schlüsselspeicher befindet sich wahrscheinlich im Basisordner unter .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

Kopieren Sie den SHA-1-Hash und füllen Sie das letzte Feld im modalen Dialogfeld zum Senden der App aus.

Firebase für iOS einrichten: Weitere Schritte

Öffnen Sie ios/Runnder.xcworkspace mit Xcode. oder mit der IDE Ihrer Wahl.

Klicke in VSCode mit der rechten Maustaste auf den Ordner ios/ und dann auf open in xcode.

Klicke in Android Studio mit der rechten Maustaste auf den Ordner ios/, dann auf flutter und dann auf die Option open iOS module in Xcode.

Um Google Log-in unter iOS zu ermöglichen, fügen Sie den Build-plist-Dateien die Konfigurationsoption CFBundleURLTypes hinzu. Weitere Informationen finden Sie in der Dokumentation zum google_sign_in-Paket. In diesem Fall sind die Dateien ios/Runner/Info-Debug.plist und ios/Runner/Info-Release.plist.

Das Schlüssel/Wert-Paar wurde bereits hinzugefügt, die Werte müssen jedoch ersetzt werden:

  1. Rufen Sie den Wert für REVERSED_CLIENT_ID aus der Datei GoogleService-Info.plist ohne das Element <string>..</string> ab.
  2. Ersetzen Sie den Wert in den Dateien ios/Runner/Info-Debug.plist und ios/Runner/Info-Release.plist unter dem Schlüssel 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>

Die Firebase-Einrichtung ist jetzt abgeschlossen.

7. Benachrichtigungen zu Käufen anhören

In diesem Teil des Codelabs bereiten Sie die App für den Kauf der Produkte vor. Dieser Prozess umfasst das Überwachen von Kaufaktualisierungen und Fehlern nach dem Start der App.

Informationen zu Käufen anhören

Suchen Sie in main.dart, nach dem Widget MyHomePage, das eine Scaffold mit einer BottomNavigationBar enthält, die zwei Seiten enthält. Auf dieser Seite werden außerdem drei Providers für DashCounter, DashUpgrades, und DashPurchases erstellt. DashCounter verfolgt die aktuelle Anzahl an Bindestrichen und erhöht sie automatisch. DashUpgrades verwaltet die Upgrades, die Sie mit Dashes erwerben können. In diesem Codelab geht es um DashPurchases.

Standardmäßig wird das Objekt eines Anbieters definiert, wenn dieses Objekt zum ersten Mal angefordert wird. Dieses Objekt überwacht Kaufaktualisierungen direkt, wenn die App gestartet wird. Deaktivieren Sie daher Lazy Loading für dieses Objekt mit lazy: false:

lib/main.dart

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

Außerdem benötigen Sie eine Instanz von InAppPurchaseConnection. Damit die App jedoch testbar bleibt, müssen Sie die Verbindung simulieren. Erstellen Sie dazu eine Instanzmethode, die im Test überschrieben werden kann, und fügen Sie sie zu main.dart hinzu.

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

Sie müssen den Test geringfügig aktualisieren, damit er weiterhin funktioniert. Den vollständigen Code für TestIAPConnection finden Sie unter widget_test.dart auf GitHub.

test/widget_test.dart

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

Rufen Sie in lib/logic/dash_purchases.dart den Code für DashPurchases ChangeNotifier auf. Derzeit gibt es nur ein DashCounter, das du deinen gekauften Dashes hinzufügen kannst.

Fügen Sie die Stream-Abo-Property _subscription (vom Typ StreamSubscription<List<PurchaseDetails>> _subscription;), die IAPConnection.instance, und die Importe hinzu. Der Code sollte so aussehen:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

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

  DashPurchases(this.counter);
}

Das Keyword late wird zu _subscription hinzugefügt, da _subscription im Konstruktor initialisiert wird. Dieses Projekt ist standardmäßig so eingerichtet, dass keine Nullwerte zulässig sind (NNBD). Das bedeutet, dass Attribute, für die keine Nullwerte deklariert werden, einen Wert haben müssen, der nicht null ist. Mit dem Qualifier late können Sie die Definition dieses Werts verzögern.

Rufen Sie im Konstruktor den purchaseUpdatedStream ab und beginnen Sie mit dem Anhören des Streams. Kündigen Sie in der Methode dispose() das Streamabo.

lib/logic/dash_purchases.dart

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

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

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

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

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

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

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

Jetzt erhält die App die Kauf-Updates, sodass Sie im nächsten Abschnitt einen Kauf tätigen!

Führe die Tests mit „flutter test"“ aus, um zu prüfen, ob alles korrekt eingerichtet ist, bevor du fortfährst.

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. Einkaufen

In diesem Teil des Codelabs ersetzen Sie die vorhandenen simulierten Produkte durch echte käufliche Produkte. Diese Produkte werden in den Geschäften geladen, in einer Liste angezeigt und gekauft, wenn sie auf das Produkt tippen.

Kaufbare Produkte anpassen

PurchasableProduct zeigt ein simuliertes Produkt an. Aktualisieren Sie sie, um die tatsächlichen Inhalte anzuzeigen. Ersetzen Sie dazu die Klasse PurchasableProduct in purchasable_product.dart durch den folgenden Code:

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

Entferne in dash_purchases.dart, die Dummy-Käufe und ersetze sie durch eine leere Liste: List<PurchasableProduct> products = [];

Verfügbare Käufe laden

Damit Nutzer die Möglichkeit haben, etwas zu kaufen, laden Sie die Käufe aus dem Store. Prüfen Sie zuerst, ob das Geschäft verfügbar ist. Wenn der Shop nicht verfügbar ist, wird dem Nutzer eine Fehlermeldung angezeigt, wenn du storeState auf notAvailable setzt.

lib/logic/dash_purchases.dart

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

Wenn der Shop verfügbar ist, laden Sie die verfügbaren Käufe. Bei der bisherigen Firebase-Einrichtung können Sie mit storeKeyConsumable, storeKeySubscription, und storeKeyUpgrade rechnen. Wenn ein erwarteter Kauf nicht verfügbar ist, geben Sie diese Informationen in der Konsole aus. können Sie diese Informationen an den Back-End-Dienst senden.

Die Methode await iapConnection.queryProductDetails(ids) gibt sowohl die nicht gefundenen IDs als auch die gefundenen kaufbaren Produkte zurück. Verwenden Sie productDetails aus der Antwort, um die UI zu aktualisieren, und legen Sie StoreState auf available fest.

lib/logic/dash_purchases.dart

import '../constants.dart';

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

Rufen Sie die Funktion loadPurchases() im Konstruktor auf:

lib/logic/dash_purchases.dart

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

Ändern Sie zuletzt den Wert des Felds storeState von StoreState.available in StoreState.loading:.

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Kaufbare Produkte anzeigen

Sehen Sie sich die Datei purchase_page.dart an. Das Widget PurchasePage zeigt je nach StoreState _PurchasesLoading, _PurchaseList, oder _PurchasesNotAvailable, an. Das Widget zeigt auch die bisherigen Käufe des Nutzers an, die im nächsten Schritt verwendet werden.

Das _PurchaseList-Widget zeigt die Liste der kaufbaren Produkte an und sendet eine Kaufanfrage an das DashPurchases-Objekt.

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

Wenn die Produkte richtig konfiguriert sind, sollten Sie die verfügbaren Produkte im Android- und iOS-Store sehen. Hinweis: Es kann einige Zeit dauern, bis die Käufe in den jeweiligen Konsolen verfügbar sind.

ca1a9f97c21e552d.png

Kehren Sie zu dash_purchases.dart zurück und implementieren Sie die Funktion, um ein Produkt zu kaufen. Du musst nur die Verbrauchsmaterialien von den nicht Verbrauchsgütern trennen. Das Upgrade und die Aboprodukte sind nicht nutzbar.

lib/logic/dash_purchases.dart

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

Bevor Sie fortfahren, erstellen Sie die Variable _beautifiedDashUpgrade und aktualisieren Sie den beautifiedDash-Getter, um darauf zu verweisen.

lib/logic/dash_purchases.dart

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

Die Methode _onPurchaseUpdate empfängt die Kaufaktualisierungen, aktualisiert den Status des auf der Kaufseite angezeigten Produkts und wendet den Kauf auf die Zählerlogik an. Es ist wichtig, dass Sie completePurchase anrufen, nachdem der Kauf bearbeitet wurde, damit der Händler weiß, dass er ordnungsgemäß abgewickelt wird.

lib/logic/dash_purchases.dart

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

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

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

9. Backend einrichten

Bevor Sie mit dem Tracking und der Verifizierung von Käufen fortfahren, richten Sie ein Dart-Backend ein, das dies unterstützt.

In diesem Abschnitt arbeiten Sie vom Ordner dart-backend/ als Stammverzeichnis aus.

Stellen Sie sicher, dass die folgenden Tools installiert sind:

Basisprojekt – Übersicht

Da einige Teile dieses Projekts für dieses Codelab als nicht vorgesehen angesehen werden, sind sie im Startcode enthalten. Es ist ratsam, vor Beginn durchzugehen, was bereits im Startcode enthalten ist, um eine Vorstellung davon zu bekommen, wie Sie die Dinge strukturieren werden.

Dieser Back-End-Code kann lokal auf Ihrem Computer ausgeführt werden. Sie müssen ihn nicht bereitstellen, um ihn verwenden zu können. Sie müssen jedoch eine Verbindung von Ihrem Entwicklungsgerät (Android oder iPhone) zu dem Computer herstellen können, auf dem der Server ausgeführt wird. Dazu müssen sie sich im selben Netzwerk befinden und Sie müssen die IP-Adresse Ihres Computers kennen.

Versuchen Sie, den Server mit dem folgenden Befehl auszuführen:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

Das Dart-Back-End verwendet shelf und shelf_router, um API-Endpunkte bereitzustellen. Standardmäßig stellt der Server keine Routen bereit. Später erstellen Sie eine Route für die Bestätigung von Käufen.

Ein Teil, der bereits im Startcode enthalten ist, ist IapRepository in lib/iap_repository.dart. Da es für dieses Codelab nicht relevant ist, die Interaktion mit Firestore oder Datenbanken im Allgemeinen zu erlernen, enthält der Startcode Funktionen zum Erstellen oder Aktualisieren von Käufen in Firestore sowie alle Klassen für diese Käufe.

Firebase-Zugriff einrichten

Für den Zugriff auf Firebase Firestore benötigen Sie einen Zugriffsschlüssel für das Dienstkonto. Generieren Sie einen Schlüssel in den Firebase-Projekteinstellungen, gehen Sie zum Bereich Dienstkonten und wählen Sie Neuen privaten Schlüssel generieren aus.

27590fc77ae94ad4.png

Kopieren Sie die heruntergeladene JSON-Datei in den Ordner assets/ und benennen Sie sie in service-account-firebase.json um.

Zugriff auf Google Play einrichten

Wenn Sie zum Bestätigen von Käufen auf den Play Store zugreifen möchten, müssen Sie ein Dienstkonto mit diesen Berechtigungen erstellen und die JSON-Anmeldedaten dafür herunterladen.

  1. Rufen Sie die Google Play Console auf und beginnen Sie auf der Seite Alle Apps.
  2. Gehen Sie zu Einrichtung > API-Zugriff. 317fdfb54921f50e.png Falls du in der Google Play Console aufgefordert wirst, ein Projekt zu erstellen oder eine Verknüpfung zu einem vorhandenen Projekt herzustellen, musst du das zuerst tun und dann zu dieser Seite zurückkehren.
  3. Klicken Sie im Abschnitt zum Definieren von Dienstkonten auf Neues Dienstkonto erstellen.1e70d3f8d794bebb.png
  4. Klicken Sie im angezeigten Dialogfeld auf den Link Google Cloud Platform. 7c9536336dd9e9b4.png
  5. Wählen Sie Ihr Projekt aus. Wenn Sie es nicht sehen, prüfen Sie, ob Sie oben rechts in der Drop-down-Liste Konto im richtigen Google-Konto angemeldet sind. 3fb3a25bad803063.png
  6. Nachdem Sie Ihr Projekt ausgewählt haben, klicken Sie in der oberen Menüleiste auf + Dienstkonto erstellen. 62fe4c3f8644acd8.png
  7. Geben Sie einen Namen für das Dienstkonto und optional eine Beschreibung ein, damit Sie sich daran erinnern, und fahren Sie mit dem nächsten Schritt fort. 8a92d5d6a3dff48c.png
  8. Weisen Sie dem Dienstkonto die Rolle Bearbeiter zu. 6052b7753667ed1a.png
  9. Schließen Sie den Assistenten ab, kehren Sie in der Entwicklerkonsole zur Seite API-Zugriff zurück und klicken Sie auf Dienstkonten aktualisieren. Das neu erstellte Konto sollte in der Liste angezeigt werden. 5895a7db8b4c7659.png
  10. Klicken Sie für das neue Dienstkonto auf Zugriff gewähren.
  11. Scrollen Sie auf der nächsten Seite nach unten zum Block Finanzdaten. Wählen Sie Finanzdaten, Bestellungen und Antworten aus der Kündigungsumfrage ansehen sowie Bestellungen und Abos verwalten aus. 75b22d0201cf67e.png
  12. Klicken Sie auf Nutzer einladen. 70ea0b1288c62a59.png
  13. Nachdem das Konto eingerichtet ist, müssen Sie nur noch einige Anmeldedaten generieren. Gehen Sie zurück zur Cloud Console und suchen Sie in der Liste der Dienstkonten nach Ihrem Dienstkonto. Klicken Sie auf das Dreipunkt-Menü und wählen Sie Schlüssel verwalten aus. 853ee186b0e9954e.png
  14. Erstellen Sie einen neuen JSON-Schlüssel und laden Sie ihn herunter. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Benennen Sie die heruntergeladene Datei in service-account-google-play.json, um und verschieben Sie sie in das Verzeichnis assets/.

Öffnen Sie lib/constants.dart, und ersetzen Sie den Wert von androidPackageId durch die Paket-ID, die Sie für Ihre Android-App ausgewählt haben.

Zugriff auf den Apple App Store einrichten

Um auf den App Store zur Bestätigung von Käufen zugreifen zu können, müssen Sie ein gemeinsames Secret einrichten:

  1. Öffnen Sie App Store Connect.
  2. Gehen Sie zu Meine Apps und wählen Sie Ihre App aus.
  3. Wählen Sie in der Navigationsleiste In-App-Käufe > Verwalten.
  4. Klicken Sie rechts oben in der Liste auf App-Specific Shared Secret (App-spezifisches gemeinsames Secret).
  5. Generieren Sie ein neues Secret und kopieren Sie es.
  6. Öffnen Sie lib/constants.dart, und ersetzen Sie den Wert von appStoreSharedSecret durch das soeben generierte gemeinsame Secret.

d8b8042470aaeff.png

b72f4565750e2f40.png

Konstanten-Konfigurationsdatei

Bevor Sie fortfahren, prüfen Sie, ob die folgenden Konstanten in der Datei lib/constants.dart konfiguriert sind:

  • androidPackageId: unter Android verwendete Paket-ID. z.B. com.example.dashclicker
  • appStoreSharedSecret: gemeinsames Secret für den Zugriff auf App Store Connect zur Bestätigung von Käufen.
  • bundleId: Bundle-ID, die unter iOS verwendet wird. z.B. com.example.dashclicker

Die übrigen Konstanten können Sie vorerst ignorieren.

10. Käufe bestätigen

Der allgemeine Ablauf zum Bestätigen von Käufen ist für iOS und Android ähnlich.

In beiden Stores erhält Ihre Anwendung bei einem Kauf ein Token.

Dieses Token wird von der App an Ihren Back-End-Dienst gesendet, der wiederum den Kauf mithilfe des bereitgestellten Tokens auf den Servern des jeweiligen Shops verifiziert.

Der Back-End-Dienst kann dann festlegen, dass der Kauf gespeichert wird, und der Anwendung antworten, unabhängig davon, ob der Kauf gültig war oder nicht.

Wenn der Back-End-Dienst die Validierung in den Shops und nicht in der Anwendung durchführt, die auf dem Gerät des Nutzers ausgeführt wird, können Sie verhindern, dass der Nutzer Zugriff auf Premium-Funktionen erhält, indem Sie beispielsweise seine Systemuhr zurückspulen.

Flutter-Seite einrichten

Authentifizierung einrichten

Wenn Sie die Käufe an Ihren Backend-Dienst senden, möchten Sie sicherstellen, dass der Nutzer während eines Kaufs authentifiziert ist. Der größte Teil der Authentifizierungslogik wurde bereits im Startprojekt für Sie hinzugefügt. Sie müssen nur darauf achten, dass PurchasePage die Anmeldeschaltfläche anzeigt, wenn der Nutzer noch nicht angemeldet ist. Fügen Sie den folgenden Code am Anfang der Build-Methode von PurchasePage ein:

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

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

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

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

Endpunkt für die Anrufbestätigung über die App

Erstelle in der App die Funktion _verifyPurchase(PurchaseDetails purchaseDetails), die den Endpunkt /verifypurchase auf deinem Dart-Backend über einen HTTP-Post-Aufruf aufruft.

Sende den ausgewählten Store (google_play für den Play Store oder app_store für den App Store), die serverVerificationData und die productID. Der Server gibt einen Statuscode zurück, der angibt, ob der Kauf bestätigt wurde.

Konfigurieren Sie in den App-Konstanten die Server-IP-Adresse für die IP-Adresse Ihres lokalen Computers.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

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

firebaseNotifier durch Erstellen von DashPurchases in main.dart: hinzufügen

lib/main.dart

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

Fügen Sie in FirebaseNotifier einen Getter für den Nutzer hinzu, damit Sie die User-ID an die Funktion zur Bestätigung des Kaufs übergeben können.

lib/logic/firebase_notifier.dart

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

Fügen Sie der Klasse DashPurchases die Funktion _verifyPurchase hinzu. Diese async-Funktion gibt einen booleschen Wert zurück, der angibt, ob der Kauf validiert wurde.

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

Rufen Sie die Funktion _verifyPurchase in _handlePurchase auf, bevor Sie den Kauf anwenden. Du solltest einen Kauf erst dann anwenden, wenn er bestätigt ist. In einer Produktions-App können Sie dies beispielsweise genauer festlegen, wenn Sie beispielsweise ein Probeabo nutzen möchten, wenn der Store vorübergehend nicht verfügbar ist. In diesem Beispiel sollten Sie es einfach halten und den Kauf nur anwenden, wenn er erfolgreich bestätigt wurde.

lib/logic/dash_purchases.dart

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

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

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

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

In der App ist jetzt alles bereit, um die Käufe zu validieren.

Back-End-Dienst einrichten

Als Nächstes richten Sie die Cloud Functions-Funktion zum Überprüfen von Käufen im Backend ein.

Kauf-Handler erstellen

Da der Bestätigungsvorgang für beide Geschäfte fast identisch ist, richten Sie eine abstrakte PurchaseHandler-Klasse mit separaten Implementierungen für jedes Geschäft ein.

be50c207c5a2a519.png

Füge dem Ordner lib/ zuerst eine purchase_handler.dart-Datei hinzu, in der du eine abstrakte PurchaseHandler-Klasse mit zwei abstrakten Methoden zur Bestätigung von zwei verschiedenen Arten von Käufen definierst: Abos und andere Käufe.

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

Wie Sie sehen, sind für jede Methode drei Parameter erforderlich:

  • userId: Die ID des angemeldeten Nutzers, anhand derer du die Käufe dem Nutzer zuordnen kannst.
  • productData: Daten zum Produkt. Sie definieren dies gleich.
  • token: Das Token, das dem Nutzer vom Store bereitgestellt wird.

Zur einfacheren Verwendung dieser Kauf-Handler fügen Sie außerdem eine verifyPurchase()-Methode hinzu, die sowohl für Abos als auch für Nicht-Abos verwendet werden kann:

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

Jetzt können Sie einfach für beide Fälle verifyPurchase aufrufen, haben aber immer noch separate Implementierungen.

Die Klasse ProductData enthält grundlegende Informationen zu den verschiedenen kaufbaren Produkten, darunter die Produkt-ID (manchmal auch als SKU bezeichnet) und die ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType kann entweder ein Abo oder ein Nicht-Abo sein.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

Schließlich wird die Liste der Produkte als Zuordnung in derselben Datei definiert.

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

Definieren Sie als Nächstes einige Platzhalterimplementierungen für den Google Play Store und den Apple App Store. Mit Google Play loslegen:

Erstellen Sie lib/google_play_purchase_handler.dart und fügen Sie eine Klasse hinzu, die die soeben geschriebene PurchaseHandler erweitert:

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

Vorerst wird true für die Handler-Methoden zurückgegeben. kommen Sie später darauf zurück.

Wie Sie vielleicht bemerkt haben, verwendet der Konstruktor eine Instanz von IapRepository. Der Kauf-Handler verwendet diese Instanz, um später Informationen zu Käufen in Firestore zu speichern. Für die Kommunikation mit Google Play verwendest du das bereitgestellte AndroidPublisherApi.

Wiederholen Sie anschließend den Vorgang für den App-Shop-Handler. Erstellen Sie lib/app_store_purchase_handler.dart und fügen Sie eine Klasse hinzu, die die PurchaseHandler noch einmal erweitert:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

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

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

Sehr gut! Jetzt haben Sie zwei Kauf-Handler. Als Nächstes erstellen wir den API-Endpunkt für die Kaufüberprüfung.

Kauf-Handler verwenden

Öffnen Sie bin/server.dart und erstellen Sie mit shelf_route einen API-Endpunkt:

bin/server.dart

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router);
}

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

Der obige Code führt Folgendes aus:

  1. Definieren Sie einen POST-Endpunkt, der von der zuvor erstellten Anwendung aufgerufen wird.
  2. Decodieren Sie die JSON-Nutzlast und extrahieren Sie die folgenden Informationen:
  3. userId: Derzeit angemeldete User-ID
  4. source: Belegter Speicher, app_store oder google_play.
  5. productData: Aus der zuvor erstellten productDataMap.
  6. token: Enthält die Verifizierungsdaten, die an die Geschäfte gesendet werden sollen.
  7. Rufen Sie die Methode verifyPurchase auf, je nach Quelle für GooglePlayPurchaseHandler oder AppStorePurchaseHandler.
  8. Wenn die Überprüfung erfolgreich war, gibt die Methode Response.ok an den Client zurück.
  9. Wenn die Überprüfung fehlschlägt, gibt die Methode eine Response.internalServerError an den Client zurück.

Nachdem Sie den API-Endpunkt erstellt haben, müssen Sie die beiden Kauf-Handler konfigurieren. Dazu müssen Sie die Dienstkontoschlüssel laden, die Sie im vorherigen Schritt erhalten haben, und den Zugriff auf die verschiedenen Dienste konfigurieren, einschließlich der Android Publisher API und der Firebase Firestore API. Erstellen Sie dann die beiden Kauf-Handler mit den verschiedenen Abhängigkeiten:

bin/server.dart

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

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

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

Android-Käufe bestätigen: Kaufvorgang implementieren

Implementieren Sie als Nächstes den Google Play-Kauf-Handler.

Google stellt bereits Dart-Pakete für die Interaktion mit den APIs zur Verfügung, die Sie zum Bestätigen von Käufen benötigen. Sie haben sie in der Datei server.dart initialisiert und jetzt in der Klasse GooglePlayPurchaseHandler verwendet.

Implementieren Sie den Handler für Käufe ohne Abo:

lib/google_play_purchase_handler.dart

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

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

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

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

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

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

Der Handler für den Abokauf lässt sich auf ähnliche Weise aktualisieren:

lib/google_play_purchase_handler.dart

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

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

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

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

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

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

Fügen Sie die folgende Methode hinzu, um das Parsen von Bestell-IDs sowie zwei Methoden zum Parsen des Kaufstatus zu erleichtern.

lib/google_play_purchase_handler.dart

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

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

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

Ihre Google Play-Käufe sollten jetzt bestätigt und in der Datenbank gespeichert sein.

Fahren Sie als Nächstes mit den App Store-Käufen für iOS fort.

iOS-Käufe bestätigen: Kauf-Handler implementieren

Für die Verifizierung von Käufen im App Store gibt es ein Drittanbieter-Dart-Paket mit dem Namen app_store_server_sdk, das den Vorgang vereinfacht.

Erstellen Sie zuerst die Instanz ITunesApi. Verwenden Sie die Sandbox-Konfiguration und aktivieren Sie das Logging, um die Fehlerbehebung zu erleichtern.

lib/app_store_purchase_handler.dart

  final _iTunesAPI = ITunesApi(
    ITunesHttpClient(
      ITunesEnvironment.sandbox(),
      loggingEnabled: true,
    ),
  );

Jetzt verwendet der App Store im Gegensatz zu den Google Play-APIs dieselben API-Endpunkte für Abos und andere Nutzer. Das bedeutet, dass Sie für beide Handler dieselbe Logik verwenden können. Führen Sie sie zusammen, sodass sie dieselbe Implementierung aufrufen:

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 {
   //..
  }

Implementieren Sie nun 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;
    }
  }

Ihre App Store-Käufe sollten jetzt bestätigt und in der Datenbank gespeichert sein.

Back-End ausführen

Jetzt können Sie dart bin/server.dart ausführen, um den Endpunkt /verifypurchase bereitzustellen.

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

11. Käufe im Blick behalten

Die empfohlene Methode zum Erfassen der Käufe befindet sich im Back-End-Dienst. Das liegt daran, dass Ihr Back-End auf Ereignisse aus dem Speicher reagieren kann und somit weniger anfällig für veraltete Informationen durch Caching ist. Außerdem ist es weniger anfällig für Manipulationen.

Richten Sie zuerst die Verarbeitung von Ladenereignissen auf dem Back-End mit dem Dart-Back-End ein, das Sie erstellt haben.

Speicherereignisse auf dem Back-End verarbeiten

Geschäfte können Ihr Backend über alle Abrechnungsereignisse informieren, z. B. wenn Abos verlängert werden. Sie können diese Ereignisse in Ihrem Backend verarbeiten, um die Käufe in Ihrer Datenbank auf dem neuesten Stand zu halten. In diesem Abschnitt können Sie dies sowohl für den Google Play Store als auch für den Apple App Store einrichten.

Google Play Billing-Ereignisse verarbeiten

Google Play stellt Abrechnungsereignisse über sogenannte Cloud Pub/Sub-Themen bereit. Dies sind im Wesentlichen Nachrichtenwarteschlangen, in denen Nachrichten veröffentlicht und aus denen sie abgerufen werden können.

Da diese Funktion spezifisch für Google Play ist, nehmen Sie diese Funktion in die GooglePlayPurchaseHandler auf.

Öffnen Sie zuerst lib/google_play_purchase_handler.dart und fügen Sie den PubsubApi-Import hinzu:

lib/google_play_purchase_handler.dart

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

Übergeben Sie dann PubsubApi an GooglePlayPurchaseHandler und ändern Sie den Klassenkonstruktor so, dass ein Timer erstellt wird:

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 ist so konfiguriert, dass die Methode _pullMessageFromSubSub alle zehn Sekunden aufgerufen wird. Sie können die Dauer nach Bedarf anpassen.

Erstellen Sie dann den _pullMessageFromSubSub.

lib/google_play_purchase_handler.dart

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

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

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

Der soeben hinzugefügte Code kommuniziert alle zehn Sekunden mit dem Pub/Sub-Thema von Google Cloud und fragt nach neuen Nachrichten. Anschließend wird jede Nachricht in der Methode _processMessage verarbeitet.

Mit dieser Methode werden die eingehenden Nachrichten decodiert und die aktualisierten Informationen zu jedem Kauf (sowohl Abos als auch Nicht-Abos) werden abgerufen, wobei bei Bedarf die vorhandene handleSubscription oder handleNonSubscription aufgerufen wird.

Jede Nachricht muss mit der Methode _askMessage bestätigt werden.

Als Nächstes fügen Sie der Datei server.dart die erforderlichen Abhängigkeiten hinzu. Fügen Sie der Konfiguration der Anmeldedaten den PubsubApi.cloudPlatformScope hinzu:

bin/server.dart

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

Erstellen Sie dann die PubsubApi-Instanz:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Übergeben Sie es abschließend an den GooglePlayPurchaseHandler-Konstruktor:

bin/server.dart

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

Google Play einrichten

Sie haben den Code zur Verarbeitung von Abrechnungsereignissen aus dem Pub/Sub-Thema geschrieben, aber weder das Pub/Sub-Thema erstellt noch veröffentlichen Sie Abrechnungsereignisse. Jetzt ist es Zeit für die Einrichtung.

Erstellen Sie zuerst ein Pub/Sub-Thema:

  1. Rufen Sie in der Google Cloud Console die Cloud Pub/Sub-Seite auf.
  2. Achten Sie darauf, dass Sie sich in Ihrem Firebase-Projekt befinden, und klicken Sie auf + Thema erstellen. d5ebf6897a0a8bf5.png
  3. Geben Sie dem neuen Thema einen Namen, der mit dem Wert identisch ist, der in constants.ts für GOOGLE_PLAY_PUBSUB_BILLING_TOPIC festgelegt wurde. Nennen Sie es in diesem Fall play_billing. Wenn du etwas anderes auswählst, aktualisiere constants.ts. Erstellen Sie das Thema. 20d690fc543c4212.png
  4. Klicken Sie in der Liste der Pub/Sub-Themen neben dem gerade erstellten Thema auf das Dreipunkt-Menü und dann auf Berechtigungen ansehen. ea03308190609fb.png
  5. Wählen Sie in der rechten Seitenleiste Hauptkonto hinzufügen aus.
  6. Fügen Sie hier google-play-developer-notifications@system.gserviceaccount.com hinzu und weisen Sie ihm die Rolle Pub/Sub-Publisher zu. 55631ec0549215bc.png
  7. Speichern Sie die Änderungen an den Berechtigungen.
  8. Kopieren Sie den Namen des Themas, das Sie gerade erstellt haben.
  9. Öffnen Sie die Play Console wieder und wählen Sie Ihre App aus der Liste Alle Apps aus.
  10. Scrolle nach unten und gehe zu Monetarisieren > Einrichtung der Monetarisierung:
  11. Füllen Sie das vollständige Thema aus und speichern Sie Ihre Änderungen. 7e5e875dc6ce5d54.png

Alle Google Play Billing-Ereignisse werden jetzt zu diesem Thema veröffentlicht.

App Store-Abrechnungsereignisse verarbeiten

Wiederhole den Vorgang für die Abrechnungsereignisse im App Store. Es gibt zwei effektive Möglichkeiten, die Verarbeitung von Updates bei Käufen für den App Store zu implementieren. Eine Möglichkeit besteht darin, einen Webhook zu implementieren, den Sie für Apple bereitstellen und der für die Kommunikation mit Ihrem Server verwendet wird. Die zweite Möglichkeit, die Sie in diesem Codelab finden, besteht darin, eine Verbindung zur App Store Server API herzustellen und die Aboinformationen manuell abzurufen.

Dieses Codelab konzentriert sich hauptsächlich auf die zweite Lösung, weil Sie Ihren Server zum Implementieren des Webhooks im Internet verfügbar machen müssen.

In einer Produktionsumgebung würden Sie idealerweise beides haben. Der Webhook, um Ereignisse aus dem App Store und der Server API abzurufen, falls Sie ein Ereignis verpasst haben oder den Abostatus noch einmal prüfen müssen.

Öffnen Sie zuerst lib/app_store_purchase_handler.dart und fügen Sie die AppStoreServerAPI-Abhängigkeit hinzu:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

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

Fügen Sie dem Konstruktor einen Timer hinzu, der die Methode _pullStatus aufruft. Dieser Timer ruft alle 10 Sekunden die Methode _pullStatus auf. Sie können die Dauer des Timers an Ihre Bedürfnisse anpassen.

lib/app_store_purchase_handler.dart

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,
  ) {
    // Poll Subscription status every 10 seconds.
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullStatus();
    });
  }

Erstellen Sie dann die _pullStatus-Methode wie folgt:

lib/app_store_purchase_handler.dart

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

Diese Methode funktioniert so:

  1. Ruft die Liste der aktiven Abos aus Firestore mithilfe des IapRepository ab.
  2. Für jede Bestellung wird der Abostatus an die App Store Server API gesendet.
  3. Erhält die letzte Transaktion für diesen Abonnementkauf.
  4. Prüft das Ablaufdatum.
  5. Aktualisiert den Abostatus in Firestore. Wenn das Abo abgelaufen ist, wird es entsprechend markiert.

Fügen Sie abschließend den gesamten erforderlichen Code zum Konfigurieren des App Store Server-API-Zugriffs hinzu:

bin/server.dart

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

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

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

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


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

App Store einrichten

Als Nächstes richten Sie den App Store ein:

  1. Melden Sie sich in App Store Connect an und wählen Sie Users and Access (Nutzer und Zugriff) aus.
  2. Gehen Sie zu Schlüsseltyp > In-App-Käufe:
  3. Tippen Sie auf das Pluszeichen. um ein neues hinzuzufügen.
  4. Geben Sie einen Namen ein, z.B. „Codelab-Schlüssel“.
  5. Laden Sie die p8-Datei herunter, die den Schlüssel enthält.
  6. Kopieren Sie es in den Asset-Ordner mit dem Namen SubscriptionKey.p8.
  7. Kopieren Sie die Schlüssel-ID aus dem neu erstellten Schlüssel und setzen Sie sie in der Datei lib/constants.dart auf die Konstante appStoreKeyId.
  8. Kopieren Sie die Aussteller-ID ganz oben in der Schlüsselliste und setzen Sie sie in der Datei lib/constants.dart auf die Konstante appStoreIssuerId.

9540ea9ada3da151.png

Käufe auf dem Gerät verfolgen

Die sicherste Methode, Ihre Käufe zu verfolgen, ist serverseitig, da der Client schwer zu sichern ist. Sie müssen jedoch eine Möglichkeit haben, die Informationen an den Client zurückzusenden, damit die App auf die Informationen zum Abostatus reagieren kann. Durch das Speichern der Käufe in Firestore können Sie die Daten ganz einfach mit dem Client synchronisieren und sie automatisch aktualisieren lassen.

Sie haben IAPRepo bereits in die Anwendung eingefügt. Dies ist das Firestore-Repository, das alle Kaufdaten des Nutzers in List<PastPurchase> purchases enthält. Das Repository enthält außerdem hasActiveSubscription,, was „true“ ist, wenn es einen Kauf mit productId storeKeySubscription mit einem Status gibt, der nicht abgelaufen ist. Wenn der Nutzer nicht angemeldet ist, ist die Liste leer.

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((DocumentSnapshot document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

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

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

      notifyListeners();
    });
  }

Die gesamte Kauflogik befindet sich in der Klasse DashPurchases. Hier sollten Abos angewendet oder entfernt werden. Fügen Sie der Klasse also das iapRepo als Attribut hinzu und weisen Sie das iapRepo im Konstruktor zu. Als Nächstes fügen Sie dem Konstruktor direkt einen Listener hinzu und entfernen den Listener in der Methode dispose(). Zunächst kann der Listener einfach eine leere Funktion sein. Da die IAPRepo ein ChangeNotifier ist und Sie jedes Mal notifyListeners() aufrufen, wenn sich die Käufe in Firestore ändern, wird die Methode purchasesUpdate() immer aufgerufen, wenn sich die gekauften Produkte ändern.

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

Geben Sie als Nächstes das IAPRepo an den Konstruktor in main.dart.. Sie können das Repository mit context.read abrufen, da es bereits in einem Provider erstellt wurde.

lib/main.dart

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

Schreiben Sie als Nächstes den Code für die Funktion purchaseUpdate(). In dash_counter.dart, setzen die Methoden applyPaidMultiplier und removePaidMultiplier den Multiplikator auf 10 bzw. 1, sodass du nicht prüfen musst, ob das Abo bereits angewendet wird. Wenn sich der Abostatus ändert, aktualisieren Sie auch den Status des käuflichen Produkts, sodass auf der Kaufseite angezeigt wird, dass es bereits aktiv ist. Legen Sie das Attribut _beautifiedDashUpgrade abhängig davon fest, ob das Upgrade gekauft wird.

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

Sie haben nun dafür gesorgt, dass der Abo- und Upgradestatus im Backend-Dienst immer aktuell ist und mit der App synchronisiert wird. Die App verhält sich entsprechend und wendet die Abo- und Upgrade-Funktionen auf Ihr Dash-Clicker-Spiel an.

12. Fertig

Herzlichen Glückwunsch!!! Du hast das Codelab abgeschlossen. Den fertigen Code für dieses Codelab findest du im Ordner android_studio_folder.pngVollständig.

Weitere Informationen finden Sie in den anderen Flutter-Codelabs.