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 gewähren, z. B. für Abovorteile.

In diesem Codelab fügen Sie einer App (die Ihnen zur Verfügung gestellt wird) drei Arten von In-App-Käufen hinzu und bestätigen 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. Fügen Sie die folgenden Kaufoptionen hinzu:

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

Bei der ersten Kaufoption erhält der Nutzer direkt 2.000 Dashes. Sie sind direkt für den Nutzer verfügbar und können mehrmals gekauft werden. Dieser wird als Verbrauchsmaterial bezeichnet, da er direkt verbraucht und mehrmals verwendet werden kann.

Die zweite Option aktualisiert das Dashboard zu einem schöneren Dashboard. Dieser muss nur einmal gekauft werden und ist dann dauerhaft verfügbar. Ein solcher Kauf wird als nicht verbrauchbar bezeichnet, da er nicht von der App genutzt werden kann, aber dauerhaft gültig ist.

Die dritte und letzte Kaufoption ist ein Abo. Solange das Abo aktiv ist, erhält der Nutzer Dashes schneller. Wenn er das Abo nicht mehr bezahlt, entfallen auch die Vorteile.

Der Back-End-Dienst (wird ebenfalls für Sie bereitgestellt) wird als Dart-App ausgeführt, prüft, ob 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 Verbrauchskäufe und Abos zu unterstützen.
  • Außerdem erweitern Sie eine Dart-Backend-App, um die gekauften Artikel zu überprüfen und zu speichern.

Lerninhalte

  • App Store und Play Store mit käuflichen Produkten konfigurieren
  • Wie du mit den Geschäften kommunizierst, um Käufe zu bestätigen und in Firestore zu speichern.
  • So verwalten Sie Käufe in Ihrer App.

Voraussetzungen

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

2. Entwicklungsumgebung einrichten

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

Code herunterladen

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

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

Wenn Sie die Befehlszeile 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, wo Sie am Ende jedes benannten Schritts sein sollten. Der Startcode befindet sich in Schritt 0. So finden Sie die entsprechenden Dateien ganz einfach:

cd flutter-codelabs/in_app_purchases/step_00

Wenn Sie vorspulen oder sehen möchten, wie etwas nach einem Schritt aussehen sollte, suchen Sie im Verzeichnis, das nach dem gewünschten Schritt benannt ist. Der Code für den letzten Schritt befindet sich im Ordner complete.

Startprojekt einrichten

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

Die von Ihnen erstellten Apps 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 dies als Bundle-ID bezeichnet, im Android Play Store als Anwendungs-ID. Diese IDs werden in der Regel in umgekehrter Domainnamenschreibweise angegeben. Wenn Sie beispielsweise eine App mit In-App-Käufen für flutter.dev erstellen, verwenden Sie dev.flutter.inapppurchase. Überlegen Sie sich eine Kennung für Ihre App. Sie legen diese jetzt in den Projekteinstellungen fest.

Richten Sie zuerst die Paket-ID für iOS ein.

Klicken Sie bei geöffnetem Projekt in Android Studio mit der rechten Maustaste auf den iOS-Ordner, dann auf Flutter und öffnen Sie das Modul in der Xcode App.

942772eb9a73bfaa.png

In der Ordnerstruktur von Xcode befindet sich das Runner-Projekt oben und die Ziele Flutter, Runner und Produkte befinden sich unter dem Runner-Projekt. Doppelklicken Sie auf Runner, um die Projekteinstellungen zu bearbeiten, und klicken Sie auf Signatur und 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 unter android/app, und ändern Sie die applicationId (in Zeile 37 im Screenshot unten) in die Anwendungs-ID, die mit der iOS-Bundle-ID übereinstimmt. Die IDs für den iOS- und Android-Shop müssen nicht identisch sein. Es ist jedoch weniger fehleranfällig, wenn sie identisch sind. Daher verwenden wir in diesem Codelab auch identische Kennungen.

5c4733ac560ae8c2.png

3. Plug-in installieren

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

Abhängigkeit in der pubspec hinzufügen

Fügen Sie der Pubspec in_app_purchase hinzu, indem Sie in_app_purchase zu den Abhängigkeiten in der Pubspec hinzufügen:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface

Ö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: ^5.5.1
  cupertino_icons: ^1.0.8
  firebase_auth: ^5.3.4
  firebase_core: ^3.8.1
  google_sign_in: ^6.2.2
  http: ^1.2.2
  intl: ^0.20.1
  provider: ^6.1.2
  in_app_purchase: ^3.2.0

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

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

4. App Store einrichten

