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

1. Einführung

Wenn Sie einer Flutter-App In-App-Käufe hinzufügen möchten, müssen Sie den App Store und den Play Store richtig einrichten, den Kauf bestätigen und die erforderlichen Berechtigungen wie Abo-Vorteile gewähren.

In diesem Codelab fügen Sie einer App (die Ihnen zur Verfügung gestellt wird) drei Arten von In-App-Käufen hinzu und prüfen diese Käufe mit einem Dart-Backend mit Firebase. Die bereitgestellte App „Dash Clicker“ enthält ein Spiel, in dem das Dash-Maskottchen als Währung verwendet wird. Sie fügen die folgenden Kaufoptionen hinzu:

  1. Eine wiederholbare Kaufoption für 2.000 Dashes auf einmal.
  2. Einmaliger Kauf eines Upgrades, um das alte Dash in ein modernes Dash umzuwandeln.
  3. Ein Abo, mit dem sich die Anzahl der automatisch generierten Klicks verdoppelt.

Bei der ersten Kaufoption erhält der Nutzer direkt 2.000 Dashes. Diese sind direkt für den Nutzer verfügbar und können mehrmals gekauft werden. Es wird als „Verbrauchsgut“ bezeichnet, da es direkt und mehrmals verwendet werden kann.

Die zweite Option aktualisiert das Dash zu einem schöneren Dash. Sie müssen es nur einmal kaufen und es ist dann für immer verfügbar. Ein solcher Kauf wird als „nicht verbrauchbar“ bezeichnet, da er nicht von der App verbraucht werden kann, sondern für immer gültig ist.

Die dritte und letzte Kaufoption ist ein Abo. Solange das Abo aktiv ist, erhält der Nutzer schneller Dashes. Wenn er das Abo jedoch kündigt, fallen die Vorteile weg.

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

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

Aufgaben

  • Sie erweitern eine App, um den Kauf von Verbrauchsmaterialien und Abos zu unterstützen.
  • Außerdem erweitern Sie eine Dart-Backend-App, um die gekauften Artikel zu überprüfen und zu speichern.

Lerninhalte

  • So konfigurieren Sie den App Store und den Play Store mit kaufbaren Produkten.
  • Mit den Stores kommunizieren, um Käufe zu bestätigen und in Firestore zu speichern
  • So verwalten Sie Käufe in Ihrer App.

Voraussetzungen

  • Android Studio
  • Xcode (für die iOS-Entwicklung)
  • Flutter SDK

2. Entwicklungsumgebung einrichten

Laden Sie den Code herunter und ändern Sie den Paket-Identifikator für iOS und den Paketnamen für Android, um mit diesem Codelab zu beginnen.

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 CLI-Tool von GitHub installiert haben, verwenden Sie den folgenden Befehl:

gh repo clone flutter/codelabs flutter-codelabs

Der Beispielcode wird in ein Verzeichnis flutter-codelabs 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, die zeigen, wie die Dateien am Ende jedes benannten Schritts aussehen sollten. Der Startcode befindet sich in Schritt 0. So rufen Sie ihn auf:

cd flutter-codelabs/in_app_purchases/step_00

Wenn Sie einen Schritt überspringen oder sehen möchten, wie etwas nach einem Schritt aussehen sollte, suchen Sie im Verzeichnis, das nach dem entsprechenden Schritt benannt ist. Der Code des letzten Schritts befindet sich im Ordner complete.

Startprojekt einrichten

Öffnen Sie das Starterprojekt aus step_00/app in Ihrer bevorzugten IDE. Für die Screenshots haben wir Android Studio verwendet, aber Visual Studio Code ist auch eine gute Option. Achten Sie darauf, dass in beiden Editoren die neuesten Dart- und Flutter-Plug-ins installiert sind.

Die Apps, die Sie erstellen, müssen mit dem App Store und dem Play Store kommunizieren, um zu erfahren, welche Produkte zu welchem Preis verfügbar sind. Jede App wird durch eine eindeutige ID identifiziert. Im iOS App Store wird sie als Paket-ID und im Android Play Store als Anwendungs-ID bezeichnet. Diese IDs werden in der Regel mit einer umgekehrten Domainnamenschreibweise erstellt. Wenn Sie beispielsweise eine In-App-Kauf-App für flutter.dev erstellen, verwenden Sie dev.flutter.inapppurchase. Überlegen Sie sich eine Kennung für Ihre App, die Sie jetzt in den Projekteinstellungen festlegen.

Richten Sie zuerst die Paket-ID für iOS ein. Öffnen Sie dazu die Datei Runner.xcworkspace in der Xcode App.

a9fbac80a31e28e0.png

In der Ordnerstruktur von Xcode befindet sich das Runner-Projekt oben und die Ziele Flutter, Runner und Products befinden sich unter dem Runner-Projekt. Doppelklicken Sie auf Runner, um die Projekteinstellungen zu bearbeiten, und klicken Sie auf Signieren & Funktionen. Geben Sie die gerade ausgewählte Bundle-ID in das Feld Team ein, um Ihr Team festzulegen.

812f919d965c649a.jpeg

Sie können Xcode jetzt schließen und zu Android Studio zurückkehren, um die Konfiguration für Android abzuschließen. Öffnen Sie dazu die Datei build.gradle.kts unter android/app, und ändern Sie applicationId (in der Abbildung unten in Zeile 24) in die Anwendungs-ID, die mit der iOS-Paket-ID identisch ist. Die IDs für die iOS- und Android-Stores müssen nicht identisch sein. Es ist jedoch weniger fehleranfällig, wenn sie identisch sind. Daher verwenden wir in diesem Codelab auch identische IDs.

e320a49ff2068ac2.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 der pubspec-Datei hinzu, indem Sie in_app_purchase den Abhängigkeiten Ihres Projekts hinzufügen:

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

Öffnen Sie pubspec.yaml und prüfen Sie, ob in_app_purchase jetzt als Eintrag unter dependencies und in_app_purchase_platform_interface unter dev_dependencies aufgeführt ist.

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. App Store einrichten