Wenn Sie In-App-Produkte einrichten und auf iOS-Geräten testen möchten, müssen Sie im App Store eine neue App erstellen und dort Produkte zum Verkauf anbieten. Sie müssen nichts veröffentlichen oder die App zur Überprüfung an Apple senden. Dazu benötigen Sie ein Entwicklerkonto. Wenn Sie noch keines haben, melden Sie sich beim Apple-Entwicklerprogramm 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 Vereinbarungen, Steuern und Banken.

11db9fca823e7608.png

Hier finden Sie Vereinbarungen für kostenlose und kostenpflichtige Apps. Der Status von kostenlosen Apps sollte „Aktiv“ und der Status von kostenpflichtigen Apps „Neu“ sein. Lesen Sie sich die Nutzungsbedingungen durch, akzeptieren Sie sie und geben Sie alle erforderlichen Informationen ein.

74c73197472c9aec.png

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

4a100bbb8cafdbbf.jpeg

App-ID registrieren

Erstellen Sie eine neue Kennung im Apple Developer Portal.

55d7e592d9a3fc7b.png

App-IDs auswählen

13f125598b72ca77.png

App auswählen

41ac4c13404e2526.png

Geben Sie eine Beschreibung ein und legen Sie die Paket-ID so fest, dass sie mit dem Wert übereinstimmt, 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 verbunden sein. Er wird nur zum Testen von In-App-Käufen verwendet. Sie können keine E-Mail-Adresse verwenden, die bereits für ein Apple-Konto verwendet wird. Klicken Sie auf der Seite Nutzer und Zugriff unter Sandbox auf Tester, um ein neues Sandbox-Konto zu erstellen oder die vorhandenen Sandbox-Apple-IDs zu verwalten.

3ca2b26d4e391a4c.jpeg

Sie können jetzt Ihren Sandbox-Nutzer auf Ihrem iPhone einrichten. Gehen Sie dazu zu Einstellungen > App Store > Sandbox-Konto.

d99e0b89673867cd.jpeg e1621bcaeb33d3c5.jpeg

In-App-Käufe konfigurieren

Konfigurieren Sie jetzt die drei käuflichen Artikel:

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

d156b2f5bac43ca8.png

Gehen Sie zu In-App-Käufe > Verwalten.

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

  1. Richten Sie dash_consumable_2k als Verbrauchsartikel 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 und fügen Sie Ihre Lokalisierungen für den Kauf hinzu. Rufe den Kauf Spring is in the air mit 2000 dashes fly out als Beschreibung auf.

ec1701834fd8527.png

  1. Richten Sie dash_upgrade_3d als Nicht verbrauchbare Ressource ein.

Verwenden Sie dash_upgrade_3d als Produkt-ID. Legen Sie den Referenznamen auf dash upgrade 3d fest und fügen Sie Ihre Lokalisierungen für den Kauf hinzu. Rufe den Kauf 3D Dash mit Brings your dash back to the future als Beschreibung auf.

6765d4b711764c30.png

  1. Richten Sie dash_subscription_doubler als Abo mit automatischer Verlängerung ein.

Bei Abos sieht der Ablauf etwas anders aus. Legen Sie zuerst den Referenznamen und die Produkt-ID fest:

6d29e08dae26a0c4.png

Als Nächstes müssen Sie eine Abogruppe erstellen. Wenn mehrere Abos zu derselben Gruppe gehören, kann ein Nutzer nur eines davon gleichzeitig abonnieren. Er kann jedoch ganz einfach zwischen diesen Abos wechseln. Nennen Sie diese Gruppe einfach subscriptions.

5bd0da17a85ac076.png

Geben Sie als Nächstes die Abolaufzeit und die Lokalisierungen ein. Benennen Sie dieses Abo Jet Engine und geben Sie die Beschreibung Doubles your clicks an. Klicken Sie auf Speichern.

bd1b1d82eeee4cb3.png

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

d0bf39680ef0aa2e.png

Die drei Käufe sollten jetzt in der Liste der Käufe angezeigt werden:

99d5c4b446e8fecf.png

5. Play Store einrichten

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

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 um ein Spiel handelt. Sie können dies später ändern.
  5. Geben Sie an, ob Ihre App kostenlos oder kostenpflichtig ist.
  6. Fügen Sie eine E-Mail-Adresse hinzu, unter der Play Store-Nutzer Sie bezüglich dieser App kontaktieren können.
  7. Machen Sie die erforderlichen Angaben für die Deklarationen „Inhaltsrichtlinien“ und „Exportbestimmungen der USA“.
  8. Wählen Sie App erstellen aus.

Nachdem Sie Ihre App erstellt haben, rufen Sie das Dashboard auf und führen Sie alle Aufgaben im Bereich App einrichten aus. 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 werden.

Dazu muss Ihr Release-Build mit etwas anderem als den Debug-Schlüsseln signiert sein.

Schlüsselspeicher erstellen

Wenn Sie bereits einen Schlüsselspeicher haben, fahren Sie mit dem nächsten Schritt fort. Falls nicht, erstellen Sie einen, indem Sie in der Befehlszeile Folgendes eingeben:

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 key.jks-Datei 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 Sie die

keystore

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

Schlüsselspeicher aus der App referenzieren

Erstellen Sie eine Datei mit dem Namen <your app dir>/android/key.properties, die eine Referenz 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>

Signatur in Gradle konfigurieren

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

Fügen Sie die Informationen zum Schlüsselspeicher aus Ihrer Properties-Datei vor dem Block android ein:

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

   android {
         // omitted
   }

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

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

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

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

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

Release-Builds 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 Signatur konfiguriert wurde, können Sie sie mit dem folgenden Befehl erstellen:

flutter build appbundle

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

Klicken Sie im Dashboard der Google Play Console auf Release > Test > Geschlossener Test und erstellen Sie eine neue Release-Version für geschlossene Tests.

In diesem Codelab wird die App von Google signiert. Klicken Sie daher unter Play App Signing auf Weiter, um die Funktion zu aktivieren.

ba98446d9c5c40e0.png

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

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

Klicken Sie abschließend auf Roll-out für interne Tests starten, um den internen Testrelease zu aktivieren.

Testnutzer einrichten

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

  1. Zum spezifischen Test-Track (interne Tests)
  2. Als Lizenztester

Fügen Sie den Tester zuerst dem internen Test-Track hinzu. Kehren Sie zu Release > Test > 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 das Testen von In-App-Käufen benötigen.

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

Fügen Sie dann die Lizenztester hinzu:

  1. Kehren Sie in der Google Play Console zur Ansicht Alle Apps zurück.
  2. Gehen Sie zu Einstellungen > Lizenztest.
  3. Fügen Sie die E-Mail-Adressen der Tester hinzu, die In-App-Käufe testen sollen.
  4. Legen Sie für License response (Lizenzantwort) die Option RESPOND_NORMALLY fest.
  5. Klicken Sie auf ROLLE ERSTELLEN.

a1a0f9d3e55ea8da.png

In-App-Käufe konfigurieren

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

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

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

Fügen Sie zuerst die Verbrauchsmaterialien und die nicht abnutzbaren Artikel hinzu.

  1. Rufen Sie die Google Play Console auf und wählen Sie Ihre Anwendung aus.
  2. Gehen Sie zu Monetarisieren > Produkte > In-App-Produkte.
  3. Klicken Sie auf Produkt erstellenc8d66e32f57dee21.png.
  4. Geben Sie alle erforderlichen Informationen zu Ihrem Produkt ein. Die Produkt-ID muss genau mit der ID übereinstimmen, 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 des Upgrades.

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. Gehen Sie zu Monetarisieren > Produkte > Abos.
  3. Klicken Sie auf Abo erstellen.32a6a9eefdb71dd0.png
  4. Geben Sie alle erforderlichen Informationen für Ihr Abo ein. Die Produkt-ID muss genau mit der ID übereinstimmen, 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 zu verfolgen.

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

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

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

Das Erstellen des Backends fällt nicht in den Rahmen dieses Codelabs. Der Startcode enthält daher bereits ein Firebase-Projekt, das grundlegende Käufe verarbeitet.

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

Jetzt müssen Sie nur noch ein eigenes Firebase-Projekt erstellen, die App und 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 Back-End-App binden Sie Käufe an einen 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 sie bei Bedarf.
  2. Rufen Sie den Tab Anmeldemethode auf und aktivieren Sie den Anmeldeanbieter Google.

7babb48832fbef29.png

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

e20553e0de5ac331.png

So legen Sie Cloud Firestore-Regeln fest:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /purchases/{purchaseId} {
      allow read: if request.auth != null && request.auth.uid == resource.data.userId
    }
  }
}

Firebase für Flutter einrichten

Wir empfehlen, Firebase mit der FlutterFire CLI in der Flutter-App zu installieren. Folgen Sie der Anleitung auf der Einrichtungsseite.

Wählen Sie beim Ausführen von „flutterfire configure“ das Projekt aus, das Sie im vorherigen Schritt erstellt haben.

$ flutterfire configure

i Found 5 Firebase projects.                                                                                                  
? Select a Firebase project to configure your Flutter application with                                                        in-app-purchases-1234 (in-app-purchases-1234)                                                                         
  other-flutter-codelab-1 (other-flutter-codelab-1)                                                                           
  other-flutter-codelab-2 (other-flutter-codelab-2)                                                                      
  other-flutter-codelab-3 (other-flutter-codelab-3)                                                                           
  other-flutter-codelab-4 (other-flutter-codelab-4)                                                                                                                                                               
  <create a new project>  

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

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