Wenn Sie In-App-Produkte einrichten und auf iOS-Geräten testen möchten, müssen Sie eine neue App im App Store erstellen und dort kaufbare Produkte anlegen. Sie müssen nichts veröffentlichen oder die App zur Überprüfung an Apple senden. Dazu benötigen Sie ein Entwicklerkonto. Falls Sie noch keine haben, melden Sie sich für das Apple Developer Program an.

Wenn Sie In-App-Käufe verwenden möchten, benötigen Sie außerdem eine aktive Vereinbarung für kostenpflichtige Apps in App Store Connect. Rufen Sie https://appstoreconnect.apple.com/ auf und klicken Sie auf Agreements, Tax, and Banking (Vereinbarungen, Steuern und Bankwesen).

11db9fca823e7608.png

Hier werden Vereinbarungen für kostenlose und kostenpflichtige Apps angezeigt. Der Status kostenloser Apps sollte „Aktiv“ sein und der Status kostenpflichtiger Apps „Neu“. Lesen Sie die Nutzungsbedingungen, akzeptieren Sie sie und geben Sie alle erforderlichen Informationen ein.

74c73197472c9aec.png

Wenn alles richtig eingestellt ist, ist der Status für kostenpflichtige Apps „Aktiv“. Das 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 Developer-Portal eine neue Kennung. Rufen Sie developer.apple.com/account/resources/identifiers/list auf und klicken Sie neben der Überschrift Identifiers auf das Pluszeichen.

55d7e592d9a3fc7b.png

App-IDs auswählen

13f125598b72ca77.png

App auswählen

41ac4c13404e2526.png

Geben Sie eine Beschreibung an und legen Sie die Paket-ID auf denselben Wert fest, der zuvor in Xcode festgelegt wurde.

9d2c940ad80deeef.png

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

Neue App erstellen

Erstellen Sie in App Store Connect eine neue App mit Ihrer eindeutigen Bundle-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 verknüpft 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 unter Users and Access (Nutzer und Zugriff) zu Sandbox, um ein neues Sandbox-Konto zu erstellen oder die vorhandenen Sandbox-Apple-IDs zu verwalten.

2ba0f599bcac9b36.png

Jetzt können Sie Ihren Sandbox-Nutzer auf Ihrem iPhone einrichten. Gehen Sie dazu zu Einstellungen > Entwickler > Sandbox-Apple-Konto.

74a545210b282ad8.png eaa67752f2350f74.png

In-App-Käufe konfigurieren

Jetzt konfigurieren Sie die drei kaufbaren Artikel:

  • dash_consumable_2k: Ein Verbrauchsgut, das mehrmals gekauft werden kann und dem Nutzer pro Kauf 2.000 Dashes (die In-App-Währung) gewährt.
  • dash_upgrade_3d: Ein nicht verbrauchbarer „Upgrade“-Kauf, der nur einmal gekauft werden kann und dem Nutzer einen kosmetisch anderen Dash zum Klicken bietet.
  • dash_subscription_doubler: Ein Abo, das dem Nutzer für die Dauer des Abos doppelt so viele Striche pro Klick gewährt.

a118161fac83815a.png

Rufen Sie In-App-Käufe auf.

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

  1. Richten Sie dash_consumable_2k als Verbrauchsmaterial ein. Verwenden Sie dash_consumable_2k als Produkt-ID. Der Referenzname wird nur in App Store Connect verwendet. Legen Sie ihn einfach auf dash consumable 2k fest. 1f8527fc03902099.png Verfügbarkeit einrichten Das Produkt muss im Land des Sandbox-Nutzers verfügbar sein. bd6b2ce2d9314e6e.png Fügen Sie Preise hinzu und legen Sie den Preis auf $1.99 oder den entsprechenden Betrag in einer anderen Währung fest. 926b03544ae044c4.png Fügen Sie die Übersetzungen für den Kauf hinzu. Rufen Sie den Kauf Spring is in the air mit 2000 dashes fly out als Beschreibung auf. e26dd4f966dcfece.png Fügen Sie einen Screenshot der Rezension hinzu. Der Inhalt spielt keine Rolle, es sei denn, das Produkt wird zur Überprüfung gesendet. Er ist jedoch erforderlich, damit das Produkt den Status „Bereit zum Einreichen“ erhält. Das ist notwendig, wenn die App Produkte aus dem App Store abruft. 25171bfd6f3a033a.png
  2. Richten Sie dash_upgrade_3d als nicht verbrauchbares Produkt ein. Verwenden Sie dash_upgrade_3d als Produkt-ID. Legen Sie dash upgrade 3d als Referenzname fest. Rufen Sie den Kauf 3D Dash mit Brings your dash back to the future als Beschreibung auf. Legen Sie den Preis auf $0.99 fest. Konfigurieren Sie die Verfügbarkeit und laden Sie den Screenshot der Rezension auf dieselbe Weise wie für das Produkt dash_consumable_2k hoch. 83878759f32a7d4a.png
  3. Richten Sie dash_subscription_doubler als Abo mit automatischer Verlängerung ein. Der Ablauf für Abos ist etwas anders. Zuerst müssen Sie eine Abogruppe erstellen. Wenn mehrere Abos zur selben Gruppe gehören, kann ein Nutzer nur eines davon gleichzeitig abonnieren. Er kann aber zwischen diesen Abos wechseln. Nenne diese Gruppe einfach subscriptions. 393a44b09f3cd8bf.png Fügen Sie der Abo-Gruppe Lokalisierungen hinzu. 595aa910776349bd.png Als Nächstes erstellen Sie das Abo. Legen Sie den Referenznamen auf dash subscription doubler und die Produkt-ID auf dash_subscription_doubler fest. 7bfff7bbe11c8eec.png Wählen Sie als Nächstes die Abodauer von einer Woche und die Lokalisierungen aus. Nenne dieses Abo Jet Engine mit der Beschreibung Doubles your clicks. Legen Sie den Preis auf $0.49 fest. Konfigurieren Sie die Verfügbarkeit und laden Sie den Screenshot der Rezension auf dieselbe Weise wie für das Produkt dash_consumable_2k hoch. 44d18e02b926a334.png