Wählen Sie „Ja“, wenn Sie gefragt werden, ob Sie „firebase_options.dart“ überschreiben möchten.

? 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

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

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

b22d46a759c0c834.png

Wenn Sie Google Sign-in im Debug-Modus zulassen möchten, müssen Sie den SHA-1-Hash-Fingerabdruck Ihres Debugzertifikats angeben.

Hash des Debug-Signaturzertifikats abrufen

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

cd android
./gradlew :app:signingReport

Es wird eine lange Liste von Signaturschlüsseln angezeigt. Da Sie nach dem Hash für das Debug-Zertifikat suchen, suchen Sie nach dem Zertifikat, für das die Eigenschaften Variant und Config auf debug festgelegt sind. Der Schlüsselspeicher befindet sich wahrscheinlich in Ihrem Basisordner unter .android/debug.keystore.

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

Kopieren Sie den SHA-1-Hash und füllen Sie das letzte Feld im modalen Dialogfeld für die App-Einreichung aus.

Firebase für iOS einrichten: Weitere Schritte

Öffnen Sie die ios/Runnder.xcworkspace mit Xcode. oder mit Ihrer bevorzugten IDE.

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/, dann auf flutter und dann auf die Option open iOS module in Xcode.

Wenn Sie Google Sign-in auf iOS-Geräten zulassen möchten, fügen Sie Ihren Build-plist-Dateien die Konfigurationsoption CFBundleURLTypes hinzu. Weitere Informationen finden Sie in der google_sign_in-Paketdokumentation. In diesem Fall sind das die Dateien ios/Runner/Info-Debug.plist und ios/Runner/Info-Release.plist.

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

  1. Rufen Sie den Wert für REVERSED_CLIENT_ID aus der Datei GoogleService-Info.plist ab, ohne das umgebende <string>..</string>-Element.
  2. Ersetzen Sie den Wert sowohl in der Datei ios/Runner/Info-Debug.plist als auch in der Datei ios/Runner/Info-Release.plist unter dem Schlüssel CFBundleURLTypes.
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <!-- TODO Replace this value: -->
            <!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
            <string>com.googleusercontent.apps.REDACTED</string>
        </array>
    </dict>
</array>

Die Firebase-Einrichtung ist jetzt abgeschlossen.

7. Kaufaktualisierungen anhören

In diesem Teil des Codelabs bereiten Sie die App auf den Kauf der Produkte vor. Dazu gehört auch das Abhören von Kaufaktualisierungen und Fehlern nach dem Starten der App.

Updates zu Käufen anhören

Suchen Sie in main.dart, nach dem Widget MyHomePage mit einer Scaffold mit einer BottomNavigationBar, die zwei Seiten enthält. Auf dieser Seite werden auch drei Providers für DashCounter, DashUpgrades, und DashPurchases erstellt. DashCounter überwacht 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 dieses Objekt zum ersten Mal angefordert wird. Dieses Objekt überwacht Kaufaktualisierungen direkt beim Starten der App. Deaktiviere 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 der 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!;
  }
}

Sie müssen den Test leicht aktualisieren, damit er weiterhin funktioniert.

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.

Rufe in lib/logic/dash_purchases.dart den Code für DashPurchases ChangeNotifier auf. Derzeit gibt es nur eine DashCounter, die Sie Ihren gekauften Dashes hinzufügen können.

Fügen Sie eine Property für das Streamabo, _subscription (vom Typ StreamSubscription<List<PurchaseDetails>> _subscription;), die IAPConnection.instance, und die Importe hinzu. Der Code sollte dann in etwa 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, da _subscription im Konstruktor initialisiert wird. Dieses Projekt ist so eingerichtet, dass standardmäßig keine Nullwerte zulässig sind (Non-Nullable By Default, NNBD). Das bedeutet, dass Eigenschaften, die nicht als nullable deklariert sind, einen Wert haben müssen, der nicht null ist. Mit dem late-Qualifikator können Sie die Definition dieses Werts verschieben.

Rufe im Konstruktor den purchaseUpdated-Stream ab und beginne mit dem Streamen. Kündige in der dispose()-Methode das Stream-Abo.

lib/logic/dash_purchases.dart

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

  bool get beautifiedDash => false;

  final iapConnection = IAPConnection.instance;

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

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

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

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

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

Jetzt erhält die App die Kaufaktualisierungen. Im nächsten Abschnitt führen Sie einen Kauf durch.

Führen Sie vor dem Fortfahren die Tests mit „flutter test"“ aus, um sicherzustellen, dass alles richtig eingerichtet ist.

$ flutter test

00:01 +1: All tests passed!                                                                                   

8. Einkaufen

In diesem Teil des Codelabs ersetzen Sie die derzeit vorhandenen Mockups durch echte, kaufbare Produkte. Diese Produkte werden aus den Shops geladen, in einer Liste angezeigt und können durch Tippen auf das Produkt gekauft werden.

PurchasableProduct anpassen

PurchasableProduct zeigt ein Mockup eines Produkts. Aktualisieren Sie sie, damit tatsächliche Inhalte angezeigt werden. Ersetzen Sie dazu die Klasse PurchasableProduct in purchasable_product.dart durch den folgenden Code:

lib/model/purchasable_product.dart

import 'package:in_app_purchase/in_app_purchase.dart';

enum ProductStatus {
  purchasable,
  purchased,
  pending,
}

class PurchasableProduct {
  String get id => productDetails.id;
  String get title => productDetails.title;
  String get description => productDetails.description;
  String get price => productDetails.price;
  ProductStatus status;
  ProductDetails productDetails;

  PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}

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

Wenn Sie Nutzern die Möglichkeit geben möchten, etwas zu kaufen, laden Sie die Käufe aus dem Store herunter. Prüfen Sie zuerst, ob der Shop verfügbar ist. Wenn der Store nicht verfügbar ist, wird dem Nutzer beim Festlegen von storeState auf notAvailable eine Fehlermeldung angezeigt.

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, lade die verfügbaren Käufe. Aufgrund der vorherigen Firebase-Einrichtung sollten Sie storeKeyConsumable, storeKeySubscription, und storeKeyUpgrade sehen. Wenn ein erwarteter Kauf nicht verfügbar ist, drucke diese Informationen in der Konsole aus. Du kannst diese Informationen auch an den Backend-Dienst senden.

Die await iapConnection.queryProductDetails(ids)-Methode gibt sowohl die nicht gefundenen IDs als auch die gefundenen kaufbaren Produkte zurück. Verwende den Wert productDetails aus der Antwort, um die Benutzeroberfläche zu aktualisieren, und setze StoreState auf available.

lib/logic/dash_purchases.dart

import '../constants.dart';

  Future<void> loadPurchases() async {
    final available = await iapConnection.isAvailable();
    if (!available) {
      storeState = StoreState.notAvailable;
      notifyListeners();
      return;
    }
    const ids = <String>{
      storeKeyConsumable,
      storeKeySubscription,
      storeKeyUpgrade,
    };
    final response = await iapConnection.queryProductDetails(ids);
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

Rufen Sie die Funktion loadPurchases() im Konstruktor auf:

lib/logic/dash_purchases.dart

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

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

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Kaufbare Produkte anzeigen

Betrachten Sie die Datei purchase_page.dart. 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.

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

lib/pages/purchase_page.dart

class _PurchaseList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var purchases = context.watch<DashPurchases>();
    var products = purchases.products;
    return Column(
      children: products
          .map((product) => _PurchaseWidget(
              product: product,
              onPressed: () {
                purchases.buy(product);
              }))
          .toList(),
    );
  }
}

Wenn die Produkte richtig konfiguriert sind, sollten Sie sie in den Android- und iOS-Shops sehen können. Es kann einige Zeit dauern, bis die Käufe in den jeweiligen Konsolen verfügbar sind.

ca1a9f97c21e552d.png

Kehren Sie zu dash_purchases.dart zurück und implementieren Sie die Funktion zum Kauf eines Produkts. Sie müssen nur die Verbrauchsmaterialien von den nicht abnutzbaren Materialien trennen. Das Upgrade und die Aboprodukte sind nicht abnutzbare Produkte.

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, um darauf zu verweisen.

lib/logic/dash_purchases.dart

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

Die _onPurchaseUpdate-Methode empfängt die Kaufaktualisierungen, aktualisiert den Status des Produkts, der auf der Kaufseite angezeigt wird, und wendet den Kauf auf die Zählerlogik an. Es ist wichtig, completePurchase nach der Bearbeitung des Kaufs aufzurufen, damit der Händler 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 Überprüfung von Käufen fortfahren, richten Sie ein Dart-Backend ein, das dies unterstützt.

In diesem Abschnitt verwenden Sie den Ordner dart-backend/ als Stammordner.

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 anzusehen, was bereits im Startercode enthalten ist, bevor Sie beginnen, 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) eine Verbindung zum 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 einen Routenabschnitt, um den Kaufbestätigungsprozess zu verarbeiten.