Die Produkte sollten jetzt in den Listen angezeigt werden:

17f242b5c1426b79.png d71da951f595054a.png

5. Play Store einrichten

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

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 aus.
  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 Anwendung um ein Spiel handelt. Sie können dies später ändern.
  5. Geben Sie an, ob Ihre App kostenlos oder kostenpflichtig ist.
  6. Machen Sie die erforderlichen Angaben für die Deklarationen „Inhaltsrichtlinien“ und „Exportbestimmungen der USA“.
  7. Wählen Sie App erstellen aus.

Nachdem Sie Ihre App erstellt haben, rufen Sie das Dashboard auf und erledigen Sie alle Aufgaben im Bereich App einrichten. Hier geben Sie einige Informationen zu Ihrer App an, z. B. die Altersfreigabe und Screenshots. 13845badcf9bc1db.png

Anwendung signieren

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

Dazu muss Ihr Release-Build mit einem anderen Schlüssel als den Debug-Schlüsseln signiert sein.

Schlüsselspeicher erstellen

Wenn Sie bereits einen Keystore haben, fahren Sie mit dem nächsten Schritt fort. Falls nicht, erstellen Sie eine, indem Sie den folgenden Befehl in der Befehlszeile 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. Behalten

keystore

Datei privat halten und nicht in die öffentliche Versionsverwaltung einchecken!

Auf den Schlüsselspeicher über die App verweisen

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>

Signierung in Gradle konfigurieren

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

Fügen Sie die Keystore-Informationen aus Ihrer Properties-Datei vor dem android-Block hinzu:

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
}

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

Aktualisieren Sie den Block buildTypes auf:

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

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

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

Release-Builds Ihrer App werden jetzt automatisch signiert.

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

Ersten Build hochladen

Nachdem Ihre App für die Signierung konfiguriert wurde, sollten Sie Ihre Anwendung mit dem folgenden Befehl erstellen können:

flutter build appbundle

Mit diesem Befehl wird standardmäßig ein Release-Build generiert. Die Ausgabe befindet sich unter <your app dir>/build/app/outputs/bundle/release/.

Rufen Sie im Dashboard der Google Play Console Test und Release > Test > Geschlossener Test auf und erstellen Sie einen neuen Release für den geschlossenen Test.

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

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

Klicken Sie abschließend auf Einführung in den geschlossenen Test starten, um den Release für den geschlossenen Test zu aktivieren.

Testnutzer einrichten

Damit Ihre Tester In-App-Käufe testen können, müssen ihre Google-Konten an zwei Stellen in der Google Play Console hinzugefügt werden:

  1. Auf den jeweiligen Test-Track (interner Test)
  2. Als Lizenztester

Fügen Sie den Tester zuerst dem internen Test-Track hinzu. Kehren Sie zu Testen und veröffentlichen > Testen > Interner Test zurück und klicken Sie auf den Tab Tester.

a0d0394e85128f84.png

Klicken Sie auf E-Mail-Liste erstellen, um eine neue E-Mail-Liste zu erstellen. Geben Sie der Liste einen Namen und fügen Sie die E-Mail-Adressen der Google-Konten hinzu, die Zugriff auf In-App-Testkäufe benötigen.

Klicken Sie dann das Kästchen für die Liste an und wählen Sie Änderungen speichern aus.

Fügen Sie dann die Lizenztester hinzu:

  1. Kehren Sie in der Google Play Console zur Ansicht Alle Apps zurück.
  2. Rufen Sie Einstellungen > Lizenztest auf.
  3. Fügen Sie die E-Mail-Adressen der Tester hinzu, die In-App-Käufe testen müssen.
  4. Legen Sie Lizenzantwort auf RESPOND_NORMALLY fest.
  5. Klicken Sie auf ROLLE ERSTELLEN.

a1a0f9d3e55ea8da.png

In-App-Käufe konfigurieren

Als Nächstes konfigurieren Sie die Artikel, die in der App gekauft werden können.

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

  • dash_consumable_2k: Ein Verbrauchsgut, das mehrmals gekauft werden kann und dem Nutzer pro Kauf 2.000 Dashes (die In-App-Währung) gewährt.
  • dash_upgrade_3d: Ein nicht verbrauchbarer „Upgrade“-Kauf, der nur einmal gekauft werden kann und dem Nutzer ein kosmetisch anderes Dash zum Klicken bietet.
  • dash_subscription_doubler: Ein Abo, das dem Nutzer für die Dauer des Abos doppelt so viele Striche pro Klick gewährt.

Fügen Sie zuerst das Einmalprodukt und das nicht verbrauchbare Produkt hinzu.

  1. Rufen Sie die Google Play Console auf und wählen Sie Ihre Anwendung aus.
  2. Rufen Sie Monetarisieren > Produkte > In‑App-Produkte auf.
  3. Klicken Sie auf Produkt erstellenc8d66e32f57dee21.png
  4. Geben Sie alle erforderlichen Informationen zu Ihrem 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 den nicht verbrauchbaren Kauf „Upgrade“.

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

  1. Rufen Sie die Google Play Console auf und wählen Sie Ihre Anwendung aus.
  2. Rufen Sie Monetarisieren > Produkte > Abos auf.
  3. Klicken Sie auf Abo erstellen.32a6a9eefdb71dd0.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.

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

6. Firebase einrichten

In diesem Codelab verwenden Sie einen Back-End-Dienst, um die Käufe von Nutzern zu bestätigen und nachzuverfolgen.

Die Verwendung eines Backend-Dienstes hat 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 erfassen.
  • Nutzer können Ihre App nicht dazu bringen, Premium-Funktionen bereitzustellen, indem sie die Systemuhr zurückstellen.

Es gibt viele Möglichkeiten, einen Back-End-Dienst einzurichten. In diesem Fall verwenden Sie Cloud Functions und Firestore mit Firebase von Google.