Ein Teil, der bereits im Startcode enthalten ist, ist die IapRepository in lib/iap_repository.dart. Da das Interagieren mit Firestore oder Datenbanken im Allgemeinen für dieses Codelab nicht relevant ist, enthält der Startercode 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 ein Dienstkonto. Sie können einen privaten 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 bestätigen, müssen Sie ein Dienstkonto mit diesen Berechtigungen generieren und die JSON-Anmeldedaten dafür herunterladen.

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

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

Zugriff auf den Apple App Store einrichten

Wenn Sie auf den App Store zugreifen und Käufe bestätigen möchten, müssen Sie ein gemeinsames Secret einrichten:

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

d8b8042470aaeff.png

b72f4565750e2f40.png

Konfigurationsdatei für Konstanten

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

  • androidPackageId: Paket-ID, die auf Android-Geräten 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: Bundle-ID, die auf iOS-Geräten verwendet wird, z. B. com.example.dashclicker

Die restlichen Konstanten können Sie vorerst ignorieren.

10. Käufe bestätigen

Der allgemeine Ablauf für die Überprüfung von Käufen ist für iOS und Android ähnlich.

Bei beiden Shops erhält Ihre Anwendung bei einem Kauf ein Token.

Dieses Token wird von der App an Ihren Back-End-Dienst gesendet, der den Kauf dann mit dem bereitgestellten Token auf den Servern des jeweiligen Shops bestätigt.

Der Back-End-Dienst kann dann den Kauf speichern und an die Anwendung antworten, ob der Kauf gültig war oder nicht.

Wenn die Validierung durch den Backend-Dienst und nicht durch die Anwendung auf dem Gerät des Nutzers erfolgt, können Sie verhindern, dass der Nutzer Zugriff auf Premium-Funktionen erhält, indem er beispielsweise seine Systemuhr zurückdreht.

Flutter-Seite einrichten

Authentifizierung einrichten

Da Sie die Käufe an Ihren Backend-Dienst senden, sollten Sie darauf achten, dass der Nutzer beim Kauf authentifiziert ist. Der Großteil der Authentifizierungslogik wurde im Starterprojekt bereits für Sie hinzugefügt. Sie müssen nur dafür sorgen, dass die Schaltfläche „Anmelden“ auf der PurchasePage 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 '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

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

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

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

Endpunkt für die Bestätigung von Anrufen über die App

Erstellen Sie in der App die Funktion _verifyPurchase(PurchaseDetails purchaseDetails), die den Endpunkt /verifypurchase in Ihrem Dart-Backend über einen HTTP-Postaufruf aufruft.

Senden Sie 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 als IP-Adresse Ihres lokalen Computers.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

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

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

lib/main.dart

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

Fügen Sie im FirebaseNotifier einen Getter für den Nutzer hinzu, damit Sie die User-ID an die Funktion „verify purchase“ übergeben können.

lib/logic/firebase_notifier.dart

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

Fügen Sie der Klasse DashPurchases die Funktion _verifyPurchase hinzu. Diese async-Funktion gibt einen booleschen Wert zurück, der angibt, ob der Kauf bestätigt 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;
    }
  }

Rufe die Funktion _verifyPurchase in _handlePurchase kurz vor dem Anwenden des Kaufs auf. Sie sollten den Kauf erst anwenden, wenn er bestätigt wurde. In einer Produktions-App können Sie dies weiter spezifizieren, um beispielsweise ein Probeabo anzuwenden, wenn der Store vorübergehend nicht verfügbar ist. Für dieses Beispiel halten wir es jedoch einfach und wenden den Kauf nur an, 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(1000);
        }
      }
    }

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

In der App ist jetzt alles für die Validierung der Käufe bereit.

Backend-Dienst einrichten

Richten Sie als Nächstes die Cloud-Funktion für die Überprüfung von Käufen im Backend ein.

Kauf-Handler erstellen

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

be50c207c5a2a519.png

Fügen Sie dem Ordner lib/ zuerst eine purchase_handler.dart-Datei hinzu. Dort definieren Sie eine abstrakte PurchaseHandler-Klasse mit zwei abstrakten Methoden zur Überprüfung von zwei verschiedenen Arten von Käufen: Abos und nicht abobasierte Käufe.

lib/purchase_handler.dart

import 'products.dart';

/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {

  /// Verify if non-subscription purchase (aka consumable) is valid
  /// and update the database
  Future<bool> handleNonSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });

  /// Verify if subscription purchase (aka non-consumable) is valid
  /// and update the database
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  });
}

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

  • userId: Die ID des angemeldeten Nutzers, damit Sie Käufe dem Nutzer zuordnen können.
  • productData: Daten zum Produkt. Das definieren Sie gleich.
  • token: Das vom Händler bereitgestellte Token.

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 abonnierte Produkte 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 und trotzdem separate Implementierungen haben.

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