Das Schreiben des Back-Ends wird in diesem Codelab nicht behandelt. Der Startcode enthält bereits ein Firebase-Projekt, das grundlegende Käufe abwickelt.

Firebase-Plug-ins sind ebenfalls in der Starter-App enthalten.

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

Firebase-Projekt erstellen

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

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

  1. Rufen Sie im Firebase-Dashboard Authentifizierung auf und aktivieren Sie die Funktion bei Bedarf.
  2. Rufen Sie den Tab Anmeldemethode auf und aktivieren Sie den Anmeldeanbieter Google.

fe2e0933d6810888.png

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

d02d641821c71e2c.png

Cloud Firestore-Regeln festlegen:

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 für 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.

Wählen Sie beim Ausführen von „flutterfire configure“ das Projekt aus, das Sie gerade 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 dann 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 gefragt werden, ob Sie „firebase_options.dart“ überschreiben möchten, wählen Sie „Ja“ aus.

? 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

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

Scrolle nach unten zu Meine Apps und wähle die App dashclicker (android) aus.

b22d46a759c0c834.png

Damit Google Sign-in im Debug-Modus verwendet werden kann, müssen Sie den SHA-1-Hash-Fingerabdruck Ihres Debug-Zertifikats angeben.

Hash des Debugging-Signaturzertifikats abrufen

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

cd android
./gradlew :app:signingReport

Es wird eine lange Liste mit Signaturschlüsseln angezeigt. Da Sie nach dem Hash für das Debug-Zertifikat suchen, suchen Sie nach dem Zertifikat, bei dem die Eigenschaften Variant und Config auf debug festgelegt sind. Der Keystore befindet sich wahrscheinlich in Ihrem Home-Ordner 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 Modal-Dialogfeld für die App-Einreichung aus.

Führen Sie zum Schluss den Befehl flutterfire configure noch einmal aus, um die App zu aktualisieren und die Signaturkonfiguration einzuschließen.

$ 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

Firebase für iOS einrichten: Weitere Schritte

Öffnen Sie ios/Runner.xcworkspace mit Xcode. Oder mit der IDE Ihrer Wahl.

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

Klicken Sie in Android Studio mit der rechten Maustaste auf den Ordner ios/ und dann auf flutter, gefolgt von der Option open iOS module in Xcode.

Damit das Google-Log-in unter iOS möglich ist, fügen Sie die Konfigurationsoption CFBundleURLTypes Ihren Build-plist-Dateien hinzu. Weitere Informationen finden Sie in der Dokumentation zum google_sign_in-Paket. In diesem Fall lautet die Datei ios/Runner/Info.plist.

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

  1. Rufen Sie den Wert für REVERSED_CLIENT_ID aus der Datei GoogleService-Info.plist ab, ohne das umgebende <string>..</string>-Element.
  2. Ersetzen Sie den Wert in Ihrer ios/Runner/Info.plist-Datei 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 nun abgeschlossen.

7. Auf Kaufaktualisierungen warten

In diesem Teil des Codelabs bereiten Sie die App für den Kauf der Produkte vor. Dazu gehört, dass nach dem Start der App auf Kaufaktualisierungen und Fehler gewartet wird.

Kauf-Updates anhören

Suchen Sie in main.dart, nach dem Widget MyHomePage mit einem Scaffold, das ein BottomNavigationBar mit zwei Seiten enthält. Auf dieser Seite werden auch drei Providers für DashCounter, DashUpgrades, und DashPurchases erstellt. DashCounter verfolgt die aktuelle Anzahl der Striche und erhöht sie automatisch. DashUpgrades verwaltet die Upgrades, die Sie mit Dashes kaufen können. In diesem Codelab geht es um DashPurchases.

Standardmäßig wird das Objekt eines Anbieters definiert, wenn es zum ersten Mal angefordert wird. Dieses Objekt wartet direkt beim Start der App auf Kaufaktualisierungen. Deaktivieren Sie daher das Lazy Loading für dieses Objekt mit lazy: false:

lib/main.dart

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

Außerdem benötigen Sie eine Instanz von InAppPurchaseConnection. Damit die App jedoch testbar bleibt, müssen Sie die Verbindung irgendwie simulieren. Erstellen Sie dazu eine Instanzmethode, die im Test überschrieben werden kann, und fügen Sie sie 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!;
  }
}

Aktualisieren Sie den Test so:

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.

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

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

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

Das Keyword late wird _subscription hinzugefügt, weil _subscription im Konstruktor initialisiert wird. Dieses Projekt ist standardmäßig so eingerichtet, dass Nullwerte nicht zulässig sind (non-nullable by default, NNBD). Das bedeutet, dass Eigenschaften, die nicht als nullable deklariert sind, einen Wert ungleich null haben müssen. Mit dem Qualifikator late können Sie die Definition dieses Werts verzögern.

Rufen Sie im Konstruktor den purchaseUpdated-Stream ab und beginnen Sie, auf den Stream zu warten. Kündigen Sie im Rahmen der Methode dispose() das Stream-Abo.

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

Die App erhält jetzt die Kaufaktualisierungen. Im nächsten Abschnitt werden Sie einen Kauf tätigen.

Führen Sie vor dem Fortfahren die Tests mit flutter test" aus, um zu prüfen, ob alles richtig eingerichtet ist.

$ flutter test

00:01 +1: All tests passed!

8. Einkaufen

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

PurchasableProduct anpassen

PurchasableProduct zeigt ein Mock-Produkt. Aktualisieren Sie die Klasse PurchasableProduct in purchasable_product.dart mit dem folgenden Code, damit tatsächliche Inhalte angezeigt werden:

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

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

Verfügbare Käufe laden

Damit ein Nutzer einen Kauf tätigen kann, müssen Sie die Käufe aus dem Store laden. Prüfen Sie zuerst, ob der Store verfügbar ist. Wenn der Store nicht verfügbar ist, wird dem Nutzer eine Fehlermeldung angezeigt, wenn storeState auf notAvailable festgelegt ist.

lib/logic/dash_purchases.dart

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

Wenn der Store verfügbar ist, laden Sie die verfügbaren Käufe. Bei der bisherigen Einrichtung von Google Play und App Store sollten storeKeyConsumable, storeKeySubscription, und storeKeyUpgrade angezeigt werden. Wenn ein erwarteter Kauf nicht verfügbar ist, geben Sie diese Informationen in der Konsole aus. Möglicherweise möchten Sie diese Informationen auch an den Backend-Dienst senden.

Die Methode await iapConnection.queryProductDetails(ids) gibt sowohl die IDs, die nicht gefunden wurden, als auch die gefundenen kaufbaren Produkte zurück. Verwenden Sie den Wert productDetails aus der Antwort, um die Benutzeroberfläche 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);
    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();                                       // Add this line
  }

Ändern Sie schließlich den Wert des Felds storeState von StoreState.available in StoreState.loading:.

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Käuflich erwerbbare Produkte anzeigen

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

Im _PurchaseList-Widget wird die Liste der kaufbaren Produkte angezeigt und eine Kaufanfrage an das DashPurchases-Objekt gesendet.

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 in den Android- und iOS-Stores angezeigt werden. Es kann einige Zeit dauern, bis die Käufe in den jeweiligen Konsolen verfügbar sind.

ca1a9f97c21e552d.png

Gehen Sie zurück zu dash_purchases.dart und implementieren Sie die Funktion zum Kaufen eines Produkts. Sie müssen nur die Verbrauchsmaterialien von den nicht verbrauchbaren Artikeln trennen. Das Upgrade und die Aboprodukte sind nicht verbrauchbar.

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

Bevor Sie fortfahren, erstellen Sie die Variable _beautifiedDashUpgrade und aktualisieren Sie den beautifiedDash-Getter, damit er darauf verweist.

lib/logic/dash_purchases.dart

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

Die _onPurchaseUpdate-Methode empfängt die Kaufaktualisierungen, aktualisiert den Status des Produkts, das auf der Kaufseite angezeigt wird, und wendet den Kauf auf die Zählerlogik an. Es ist wichtig, completePurchase nach der Verarbeitung des Kaufs aufzurufen, damit der Store weiß, dass der Kauf korrekt verarbeitet 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) {
      switch (purchaseDetails.productID) {
        case storeKeySubscription:
          counter.applyPaidMultiplier();
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
      }
    }

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

9. Backend einrichten

Bevor Sie mit dem Tracking und der Bestätigung von Käufen fortfahren, müssen Sie ein Dart-Backend einrichten, das dies unterstützt.

In diesem Abschnitt arbeiten Sie mit dem Ordner dart-backend/ als Stammverzeichnis.

Die folgenden Tools müssen installiert sein:

Übersicht über das Basisprojekt

Da einige Teile dieses Projekts für dieses Codelab nicht relevant sind, sind sie im Startcode enthalten. Es ist eine gute Idee, sich den Startercode anzusehen, bevor Sie loslegen, 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 zu verwenden. Sie müssen jedoch von Ihrem Entwicklungsgerät (Android oder iPhone) aus eine Verbindung 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, um den Kaufbestätigungsprozess zu verarbeiten.

Ein Teil, der bereits im Startcode enthalten ist, ist IapRepository in lib/iap_repository.dart. Da das Erlernen der Interaktion mit Firestore oder Datenbanken im Allgemeinen nicht als relevant für dieses Codelab angesehen wird, 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. Sie können einen solchen Schlüssel generieren, indem Sie die Firebase-Projekteinstellungen öffnen, zum Abschnitt Dienstkonten wechseln und dann Neuen privaten Schlüssel generieren auswählen.

27590fc77ae94ad4.png

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

Google Play-Zugriff einrichten

Wenn Sie auf den Play Store zugreifen möchten, um Käufe zu überprüfen, müssen Sie ein Dienstkonto mit diesen Berechtigungen erstellen und die JSON-Anmeldedaten dafür herunterladen.

  1. Rufen Sie in der Google Cloud Console die Seite Google Play Android Developer API auf. 629f0bd8e6b50be8.png Falls Sie in der Google Play Console aufgefordert werden, ein Projekt zu erstellen oder mit einem vorhandenen Projekt zu verknüpfen, tun Sie das zuerst und kehren Sie dann zu dieser Seite zurück.
  2. Rufen Sie als Nächstes die Seite „Dienstkonten“ auf und klicken Sie auf + Dienstkonto erstellen. 8dc97e3b1262328a.png
  3. Geben Sie den Namen des Dienstkontos ein und klicken Sie auf Erstellen und fortfahren. 4fe8106af85ce75f.png
  4. Wählen Sie die Rolle Pub/Sub-Abonnent aus und klicken Sie auf Fertig. a5b6fa6ea8ee22d.png
  5. Rufen Sie nach dem Erstellen des Kontos Schlüssel verwalten auf. eb36da2c1ad6dd06.png
  6. Wählen Sie Schlüssel hinzufügen > Neuen Schlüssel erstellen aus. e92db9557a28a479.png
  7. Erstellen Sie einen JSON-Schlüssel und laden Sie ihn herunter. 711d04f2f4176333.png
  8. Benennen Sie die heruntergeladene Datei in service-account-google-play.json, um und verschieben Sie sie in das Verzeichnis assets/.
  9. Rufen Sie als Nächstes in der Play Console die Seite Nutzer und Berechtigungen auf.28fffbfc35b45f97.png
  10. Klicken Sie auf Neue Nutzer einladen und geben Sie die E-Mail-Adresse des zuvor erstellten Dienstkontos ein. Die E-Mail-Adresse finden Sie in der Tabelle auf der Seite Dienstkontene3310cc077f397d.png
  11. Erteilen Sie die Berechtigungen Finanzdaten einsehen und Bestellungen und Abos verwalten für die Anwendung. a3b8cf2b660d1900.png
  12. Klicken Sie auf Nutzer einladen.