lib/products.dart

class ProductData {
  final String productId;
  final ProductType type;

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

ProductType kann entweder ein Abo oder ein nicht abopflichtiger Artikel sein.

lib/products.dart

enum ProductType {
  subscription,
  nonSubscription,
}

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

lib/products.dart

const productDataMap = {
  'dash_consumable_2k': ProductData(
    'dash_consumable_2k',
    ProductType.nonSubscription,
  ),
  'dash_upgrade_3d': ProductData(
    'dash_upgrade_3d',
    ProductType.nonSubscription,
  ),
  'dash_subscription_doubler': ProductData(
    'dash_subscription_doubler',
    ProductType.subscription,
  ),
};

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

Erstellen Sie lib/google_play_purchase_handler.dart und fügen Sie eine Klasse hinzu, die die von Ihnen gerade erstellte 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 gibt es für die Handlermethoden true zurück. Dazu kommen wir später.

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

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

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

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

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

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

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

Sehr gut! Sie haben jetzt zwei Kaufabwickler. Als Nächstes erstellen wir den API-Endpunkt für die Kaufbestätigung.

Kauf-Handler verwenden

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

bin/server.dart

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

  final purchaseHandlers = await _createPurchaseHandlers();

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

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

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

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

  await serveHandler(router.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 oben führt Folgendes aus:

  1. Definieren Sie einen POST-Endpunkt, der von der zuvor erstellten App aufgerufen wird.
  2. Dekodieren Sie die JSON-Nutzlast und extrahieren Sie die folgenden Informationen:
  3. userId: Die ID des aktuell angemeldeten Nutzers
  4. source: Verwendeter Shop, entweder app_store oder google_play.
  5. productData: Wird aus dem zuvor erstellten productDataMap abgerufen.
  6. token: Enthält die Bestätigungsdaten, die an die Geschäfte gesendet werden sollen.
  7. Rufen Sie die Methode verifyPurchase auf, je nach Quelle entweder für die GooglePlayPurchaseHandler oder die AppStorePurchaseHandler.
  8. Wenn die Überprüfung erfolgreich war, gibt die Methode eine Response.ok an den Client zurück.
  9. Wenn die Überprüfung fehlschlägt, gibt die Methode eine Response.internalServerError an den Client zurück.

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

bin/server.dart

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

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

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

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

Fahren Sie als Nächstes mit der Implementierung des Google Play-Kauf-Handlers fort.

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

Implementieren Sie den Handler für Käufe, die nicht dem Abotyp entsprechen:

lib/google_play_purchase_handler.dart

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

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

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

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

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

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

Sie können den Handler für den Abokauf auf ähnliche Weise aktualisieren:

lib/google_play_purchase_handler.dart

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

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

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

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

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

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

Füge die folgende Methode zum Parsen von Bestell-IDs sowie zwei Methoden zum Parsen des Kaufstatus hinzu.

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

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

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

Für die Überprüfung 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 die Protokollierung, um die Fehlerbehebung 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 dieselben API-Endpunkte sowohl für Abos als auch für nicht abonnierte Inhalte. Sie können also dieselbe Logik für beide Handler verwenden. Fügen 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 {
   //..
  }

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

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

Backend ausführen

Jetzt 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 über den Back-End-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 und auch weniger anfällig für Manipulationen ist.

Richten Sie zuerst die Verarbeitung von Ladengeschäftsereignissen im Backend mit dem von Ihnen erstellten Dart-Backend ein.

Geschäftsereignisse im Backend verarbeiten

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

Google Play-Abrechnungsereignisse verarbeiten

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

Da es sich hierbei um eine Google Play-spezifische Funktion handelt, nehmen Sie sie in die GooglePlayPurchaseHandler auf.

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

lib/google_play_purchase_handler.dart

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

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

lib/google_play_purchase_handler.dart

class GooglePlayPurchaseHandler extends PurchaseHandler {
  final ap.AndroidPublisherApi androidPublisher;
  final IapRepository iapRepository;
  final pubsub.PubsubApi pubsubApi; // new

  GooglePlayPurchaseHandler(
    this.androidPublisher,
    this.iapRepository,
    this.pubsubApi, // new
  ) {
    // Poll messages from Pub/Sub every 10 seconds
    Timer.periodic(Duration(seconds: 10), (_) {
      _pullMessageFromPubSub();
    });
  }

Der Timer ist so konfiguriert, dass er die _pullMessageFromSubSub-Methode alle zehn Sekunden aufruft. Sie können die Dauer nach Belieben anpassen.

Erstellen Sie dann die _pullMessageFromSubSub

lib/google_play_purchase_handler.dart

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

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

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

Der 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 _processMessage-Methode verarbeitet.

Bei dieser Methode werden die eingehenden Nachrichten decodiert und die aktualisierten Informationen zu jedem Kauf abgerufen, sowohl für Abos als auch für Käufe ohne Abo. Dabei wird bei Bedarf die vorhandene handleSubscription oder handleNonSubscription aufgerufen.

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

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

bin/server.dart

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

Erstellen Sie dann die PubsubApi-Instanz:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Und schließlich übergeben Sie sie an den Konstruktor von GooglePlayPurchaseHandler:

bin/server.dart

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

Google Play einrichten

Sie haben den Code zum Verbrauchen von Abrechnungsereignissen aus dem Pub/Sub-Thema geschrieben, aber das Pub/Sub-Thema nicht erstellt und keine Abrechnungsereignisse veröffentlicht. Jetzt ist es an der Zeit, die Einrichtung vorzunehmen.

Erstellen Sie zuerst ein Pub/Sub-Thema:

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

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

App Store-Abrechnungsereignisse verarbeiten

Wiederholen Sie diese Schritte für die App Store-Abrechnungsereignisse. Es gibt zwei effektive Möglichkeiten, die Verarbeitung von Kaufaktualisierungen für den App Store zu implementieren. Eine Möglichkeit besteht darin, einen Webhook zu implementieren, den Sie Apple zur Verfügung stellen und über den Apple mit Ihrem Server kommunizieren kann. 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 liegt der Schwerpunkt auf der zweiten Lösung, 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 ein Ereignis verpasst wurde oder der Abostatus überprüft werden muss.

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

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

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

Ä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 des Timers an Ihre Bedürfnisse anpassen.

lib/app_store_purchase_handler.dart

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

Erstelle dann die Methode _pullStatus so:

lib/app_store_purchase_handler.dart

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

So funktioniert diese Methode:

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

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

bin/server.dart

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

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

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

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


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

App Store-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. Gehen Sie zu Schlüsseltyp > In-App-Kauf.
  3. Tippe auf das Pluszeichen, um einen neuen hinzuzufügen.
  4. Geben Sie einen Namen für den Schlüssel ein, z.B. „Codelab-Schlüssel“.
  5. Laden Sie die P8-Datei mit dem Schlüssel herunter.
  6. Kopieren Sie sie unter dem Namen SubscriptionKey.p8 in den Ordner „Assets“.
  7. Kopieren Sie die Schlüssel-ID aus dem neu erstellten Schlüssel und legen Sie sie in der Datei lib/constants.dart als 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 als Konstante appStoreIssuerId fest.

9540ea9ada3da151.png

Käufe auf dem Gerät verfolgen

Die sicherste Möglichkeit, Käufe zu erfassen, ist die serverseitige, da der Client schwer zu schützen ist. Sie müssen jedoch eine Möglichkeit haben, die Informationen an den Client zurückzugeben, damit die App auf die Informationen zum Abostatus reagieren kann. Wenn Sie die Käufe in Firestore speichern, können Sie die Daten ganz einfach mit dem Client synchronisieren und automatisch auf dem neuesten Stand halten.

Sie haben bereits das IAPRepo 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 „wahr“ bedeutet, wenn es einen Kauf mit productId storeKeySubscription mit einem nicht abgelaufenen Status gibt. Wenn der Nutzer nicht angemeldet ist, ist die Liste leer.

lib/repo/iap_repo.dart

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

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

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

      notifyListeners();
    });
  }

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

lib/logic/dash_purchases.dart

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

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

lib/main.dart

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

Schreiben Sie als Nächstes den Code für die Funktion purchaseUpdate(). In dash_counter.dart, wird mit den Methoden applyPaidMultiplier und removePaidMultiplier der Multiplikator auf 10 bzw. 1 festgelegt, sodass Sie nicht prüfen müssen, ob das Abo bereits angewendet wurde. Wenn sich der Abostatus ändert, aktualisierst du auch den Status des kaufbaren Produkts, damit auf der Kaufseite angezeigt wird, dass es bereits aktiv ist. Legen Sie die Property _beautifiedDashUpgrade fest, je nachdem, 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 Upgrade-Status im Backend-Dienst immer auf dem neuesten Stand ist und mit der App synchronisiert wird. Die App reagiert entsprechend und wendet die Abo- und Upgrade-Funktionen auf Ihr Dash-Klickspiel 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.pngcomplete.

Weitere Informationen finden Sie in den anderen Flutter-Codelabs.