Außerdem müssen wir lib/constants.dart, öffnen und den Wert von androidPackageId durch die Paket-ID ersetzen, die Sie für Ihre Android-App ausgewählt haben.

Zugriff auf den Apple App Store einrichten

Damit Sie auf den App Store zugreifen können, um Käufe zu bestätigen, müssen Sie ein gemeinsames Secret einrichten:

  1. Öffnen Sie App Store Connect.
  2. Rufen Sie Meine Apps auf und wählen Sie Ihre App aus.
  3. Klicken Sie in der Seitenleistennavigation auf Allgemein > App-Informationen.
  4. Klicken Sie unter der Überschrift App-Specific Shared Secret (App-spezifisches gemeinsames Geheimnis) auf Manage (Verwalten). ad419782c5fbacb2.png
  5. Generieren Sie ein neues Secret und kopieren Sie es. b5b72a357459b0e5.png
  6. Öffnen Sie lib/constants.dart, und ersetzen Sie den Wert von appStoreSharedSecret durch das gemeinsame Secret, das Sie gerade generiert haben.

Konfigurationsdatei für Konstanten

Bevor Sie fortfahren, müssen die folgenden Konstanten in der Datei lib/constants.dart konfiguriert sein:

  • androidPackageId: Paket-ID, die unter Android verwendet wird, z. B. com.example.dashclicker
  • appStoreSharedSecret: Gemeinsames Secret für den Zugriff auf App Store Connect zur Durchführung der Kaufbestätigung.
  • bundleId: Die auf iOS verwendete Bundle-ID, z. B. com.example.dashclicker

Die restlichen 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 ein Token, wenn ein Kauf erfolgt.

Dieses Token wird von der App an Ihren Backend-Dienst gesendet, der den Kauf dann wiederum mit dem bereitgestellten Token auf den Servern des jeweiligen Stores überprüft.

Der Back-End-Dienst kann dann entscheiden, ob er den Kauf speichert, und der Anwendung antworten, ob der Kauf gültig war oder nicht.

Wenn der Backend-Dienst die Validierung mit den Stores durchführt und nicht die Anwendung auf dem Gerät des Nutzers, können Sie verhindern, dass der Nutzer Zugriff auf Premium-Funktionen erhält, indem er z. B. die Systemuhr zurückstellt.

Flutter-Seite einrichten

Authentifizierung einrichten

Da Sie die Käufe an Ihren Backend-Dienst senden, müssen Sie dafür sorgen, dass der Nutzer beim Kauf authentifiziert wird. Die meiste Authentifizierungslogik ist bereits im Starterprojekt enthalten. Sie müssen nur dafür sorgen, dass auf der PurchasePage die Schaltfläche „Anmelden“ angezeigt wird, 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 '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.

    // ...

Endpunkt für die Anrufüberprüfung über die App aufrufen

Erstellen Sie in der App die Funktion _verifyPurchase(PurchaseDetails purchaseDetails), die den Endpunkt /verifypurchase in Ihrem 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 auf die IP-Adresse Ihres lokalen Computers.

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

Fügen Sie firebaseNotifier beim Erstellen von DashPurchases in main.dart: hinzu.

lib/main.dart

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

Fügen Sie im FirebaseNotifier einen Getter für den Nutzer hinzu, damit Sie die Nutzer-ID an die Funktion zum Überprüfen des Kaufs übergeben können.

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

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) {
      return true;
    } else {
      return false;
    }
  }

Rufen Sie die Funktion _verifyPurchase in _handlePurchase auf, kurz bevor Sie den Kauf anwenden. Sie sollten den Kauf erst anwenden, wenn er bestätigt wurde. In einer Produktions-App können Sie dies weiter angeben, um beispielsweise ein Probeabo anzuwenden, wenn der Store vorübergehend nicht verfügbar ist. In diesem Beispiel wird der Kauf jedoch erst angewendet, wenn er 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();
          case storeKeyConsumable:
            counter.addBoughtDashes(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

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

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

Backend-Dienst einrichten

Richten Sie als Nächstes das Back-End zum Bestätigen von Käufen ein.

Kauf-Handler erstellen

Da der Bestätigungsablauf für beide Stores nahezu identisch ist, richten Sie eine abstrakte PurchaseHandler-Klasse mit separaten Implementierungen für jeden Store ein.

be50c207c5a2a519.png

Fügen Sie zuerst dem Ordner lib/ eine purchase_handler.dart-Datei hinzu, in der Sie eine abstrakte PurchaseHandler-Klasse mit zwei abstrakten Methoden zum Überprüfen von zwei verschiedenen Arten von Käufen definieren: Abos und Nicht-Abos.

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, damit Sie Käufe dem Nutzer zuordnen können.
  • productData: Daten zum Produkt. Sie werden das gleich definieren.
  • token: Das Token, das dem Nutzer vom Store bereitgestellt wird.

Um die Verwendung dieser Kauf-Handler zu vereinfachen, 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 verifyPurchase für beide Fälle aufrufen, haben aber weiterhin separate Implementierungen.

Die Klasse ProductData enthält grundlegende Informationen zu den verschiedenen käuflichen Produkten, einschließlich der Produkt-ID (manchmal auch als Artikelnummer bezeichnet) und der ProductType.

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

Das ProductType kann entweder ein Abo oder kein Abo sein.

lib/products.dart

enum ProductType { subscription, nonSubscription }

Schließlich wird die Liste der Produkte als Map 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. So gehts:

Erstellen Sie lib/google_play_purchase_handler.dart und fügen Sie eine Klasse hinzu, die die gerade 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;
  }
}

Derzeit wird true für die Handler-Methoden zurückgegeben. Sie werden später darauf zurückkommen.

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

Wiederholen Sie den Vorgang für den App-Shop-Handler. Erstellen Sie lib/app_store_purchase_handler.dart und fügen Sie eine Klasse hinzu, die PurchaseHandler 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,
  }) async {
    return true;
  }

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

Sehr gut! Sie haben jetzt zwei Kauf-Handler. Erstellen Sie als Nächstes den API-Endpunkt für die Kaufbestätigung.

Kauf-Handler verwenden

Öffnen Sie bin/server.dart und erstellen Sie einen API-Endpunkt mit 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');
  }
}

Der Code führt Folgendes aus:

  1. Definieren Sie einen POST-Endpunkt, der von der App aufgerufen wird, die Sie zuvor erstellt haben.
  2. Decodieren Sie die JSON-Nutzlast und extrahieren Sie die folgenden Informationen:
    1. userId: ID des angemeldeten Nutzers
    2. source: Verwendeter Store, entweder app_store oder google_play.
    3. productData: Wird aus dem zuvor erstellten productDataMap abgerufen.
    4. token: Enthält die Bestätigungsdaten, die an die Stores gesendet werden sollen.
  3. Rufen Sie die Methode verifyPurchase entweder für GooglePlayPurchaseHandler oder für AppStorePurchaseHandler auf, je nach Quelle.
  4. Wenn die Bestätigung erfolgreich war, gibt die Methode ein Response.ok an den Client zurück.
  5. Wenn die Überprüfung fehlschlägt, gibt die Methode einen 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 unterschiedlichen Abhängigkeiten:

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

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

Fahren Sie dann mit der Implementierung des Google Play-Kaufhandlers fort.

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

Handler für Käufe implementieren, die keine Abos sind:

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

Sie können den Handler für den Abo-Kauf 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 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;
  }
}

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

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

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

Fahren Sie dann mit App Store-Käufen für iOS fort.

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

Für die Bestätigung von Käufen im App Store gibt es ein Drittanbieter-Dart-Paket namens app_store_server_sdk, das den Vorgang vereinfacht.

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

lib/app_store_purchase_handler.dart

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

Im Gegensatz zu den Google Play-APIs verwendet der App Store für Abos und Nicht-Abos dieselben API-Endpunkte. Das bedeutet, dass Sie für beide Handler dieselbe Logik verwenden können. Führen Sie sie zusammen, damit 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 {

    // See next step
  }

Implementieren Sie jetzt 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 überprüft und in der Datenbank gespeichert werden.

Backend ausführen

An diesem Punkt können Sie dart bin/server.dart ausführen, um den /verifypurchase-Endpunkt bereitzustellen.

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

11. Käufe im Blick behalten

Wir empfehlen, die Käufe Ihrer Nutzer im Backend-Dienst zu erfassen. Das liegt daran, dass Ihr Backend auf Ereignisse aus dem Store reagieren kann und daher weniger anfällig für veraltete Informationen aufgrund von Caching ist. Außerdem ist es weniger anfällig für Manipulationen.

Richten Sie zuerst die Verarbeitung von Store-Ereignissen im Backend mit dem Dart-Backend ein, das Sie erstellt haben.

Geschäftsereignisse im Backend verarbeiten

Shops 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 richten Sie die Einrichtung sowohl für den Google Play Store als auch für den App Store ein.

Google Play Billing-Ereignisse verarbeiten

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

Da es sich um eine Google Play-spezifische Funktion handelt, fügen Sie sie in GooglePlayPurchaseHandler ein.

Ö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, um Timer so zu erstellen:

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

Der Timer ist so konfiguriert, dass die Methode _pullMessageFromPubSub alle 10 Sekunden aufgerufen wird. Sie können die Dauer nach Belieben anpassen.

Erstellen Sie dann die _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,
    );
  }

Der Code, den Sie gerade hinzugefügt haben, 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 (Abonnements und Nicht-Abonnements) abgerufen. Bei Bedarf wird die vorhandene handleSubscription- oder handleNonSubscription-Methode aufgerufen.

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

Fügen Sie als Nächstes der Datei server.dart die erforderlichen Abhängigkeiten hinzu. Fügen Sie der Anmeldedatenkonfiguration PubsubApi.cloudPlatformScope hinzu:

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
      ]);

Erstellen Sie dann die PubsubApi-Instanz:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Übergeben Sie sie schließlich an den GooglePlayPurchaseHandler-Konstruktor:

bin/server.dart

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

Google Play-Einrichtung

Sie haben den Code zum Empfangen von Abrechnungsereignissen aus dem Pub/Sub-Thema geschrieben, aber das Pub/Sub-Thema nicht erstellt und veröffentlichen auch keine Abrechnungsereignisse. Es ist an der Zeit, das einzurichten.

Erstellen Sie zuerst ein Pub/Sub-Thema:

  1. Legen Sie den Wert von googleCloudProjectId in constants.dart auf die ID Ihres Google Cloud-Projekts fest.
  2. Rufen Sie in der Google Cloud Console die Cloud Pub/Sub-Seite auf.
  3. Achten Sie darauf, dass Sie sich in Ihrem Firebase-Projekt befinden, und klicken Sie auf + Thema erstellen. d5ebf6897a0a8bf5.png
  4. Geben Sie dem neuen Thema einen Namen, der mit dem für googlePlayPubsubBillingTopic in constants.dart festgelegten Wert identisch ist. In diesem Fall nennen wir ihn play_billing. Wenn Sie etwas anderes auswählen, müssen Sie constants.dart aktualisieren. Erstellen Sie das Thema. 20d690fc543c4212.png
  5. Klicken Sie in der Liste Ihrer Pub/Sub-Themen auf das Dreipunkt-Menü für das gerade erstellte Thema und dann auf Berechtigungen ansehen. ea03308190609fb.png
  6. Wählen Sie in der Seitenleiste rechts Hauptkonto hinzufügen aus.
  7. Fügen Sie hier google-play-developer-notifications@system.gserviceaccount.com hinzu und weisen Sie ihr die Rolle Pub/Sub-Publisher zu. 55631ec0549215bc.png
  8. Speichern Sie die Berechtigungsänderungen.
  9. Kopieren Sie den Themennamen des Themas, das Sie gerade erstellt haben.
  10. Öffnen Sie die Play Console noch einmal und wählen Sie Ihre App aus der Liste Alle Apps aus.
  11. Scrollen Sie nach unten zu Monetarisieren > Einrichtung der Monetarisierung.
  12. Geben Sie das vollständige Thema ein und speichern Sie Ihre Änderungen. 7e5e875dc6ce5d54.png

Alle Google Play-Abrechnungsereignisse werden jetzt in diesem Thema veröffentlicht.

App Store-Abrechnungsereignisse verarbeiten

Wiederholen Sie den Vorgang für die Abrechnungsereignisse im App Store. Es gibt zwei effektive Möglichkeiten, Aktualisierungen bei Käufen im App Store zu implementieren. Eine Möglichkeit ist die Implementierung eines Webhooks, den Sie Apple zur Verfügung stellen und der für die Kommunikation mit Ihrem Server verwendet wird. Die zweite Möglichkeit, die in diesem Codelab beschrieben wird, besteht darin, eine Verbindung zur App Store Server API herzustellen und die Aboinformationen manuell abzurufen.

In diesem Codelab wird die zweite Lösung behandelt, da Sie Ihren Server für das Internet freigeben müssten, um den Webhook zu implementieren.

In einer Produktionsumgebung sollten Sie idealerweise beides haben. Der Webhook zum Abrufen von Ereignissen aus dem App Store und die Server API, falls Sie ein Ereignis verpasst haben oder den Abostatus noch einmal überprü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;                 // Add this member

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

Ändern Sie den Konstruktor, um einen Timer hinzuzufügen, der die Methode _pullStatus aufruft. Dieser Timer ruft die Methode _pullStatus alle 10 Sekunden auf. Sie können die Dauer dieses 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 Methode _pullStatus so:

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

Diese Methode funktioniert so:

  1. Ruft die Liste der aktiven Abos aus Firestore mithilfe von IapRepository ab.
  2. Für jede Bestellung wird der Abostatus von der App Store Server API angefordert.
  3. Ruft die letzte Transaktion für diesen Abo-Kauf ab.
  4. Prüft das Ablaufdatum.
  5. Aktualisiert den Abostatus in Firestore. Wenn das Abo abgelaufen ist, wird es entsprechend gekennzeichnet.

Fügen Sie schließlich den gesamten erforderlichen Code hinzu, um den Zugriff auf die App Store Server API zu konfigurieren:

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

App Store-Einrichtung

Richten Sie als Nächstes den App Store ein:

  1. Melden Sie sich in App Store Connect an und wählen Sie Nutzer und Zugriff aus.
  2. Klicken Sie auf Integrations > Keys > In-App Purchase (Integrationen > Schlüssel > In-App-Kauf).
  3. Tippe auf das Pluszeichen, um einen neuen hinzuzufügen.
  4. Geben Sie ihm einen Namen, z. B. „Codelab-Schlüssel“.
  5. Laden Sie die P8-Datei mit dem Schlüssel herunter.
  6. Kopieren Sie sie in den Assets-Ordner und geben Sie ihr den Namen SubscriptionKey.p8.
  7. Kopieren Sie die Schlüssel-ID aus dem neu erstellten Schlüssel und legen Sie sie in der Datei lib/constants.dart für die Konstante appStoreKeyId fest.
  8. Kopieren Sie die Aussteller-ID ganz oben in der Schlüsselliste und legen Sie sie in der Datei lib/constants.dart auf die Konstante appStoreIssuerId fest.

9540ea9ada3da151.png

Käufe auf dem Gerät erfassen

Die sicherste Methode zum Erfassen von Käufen ist die serverseitige Erfassung, da der Client schwer zu schützen ist. Sie benötigen jedoch eine Möglichkeit, die Informationen an den Client zurückzugeben, damit die App auf die Informationen zum Abo-Status reagieren kann. Wenn Sie die Käufe in Firestore speichern, können Sie die Daten mit dem Client synchronisieren und automatisch auf dem neuesten Stand halten.

Sie haben IAPRepo bereits in die App aufgenommen. Das ist das Firestore-Repository, das alle Kaufdaten des Nutzers in List<PastPurchase> purchases enthält. Das Repository enthält auch hasActiveSubscription,, was „true“ ist, wenn ein Kauf mit productId storeKeySubscription mit einem Status vorliegt, 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((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. Dort sollten Abos angewendet oder entfernt werden. Fügen Sie iapRepo als Property in der Klasse hinzu und weisen Sie iapRepo im Konstruktor zu. Fügen Sie als Nächstes direkt im Konstruktor einen Listener hinzu und entfernen Sie ihn in der Methode dispose(). Anfangs kann der Listener einfach eine leere Funktion sein. Da IAPRepo ein ChangeNotifier ist und Sie notifyListeners() jedes Mal 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

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
  }

Als Nächstes geben Sie IAPRepo an den Konstruktor in main.dart. an. 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>(),                         // Add this line
          ),
          lazy: false,
        ),

Schreiben Sie als Nächstes den Code für die Funktion purchaseUpdate(). In dash_counter.dart, wird der Multiplikator mit den Methoden applyPaidMultiplier und removePaidMultiplier auf 10 bzw. 1 gesetzt. Sie müssen also nicht prüfen, ob das Abo bereits angewendet wurde. Wenn sich der Abostatus ändert, aktualisieren Sie auch den Status des kaufbaren Produkts, damit auf der Kaufseite angezeigt werden kann, dass es bereits aktiv ist. Legen Sie die _beautifiedDashUpgrade-Property basierend darauf fest, ob das Upgrade gekauft wurde.

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 jetzt dafür gesorgt, dass der Abo- und Upgradestatus im Backend-Dienst immer aktuell ist und mit der App synchronisiert wird. Die App reagiert entsprechend und wendet die Abo- und Upgradefunktionen auf Ihr Dash-Clicker-Spiel an.

12. Fertig!

Herzlichen Glückwunsch!!! Sie haben das Codelab abgeschlossen. Den vollständigen Code für dieses Codelab finden Sie im Ordner android_studio_folder.png.

Weitere Informationen finden Sie in den anderen Flutter-Codelabs.