1. Einführung
Wenn Sie einer Flutter-App In-App-Käufe hinzufügen möchten, müssen Sie den App Store und den Play Store richtig einrichten, den Kauf bestätigen und die erforderlichen Berechtigungen wie Abo-Vorteile gewähren.
In diesem Codelab fügen Sie einer App (die Ihnen zur Verfügung gestellt wird) drei Arten von In-App-Käufen hinzu und prüfen diese Käufe mit einem Dart-Backend mit Firebase. Die bereitgestellte App „Dash Clicker“ enthält ein Spiel, in dem das Dash-Maskottchen als Währung verwendet wird. Sie fügen die folgenden Kaufoptionen hinzu:
- Eine wiederholbare Kaufoption für 2.000 Dashes auf einmal.
- Einmaliger Kauf eines Upgrades, um das alte Dash in ein modernes Dash umzuwandeln.
- Ein Abo, mit dem sich die Anzahl der automatisch generierten Klicks verdoppelt.
Bei der ersten Kaufoption erhält der Nutzer direkt 2.000 Dashes. Diese sind direkt für den Nutzer verfügbar und können mehrmals gekauft werden. Es wird als „Verbrauchsgut“ bezeichnet, da es direkt und mehrmals verwendet werden kann.
Die zweite Option aktualisiert das Dash zu einem schöneren Dash. Sie müssen es nur einmal kaufen und es ist dann für immer verfügbar. Ein solcher Kauf wird als „nicht verbrauchbar“ bezeichnet, da er nicht von der App verbraucht werden kann, sondern für immer gültig ist.
Die dritte und letzte Kaufoption ist ein Abo. Solange das Abo aktiv ist, erhält der Nutzer schneller Dashes. Wenn er das Abo jedoch kündigt, fallen die Vorteile weg.
Der Back-End-Dienst (ebenfalls für Sie bereitgestellt) wird als Dart-App ausgeführt, prüft, ob die Käufe getätigt wurden, und speichert sie mit Firestore. Firestore wird verwendet, um den Prozess zu vereinfachen. In Ihrer Produktions-App können Sie jedoch jeden beliebigen Back-End-Dienst verwenden.
Aufgaben
- Sie erweitern eine App, um den Kauf von Verbrauchsmaterialien und Abos zu unterstützen.
- Außerdem erweitern Sie eine Dart-Backend-App, um die gekauften Artikel zu überprüfen und zu speichern.
Lerninhalte
- So konfigurieren Sie den App Store und den Play Store mit kaufbaren Produkten.
- Mit den Stores kommunizieren, um Käufe zu bestätigen und in Firestore zu speichern
- So verwalten Sie Käufe in Ihrer App.
Voraussetzungen
- Android Studio
- Xcode (für die iOS-Entwicklung)
- Flutter SDK
2. Entwicklungsumgebung einrichten
Laden Sie den Code herunter und ändern Sie den Paket-Identifikator für iOS und den Paketnamen für Android, um mit diesem Codelab zu beginnen.
Code herunterladen
Verwenden Sie den folgenden Befehl, um das GitHub-Repository über die Befehlszeile zu klonen:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Wenn Sie das CLI-Tool von GitHub installiert haben, verwenden Sie den folgenden Befehl:
gh repo clone flutter/codelabs flutter-codelabs
Der Beispielcode wird in ein Verzeichnis flutter-codelabs
geklont, das den Code für eine Sammlung von Codelabs enthält. Der Code für dieses Codelab befindet sich in flutter-codelabs/in_app_purchases
.
Die Verzeichnisstruktur unter flutter-codelabs/in_app_purchases
enthält eine Reihe von Snapshots, die zeigen, wie die Dateien am Ende jedes benannten Schritts aussehen sollten. Der Startcode befindet sich in Schritt 0. So rufen Sie ihn auf:
cd flutter-codelabs/in_app_purchases/step_00
Wenn Sie einen Schritt überspringen oder sehen möchten, wie etwas nach einem Schritt aussehen sollte, suchen Sie im Verzeichnis, das nach dem entsprechenden Schritt benannt ist. Der Code des letzten Schritts befindet sich im Ordner complete
.
Startprojekt einrichten
Öffnen Sie das Starterprojekt aus step_00/app
in Ihrer bevorzugten IDE. Für die Screenshots haben wir Android Studio verwendet, aber Visual Studio Code ist auch eine gute Option. Achten Sie darauf, dass in beiden Editoren die neuesten Dart- und Flutter-Plug-ins installiert sind.
Die Apps, die Sie erstellen, müssen mit dem App Store und dem Play Store kommunizieren, um zu erfahren, welche Produkte zu welchem Preis verfügbar sind. Jede App wird durch eine eindeutige ID identifiziert. Im iOS App Store wird sie als Paket-ID und im Android Play Store als Anwendungs-ID bezeichnet. Diese IDs werden in der Regel mit einer umgekehrten Domainnamenschreibweise erstellt. Wenn Sie beispielsweise eine In-App-Kauf-App für flutter.dev erstellen, verwenden Sie dev.flutter.inapppurchase
. Überlegen Sie sich eine Kennung für Ihre App, die Sie jetzt in den Projekteinstellungen festlegen.
Richten Sie zuerst die Paket-ID für iOS ein. Öffnen Sie dazu die Datei Runner.xcworkspace
in der Xcode App.
In der Ordnerstruktur von Xcode befindet sich das Runner-Projekt oben und die Ziele Flutter, Runner und Products befinden sich unter dem Runner-Projekt. Doppelklicken Sie auf Runner, um die Projekteinstellungen zu bearbeiten, und klicken Sie auf Signieren & Funktionen. Geben Sie die gerade ausgewählte Bundle-ID in das Feld Team ein, um Ihr Team festzulegen.
Sie können Xcode jetzt schließen und zu Android Studio zurückkehren, um die Konfiguration für Android abzuschließen. Öffnen Sie dazu die Datei build.gradle.kts
unter android/app,
und ändern Sie applicationId
(in der Abbildung unten in Zeile 24) in die Anwendungs-ID, die mit der iOS-Paket-ID identisch ist. Die IDs für die iOS- und Android-Stores müssen nicht identisch sein. Es ist jedoch weniger fehleranfällig, wenn sie identisch sind. Daher verwenden wir in diesem Codelab auch identische IDs.
3. Plug-in installieren
In diesem Teil des Codelabs installieren Sie das Plug-in „in_app_purchase“.
Abhängigkeit in „pubspec“ hinzufügen
Fügen Sie in_app_purchase
der pubspec-Datei hinzu, indem Sie in_app_purchase
den Abhängigkeiten Ihres Projekts hinzufügen:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) flutter_lints 5.0.0 (6.0.0 available) + in_app_purchase 3.2.3 + in_app_purchase_android 0.4.0+3 + in_app_purchase_platform_interface 1.4.0 + in_app_purchase_storekit 0.4.4 + json_annotation 4.9.0 lints 5.1.1 (6.0.0 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) provider 6.1.5 (6.1.5+1 available) test_api 0.7.6 (0.7.7 available) Changed 5 dependencies! 7 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Öffnen Sie pubspec.yaml
und prüfen Sie, ob in_app_purchase
jetzt als Eintrag unter dependencies
und in_app_purchase_platform_interface
unter dev_dependencies
aufgeführt ist.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^6.0.0
cupertino_icons: ^1.0.8
firebase_auth: ^6.0.1
firebase_core: ^4.0.0
google_sign_in: ^7.1.1
http: ^1.5.0
intl: ^0.20.2
provider: ^6.1.5
logging: ^1.3.0
in_app_purchase: ^3.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
in_app_purchase_platform_interface: ^1.4.0
4. App Store einrichten
Wenn Sie In-App-Produkte einrichten und auf iOS-Geräten testen möchten, müssen Sie eine neue App im App Store erstellen und dort kaufbare Produkte anlegen. Sie müssen nichts veröffentlichen oder die App zur Überprüfung an Apple senden. Dazu benötigen Sie ein Entwicklerkonto. Falls Sie noch keine haben, melden Sie sich für das Apple Developer Program an.
Vereinbarungen zu kostenpflichtigen Apps
Wenn Sie In-App-Käufe verwenden möchten, benötigen Sie außerdem eine aktive Vereinbarung für kostenpflichtige Apps in App Store Connect. Rufen Sie https://appstoreconnect.apple.com/ auf und klicken Sie auf Agreements, Tax, and Banking (Vereinbarungen, Steuern und Bankwesen).
Hier werden Vereinbarungen für kostenlose und kostenpflichtige Apps angezeigt. Der Status kostenloser Apps sollte „Aktiv“ sein und der Status kostenpflichtiger Apps „Neu“. Lesen Sie die Nutzungsbedingungen, akzeptieren Sie sie und geben Sie alle erforderlichen Informationen ein.
Wenn alles richtig eingestellt ist, ist der Status für kostenpflichtige Apps „Aktiv“. Das ist sehr wichtig, da Sie In-App-Käufe ohne aktive Vereinbarung nicht testen können.
App-ID registrieren
Erstellen Sie im Apple Developer-Portal eine neue Kennung. Rufen Sie developer.apple.com/account/resources/identifiers/list auf und klicken Sie neben der Überschrift Identifiers auf das Pluszeichen.
App-IDs auswählen
App auswählen
Geben Sie eine Beschreibung an und legen Sie die Paket-ID auf denselben Wert fest, der zuvor in Xcode festgelegt wurde.
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.
Weitere Informationen zum Erstellen einer neuen App und zum Verwalten von Vereinbarungen finden Sie in der App Store Connect-Hilfe.
Zum Testen der In-App-Käufe benötigen Sie einen Sandbox-Testnutzer. Dieser Testnutzer sollte nicht mit iTunes verknüpft sein. Er wird nur zum Testen von In-App-Käufen verwendet. Sie können keine E‑Mail-Adresse verwenden, die bereits für ein Apple-Konto verwendet wird. Gehen Sie unter Users and Access (Nutzer und Zugriff) zu Sandbox, um ein neues Sandbox-Konto zu erstellen oder die vorhandenen Sandbox-Apple-IDs zu verwalten.
Jetzt können Sie Ihren Sandbox-Nutzer auf Ihrem iPhone einrichten. Gehen Sie dazu zu Einstellungen > Entwickler > Sandbox-Apple-Konto.
In-App-Käufe konfigurieren
Jetzt konfigurieren Sie die drei kaufbaren Artikel:
dash_consumable_2k
: Ein Verbrauchsgut, das mehrmals gekauft werden kann und dem Nutzer pro Kauf 2.000 Dashes (die In-App-Währung) gewährt.dash_upgrade_3d
: Ein nicht verbrauchbarer „Upgrade“-Kauf, der nur einmal gekauft werden kann und dem Nutzer einen kosmetisch anderen Dash zum Klicken bietet.dash_subscription_doubler
: Ein Abo, das dem Nutzer für die Dauer des Abos doppelt so viele Striche pro Klick gewährt.
Rufen Sie In-App-Käufe auf.
Erstellen Sie Ihre In-App-Käufe mit den angegebenen IDs:
- Richten Sie
dash_consumable_2k
als Verbrauchsmaterial ein. Verwenden Siedash_consumable_2k
als Produkt-ID. Der Referenzname wird nur in App Store Connect verwendet. Legen Sie ihn einfach aufdash consumable 2k
fest.Verfügbarkeit einrichten Das Produkt muss im Land des Sandbox-Nutzers verfügbar sein.
Fügen Sie Preise hinzu und legen Sie den Preis auf
$1.99
oder den entsprechenden Betrag in einer anderen Währung fest.Fügen Sie die Übersetzungen für den Kauf hinzu. Rufen Sie den Kauf
Spring is in the air
mit2000 dashes fly out
als Beschreibung auf.Fügen Sie einen Screenshot der Rezension hinzu. Der Inhalt spielt keine Rolle, es sei denn, das Produkt wird zur Überprüfung gesendet. Er ist jedoch erforderlich, damit das Produkt den Status „Bereit zum Einreichen“ erhält. Das ist notwendig, wenn die App Produkte aus dem App Store abruft.
- Richten Sie
dash_upgrade_3d
als nicht verbrauchbares Produkt ein. Verwenden Siedash_upgrade_3d
als Produkt-ID. Legen Siedash upgrade 3d
als Referenzname fest. Rufen Sie den Kauf3D Dash
mitBrings your dash back to the future
als Beschreibung auf. Legen Sie den Preis auf$0.99
fest. Konfigurieren Sie die Verfügbarkeit und laden Sie den Screenshot der Rezension auf dieselbe Weise wie für das Produktdash_consumable_2k
hoch. - Richten Sie
dash_subscription_doubler
als Abo mit automatischer Verlängerung ein. Der Ablauf für Abos ist etwas anders. Zuerst müssen Sie eine Abogruppe erstellen. Wenn mehrere Abos zur selben Gruppe gehören, kann ein Nutzer nur eines davon gleichzeitig abonnieren. Er kann aber zwischen diesen Abos wechseln. Nenne diese Gruppe einfachsubscriptions
.Fügen Sie der Abo-Gruppe Lokalisierungen hinzu.
Als Nächstes erstellen Sie das Abo. Legen Sie den Referenznamen auf
dash subscription doubler
und die Produkt-ID aufdash_subscription_doubler
fest.Wählen Sie als Nächstes die Abodauer von einer Woche und die Lokalisierungen aus. Nenne dieses Abo
Jet Engine
mit der BeschreibungDoubles your clicks
. Legen Sie den Preis auf$0.49
fest. Konfigurieren Sie die Verfügbarkeit und laden Sie den Screenshot der Rezension auf dieselbe Weise wie für das Produktdash_consumable_2k
hoch.
Die Produkte sollten jetzt in den Listen angezeigt werden:
5. Play Store einrichten
Wie beim App Store benötigen Sie auch für den Play Store ein Entwicklerkonto. Falls Sie noch kein Konto haben, registrieren Sie sich.
Neue App erstellen
So erstellen Sie eine neue App in der Google Play Console:
- Öffnen Sie die Play Console.
- Wählen Sie Alle Apps > App erstellen aus.
- 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.
- Geben Sie an, dass es sich bei Ihrer Anwendung um ein Spiel handelt. Sie können dies später ändern.
- Geben Sie an, ob Ihre App kostenlos oder kostenpflichtig ist.
- Machen Sie die erforderlichen Angaben für die Deklarationen „Inhaltsrichtlinien“ und „Exportbestimmungen der USA“.
- Wählen Sie App erstellen aus.
Nachdem Sie Ihre App erstellt haben, rufen Sie das Dashboard auf und erledigen Sie alle Aufgaben im Bereich App einrichten. Hier geben Sie einige Informationen zu Ihrer App an, z. B. die Altersfreigabe und Screenshots.
Anwendung signieren
Damit Sie In-App-Käufe testen können, muss mindestens ein Build bei Google Play hochgeladen sein.
Dazu muss Ihr Release-Build mit einem anderen Schlüssel als den Debug-Schlüsseln signiert sein.
Schlüsselspeicher erstellen
Wenn Sie bereits einen Keystore haben, fahren Sie mit dem nächsten Schritt fort. Falls nicht, erstellen Sie eine, indem Sie den folgenden Befehl in der Befehlszeile ausführen.
Verwenden Sie unter Mac/Linux den folgenden Befehl:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Verwenden Sie unter Windows den folgenden Befehl:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Mit diesem Befehl wird die Datei key.jks
in Ihrem Basisverzeichnis gespeichert. Wenn Sie die Datei an einem anderen Ort speichern möchten, ändern Sie das Argument, das Sie an den Parameter -keystore
übergeben. Behalten
keystore
Datei privat halten und nicht in die öffentliche Versionsverwaltung einchecken!
Auf den Schlüsselspeicher über die App verweisen
Erstellen Sie eine Datei mit dem Namen <your app dir>/android/key.properties
, die einen Verweis auf Ihren Schlüsselspeicher enthält:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
Signierung in Gradle konfigurieren
Konfigurieren Sie die Signierung für Ihre App, indem Sie die Datei <your app dir>/android/app/build.gradle.kts
bearbeiten.
Fügen Sie die Keystore-Informationen aus Ihrer Properties-Datei vor dem android
-Block hinzu:
import java.util.Properties
import java.io.FileInputStream
plugins {
// omitted
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Laden Sie die Datei key.properties
in das Objekt keystoreProperties
.
Aktualisieren Sie den Block buildTypes
auf:
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Konfigurieren Sie den signingConfigs
-Block in der build.gradle.kts
-Datei Ihres Moduls mit den Informationen zur Signierungskonfiguration:
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Release-Builds Ihrer App werden jetzt automatisch signiert.
Weitere Informationen zum Signieren Ihrer App finden Sie unter App signieren auf developer.android.com.
Ersten Build hochladen
Nachdem Ihre App für die Signierung konfiguriert wurde, sollten Sie Ihre Anwendung mit dem folgenden Befehl erstellen können:
flutter build appbundle
Mit diesem Befehl wird standardmäßig ein Release-Build generiert. Die Ausgabe befindet sich unter <your app dir>/build/app/outputs/bundle/release/
.
Rufen Sie im Dashboard der Google Play Console Test und Release > Test > Geschlossener Test auf und erstellen Sie einen neuen Release für den geschlossenen Test.
Laden Sie als Nächstes das app-release.aab
-App-Bundle hoch, das mit dem Build-Befehl generiert wurde.
Klicken Sie auf Speichern und dann auf Release überprüfen.
Klicken Sie abschließend auf Einführung in den geschlossenen Test starten, um den Release für den geschlossenen Test zu aktivieren.
Testnutzer einrichten
Damit Ihre Tester In-App-Käufe testen können, müssen ihre Google-Konten an zwei Stellen in der Google Play Console hinzugefügt werden:
- Auf den jeweiligen Test-Track (interner Test)
- Als Lizenztester
Fügen Sie den Tester zuerst dem internen Test-Track hinzu. Kehren Sie zu Testen und veröffentlichen > Testen > Interner Test zurück und klicken Sie auf den Tab Tester.
Klicken Sie auf E-Mail-Liste erstellen, um eine neue E-Mail-Liste zu erstellen. Geben Sie der Liste einen Namen und fügen Sie die E-Mail-Adressen der Google-Konten hinzu, die Zugriff auf In-App-Testkäufe benötigen.
Klicken Sie dann das Kästchen für die Liste an und wählen Sie Änderungen speichern aus.
Fügen Sie dann die Lizenztester hinzu:
- Kehren Sie in der Google Play Console zur Ansicht Alle Apps zurück.
- Rufen Sie Einstellungen > Lizenztest auf.
- Fügen Sie die E-Mail-Adressen der Tester hinzu, die In-App-Käufe testen müssen.
- Legen Sie Lizenzantwort auf
RESPOND_NORMALLY
fest. - Klicken Sie auf ROLLE ERSTELLEN.
In-App-Käufe konfigurieren
Als Nächstes konfigurieren Sie die Artikel, die in der App gekauft werden können.
Wie im App Store müssen Sie drei verschiedene Käufe definieren:
dash_consumable_2k
: Ein Verbrauchsgut, das mehrmals gekauft werden kann und dem Nutzer pro Kauf 2.000 Dashes (die In-App-Währung) gewährt.dash_upgrade_3d
: Ein nicht verbrauchbarer „Upgrade“-Kauf, der nur einmal gekauft werden kann und dem Nutzer ein kosmetisch anderes Dash zum Klicken bietet.dash_subscription_doubler
: Ein Abo, das dem Nutzer für die Dauer des Abos doppelt so viele Striche pro Klick gewährt.
Fügen Sie zuerst das Einmalprodukt und das nicht verbrauchbare Produkt hinzu.
- Rufen Sie die Google Play Console auf und wählen Sie Ihre Anwendung aus.
- Rufen Sie Monetarisieren > Produkte > In‑App-Produkte auf.
- Klicken Sie auf Produkt erstellen
- Geben Sie alle erforderlichen Informationen zu Ihrem Produkt ein. Achten Sie darauf, dass die Produkt-ID genau mit der ID übereinstimmt, die Sie verwenden möchten.
- Klicken Sie auf Speichern.
- Klicken Sie auf Aktivieren.
- Wiederholen Sie den Vorgang für den nicht verbrauchbaren Kauf „Upgrade“.
Fügen Sie als Nächstes das Abo hinzu:
- Rufen Sie die Google Play Console auf und wählen Sie Ihre Anwendung aus.
- Rufen Sie Monetarisieren > Produkte > Abos auf.
- Klicken Sie auf Abo erstellen.
- Geben Sie alle erforderlichen Informationen für Ihr Abo ein. Achten Sie darauf, dass die Produkt-ID genau mit der ID übereinstimmt, die Sie verwenden möchten.
- Klicken Sie auf Speichern.
Ihre Käufe sollten jetzt in der Play Console eingerichtet sein.
6. Firebase einrichten
In diesem Codelab verwenden Sie einen Back-End-Dienst, um die Käufe von Nutzern zu bestätigen und nachzuverfolgen.
Die Verwendung eines Backend-Dienstes hat mehrere Vorteile:
- Sie können Transaktionen sicher bestätigen.
- Sie können auf Abrechnungsereignisse aus den App-Shops reagieren.
- Sie können die Käufe in einer Datenbank erfassen.
- Nutzer können Ihre App nicht dazu bringen, Premium-Funktionen bereitzustellen, indem sie die Systemuhr zurückstellen.
Es gibt viele Möglichkeiten, einen Back-End-Dienst einzurichten. In diesem Fall verwenden Sie Cloud Functions und Firestore mit Firebase von Google.
Das Schreiben des Back-Ends wird in diesem Codelab nicht behandelt. Der Startcode enthält bereits ein Firebase-Projekt, das grundlegende Käufe abwickelt.
Firebase-Plug-ins sind ebenfalls in der Starter-App enthalten.
Sie müssen nur noch ein eigenes Firebase-Projekt erstellen, sowohl die App als auch das Backend für Firebase konfigurieren und das Backend schließlich bereitstellen.
Firebase-Projekt erstellen
Rufen Sie die Firebase Console auf und erstellen Sie ein neues Firebase-Projekt. Nennen Sie das Projekt in diesem Beispiel „Dash Clicker“.
In der Backend-App verknüpfen Sie Käufe mit einem bestimmten Nutzer. Daher ist eine Authentifizierung erforderlich. Verwenden Sie dazu das Authentifizierungsmodul von Firebase mit Google Log-in.
- Rufen Sie im Firebase-Dashboard Authentifizierung auf und aktivieren Sie die Funktion bei Bedarf.
- Rufen Sie den Tab Anmeldemethode auf und aktivieren Sie den Anmeldeanbieter Google.
Da Sie auch die Firestore-Datenbank von Firebase verwenden, aktivieren Sie diese ebenfalls.
Cloud Firestore-Regeln festlegen:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
Firebase für Flutter einrichten
Die empfohlene Methode zum Installieren von Firebase in der Flutter-App ist die Verwendung der FlutterFire CLI. Folgen Sie der Anleitung auf der Einrichtungsseite.
Wählen Sie beim Ausführen von „flutterfire configure“ das Projekt aus, das Sie gerade im vorherigen Schritt erstellt haben.
$ flutterfire configure i Found 5 Firebase projects. ? Select a Firebase project to configure your Flutter application with › ❯ in-app-purchases-1234 (in-app-purchases-1234) other-flutter-codelab-1 (other-flutter-codelab-1) other-flutter-codelab-2 (other-flutter-codelab-2) other-flutter-codelab-3 (other-flutter-codelab-3) other-flutter-codelab-4 (other-flutter-codelab-4) <create a new project>
Aktivieren Sie dann iOS und Android, indem Sie die beiden Plattformen auswählen.
? Which platforms should your configuration support (use arrow keys & space to select)? › ✔ android ✔ ios macos web
Wenn Sie gefragt werden, ob Sie „firebase_options.dart“ überschreiben möchten, wählen Sie „Ja“ aus.
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Firebase für Android einrichten: Weitere Schritte
Rufen Sie im Firebase-Dashboard die Projektübersicht auf, wählen Sie Einstellungen und dann den Tab Allgemein aus.
Scrolle nach unten zu Meine Apps und wähle die App dashclicker (android) aus.
Damit Google Sign-in im Debug-Modus verwendet werden kann, müssen Sie den SHA-1-Hash-Fingerabdruck Ihres Debug-Zertifikats angeben.
Hash des Debugging-Signaturzertifikats abrufen
Wechseln Sie im Stammverzeichnis Ihres Flutter-App-Projekts in den Ordner android/
und generieren Sie dann einen Signierungsbericht.
cd android ./gradlew :app:signingReport
Es wird eine lange Liste mit Signaturschlüsseln angezeigt. Da Sie nach dem Hash für das Debug-Zertifikat suchen, suchen Sie nach dem Zertifikat, bei dem die Eigenschaften Variant
und Config
auf debug
festgelegt sind. Der Keystore befindet sich wahrscheinlich in Ihrem Home-Ordner unter .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
Kopieren Sie den SHA-1-Hash und füllen Sie das letzte Feld im Modal-Dialogfeld für die App-Einreichung aus.
Führen Sie zum Schluss den Befehl flutterfire configure
noch einmal aus, um die App zu aktualisieren und die Signaturkonfiguration einzuschließen.
$ flutterfire configure ? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes ✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes
Firebase für iOS einrichten: Weitere Schritte
Öffnen Sie ios/Runner.xcworkspace
mit Xcode
. Oder mit der IDE Ihrer Wahl.
Klicken Sie in VSCode mit der rechten Maustaste auf den Ordner ios/
und dann auf open in xcode
.
Klicken Sie in Android Studio mit der rechten Maustaste auf den Ordner ios/
und dann auf flutter
, gefolgt von der Option open iOS module in Xcode
.
Damit das Google-Log-in unter iOS möglich ist, fügen Sie die Konfigurationsoption CFBundleURLTypes
Ihren Build-plist
-Dateien hinzu. Weitere Informationen finden Sie in der Dokumentation zum google_sign_in
-Paket. In diesem Fall lautet die Datei ios/Runner/Info.plist
.
Das Schlüssel/Wert-Paar wurde bereits hinzugefügt, aber seine Werte müssen ersetzt werden:
- Rufen Sie den Wert für
REVERSED_CLIENT_ID
aus der DateiGoogleService-Info.plist
ab, ohne das umgebende<string>..</string>
-Element. - Ersetzen Sie den Wert in Ihrer
ios/Runner/Info.plist
-Datei unter dem SchlüsselCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
Die Firebase-Einrichtung ist nun abgeschlossen.
7. Auf Kaufaktualisierungen warten
In diesem Teil des Codelabs bereiten Sie die App für den Kauf der Produkte vor. Dazu gehört, dass nach dem Start der App auf Kaufaktualisierungen und Fehler gewartet wird.
Kauf-Updates anhören
Suchen Sie in main.dart,
nach dem Widget MyHomePage
mit einem Scaffold
, das ein BottomNavigationBar
mit zwei Seiten enthält. Auf dieser Seite werden auch drei Provider
s für DashCounter
, DashUpgrades,
und DashPurchases
erstellt. DashCounter
verfolgt die aktuelle Anzahl der Striche und erhöht sie automatisch. DashUpgrades
verwaltet die Upgrades, die Sie mit Dashes kaufen können. In diesem Codelab geht es um DashPurchases
.
Standardmäßig wird das Objekt eines Anbieters definiert, wenn es zum ersten Mal angefordert wird. Dieses Objekt wartet direkt beim Start der App auf Kaufaktualisierungen. Deaktivieren Sie daher das Lazy Loading für dieses Objekt mit lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
Außerdem benötigen Sie eine Instanz von InAppPurchaseConnection
. Damit die App jedoch testbar bleibt, müssen Sie die Verbindung irgendwie simulieren. Erstellen Sie dazu eine Instanzmethode, die im Test überschrieben werden kann, und fügen Sie sie main.dart
hinzu.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
Aktualisieren Sie den Test so:
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable({
required PurchaseParam purchaseParam,
bool autoConsume = true,
}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(
ProductDetailsResponse(productDetails: [], notFoundIDs: []),
);
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
Rufen Sie in lib/logic/dash_purchases.dart
den Code für DashPurchasesChangeNotifier
auf. Derzeit gibt es nur DashCounter
, die du deinen gekauften Dashes hinzufügen kannst.
Fügen Sie eine Stream-Abo-Property (_subscription
vom Typ StreamSubscription<List<PurchaseDetails>> _subscription;
), IAPConnection.instance,
und die Importe hinzu. Der resultierende Code sollte so aussehen:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
Das Keyword late
wird _subscription
hinzugefügt, weil _subscription
im Konstruktor initialisiert wird. Dieses Projekt ist standardmäßig so eingerichtet, dass Nullwerte nicht zulässig sind (non-nullable by default, NNBD). Das bedeutet, dass Eigenschaften, die nicht als nullable deklariert sind, einen Wert ungleich null haben müssen. Mit dem Qualifikator late
können Sie die Definition dieses Werts verzögern.
Rufen Sie im Konstruktor den purchaseUpdated
-Stream ab und beginnen Sie, auf den Stream zu warten. Kündigen Sie im Rahmen der Methode dispose()
das Stream-Abo.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
Die App erhält jetzt die Kaufaktualisierungen. Im nächsten Abschnitt werden Sie einen Kauf tätigen.
Führen Sie vor dem Fortfahren die Tests mit flutter test"
aus, um zu prüfen, ob alles richtig eingerichtet ist.
$ flutter test 00:01 +1: All tests passed!
8. Einkaufen
In diesem Teil des Codelabs ersetzen Sie die vorhandenen Testprodukte durch echte, käufliche Produkte. Diese Produkte werden aus den Geschäften geladen, in einer Liste angezeigt und durch Tippen auf das Produkt gekauft.
PurchasableProduct anpassen
PurchasableProduct
zeigt ein Mock-Produkt. Aktualisieren Sie die Klasse PurchasableProduct
in purchasable_product.dart
mit dem folgenden Code, damit tatsächliche Inhalte angezeigt werden:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus { purchasable, purchased, pending }
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
Entfernen Sie in dash_purchases.dart,
die Dummy-Käufe und ersetzen Sie sie durch eine leere Liste, List<PurchasableProduct> products = [];
.
Verfügbare Käufe laden
Damit ein Nutzer einen Kauf tätigen kann, müssen Sie die Käufe aus dem Store laden. Prüfen Sie zuerst, ob der Store verfügbar ist. Wenn der Store nicht verfügbar ist, wird dem Nutzer eine Fehlermeldung angezeigt, wenn storeState
auf notAvailable
festgelegt ist.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Wenn der Store verfügbar ist, laden Sie die verfügbaren Käufe. Bei der bisherigen Einrichtung von Google Play und App Store sollten storeKeyConsumable
, storeKeySubscription,
und storeKeyUpgrade
angezeigt werden. Wenn ein erwarteter Kauf nicht verfügbar ist, geben Sie diese Informationen in der Konsole aus. Möglicherweise möchten Sie diese Informationen auch an den Backend-Dienst senden.
Die Methode await iapConnection.queryProductDetails(ids)
gibt sowohl die IDs, die nicht gefunden wurden, als auch die gefundenen kaufbaren Produkte zurück. Verwenden Sie den Wert productDetails
aus der Antwort, um die Benutzeroberfläche zu aktualisieren, und legen Sie StoreState
auf available
fest.
lib/logic/dash_purchases.dart
import '../constants.dart';
// ...
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails
.map((e) => PurchasableProduct(e))
.toList();
storeState = StoreState.available;
notifyListeners();
}
Rufen Sie die Funktion loadPurchases()
im Konstruktor auf:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases(); // Add this line
}
Ändern Sie schließlich den Wert des Felds storeState
von StoreState.available
in StoreState.loading:
.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Käuflich erwerbbare Produkte anzeigen
Sehen Sie sich die Datei purchase_page.dart
an. Im PurchasePage
-Widget wird je nach StoreState
_PurchasesLoading
, _PurchaseList,
oder _PurchasesNotAvailable,
angezeigt. Das Widget zeigt auch die bisherigen Käufe des Nutzers an, die im nächsten Schritt verwendet werden.
Im _PurchaseList
-Widget wird die Liste der kaufbaren Produkte angezeigt und eine Kaufanfrage an das DashPurchases
-Objekt gesendet.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map(
(product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
},
),
)
.toList(),
);
}
}
Wenn die Produkte richtig konfiguriert sind, sollten sie in den Android- und iOS-Stores angezeigt werden. Es kann einige Zeit dauern, bis die Käufe in den jeweiligen Konsolen verfügbar sind.
Gehen Sie zurück zu dash_purchases.dart
und implementieren Sie die Funktion zum Kaufen eines Produkts. Sie müssen nur die Verbrauchsmaterialien von den nicht verbrauchbaren Artikeln trennen. Das Upgrade und die Aboprodukte sind nicht verbrauchbar.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails,
'${product.id} is not a known product',
);
}
}
Bevor Sie fortfahren, erstellen Sie die Variable _beautifiedDashUpgrade
und aktualisieren Sie den beautifiedDash
-Getter, damit er darauf verweist.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
Die _onPurchaseUpdate
-Methode empfängt die Kaufaktualisierungen, aktualisiert den Status des Produkts, das auf der Kaufseite angezeigt wird, und wendet den Kauf auf die Zählerlogik an. Es ist wichtig, completePurchase
nach der Verarbeitung des Kaufs aufzurufen, damit der Store weiß, dass der Kauf korrekt verarbeitet wurde.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. Backend einrichten
Bevor Sie mit dem Tracking und der Bestätigung von Käufen fortfahren, müssen Sie ein Dart-Backend einrichten, das dies unterstützt.
In diesem Abschnitt arbeiten Sie mit dem Ordner dart-backend/
als Stammverzeichnis.
Die folgenden Tools müssen installiert sein:
- Dart
- Firebase CLI
Übersicht über das Basisprojekt
Da einige Teile dieses Projekts für dieses Codelab nicht relevant sind, sind sie im Startcode enthalten. Es ist eine gute Idee, sich den Startercode anzusehen, bevor Sie loslegen, um eine Vorstellung davon zu bekommen, wie Sie die Dinge strukturieren werden.
Dieser Back-End-Code kann lokal auf Ihrem Computer ausgeführt werden. Sie müssen ihn nicht bereitstellen, um ihn zu verwenden. Sie müssen jedoch von Ihrem Entwicklungsgerät (Android oder iPhone) aus eine Verbindung zu dem Computer herstellen können, auf dem der Server ausgeführt wird. Dazu müssen sie sich im selben Netzwerk befinden und Sie müssen die IP-Adresse Ihres Computers kennen.
Versuchen Sie, den Server mit dem folgenden Befehl auszuführen:
$ dart ./bin/server.dart Serving at http://0.0.0.0:8080
Das Dart-Back-End verwendet shelf
und shelf_router
, um API-Endpunkte bereitzustellen. Standardmäßig stellt der Server keine Routen bereit. Später erstellen Sie eine Route, um den Kaufbestätigungsprozess zu verarbeiten.
Ein Teil, der bereits im Startcode enthalten ist, ist IapRepository
in lib/iap_repository.dart
. Da das Erlernen der Interaktion mit Firestore oder Datenbanken im Allgemeinen nicht als relevant für dieses Codelab angesehen wird, enthält der Startcode Funktionen zum Erstellen oder Aktualisieren von Käufen in Firestore sowie alle Klassen für diese Käufe.
Firebase-Zugriff einrichten
Für den Zugriff auf Firebase Firestore benötigen Sie einen Zugriffsschlüssel für das Dienstkonto. Sie können einen solchen Schlüssel generieren, indem Sie die Firebase-Projekteinstellungen öffnen, zum Abschnitt Dienstkonten wechseln und dann Neuen privaten Schlüssel generieren auswählen.
Kopieren Sie die heruntergeladene JSON-Datei in den Ordner assets/
und benennen Sie sie in service-account-firebase.json
um.
Google Play-Zugriff einrichten
Wenn Sie auf den Play Store zugreifen möchten, um Käufe zu überprüfen, müssen Sie ein Dienstkonto mit diesen Berechtigungen erstellen und die JSON-Anmeldedaten dafür herunterladen.
- Rufen Sie in der Google Cloud Console die Seite Google Play Android Developer API auf.
Falls Sie in der Google Play Console aufgefordert werden, ein Projekt zu erstellen oder mit einem vorhandenen Projekt zu verknüpfen, tun Sie das zuerst und kehren Sie dann zu dieser Seite zurück.
- Rufen Sie als Nächstes die Seite „Dienstkonten“ auf und klicken Sie auf + Dienstkonto erstellen.
- Geben Sie den Namen des Dienstkontos ein und klicken Sie auf Erstellen und fortfahren.
- Wählen Sie die Rolle Pub/Sub-Abonnent aus und klicken Sie auf Fertig.
- Rufen Sie nach dem Erstellen des Kontos Schlüssel verwalten auf.
- Wählen Sie Schlüssel hinzufügen > Neuen Schlüssel erstellen aus.
- Erstellen Sie einen JSON-Schlüssel und laden Sie ihn herunter.
- Benennen Sie die heruntergeladene Datei in
service-account-google-play.json,
um und verschieben Sie sie in das Verzeichnisassets/
. - Rufen Sie als Nächstes in der Play Console die Seite Nutzer und Berechtigungen auf.
- Klicken Sie auf Neue Nutzer einladen und geben Sie die E-Mail-Adresse des zuvor erstellten Dienstkontos ein. Die E-Mail-Adresse finden Sie in der Tabelle auf der Seite Dienstkonten
- Erteilen Sie die Berechtigungen Finanzdaten einsehen und Bestellungen und Abos verwalten für die Anwendung.
- Klicken Sie auf Nutzer einladen.
Außerdem müssen wir lib/constants.dart,
öffnen und den Wert von androidPackageId
durch die Paket-ID ersetzen, die Sie für Ihre Android-App ausgewählt haben.
Zugriff auf den Apple App Store einrichten
Damit Sie auf den App Store zugreifen können, um Käufe zu bestätigen, müssen Sie ein gemeinsames Secret einrichten:
- Öffnen Sie App Store Connect.
- Rufen Sie Meine Apps auf und wählen Sie Ihre App aus.
- Klicken Sie in der Seitenleistennavigation auf Allgemein > App-Informationen.
- Klicken Sie unter der Überschrift App-Specific Shared Secret (App-spezifisches gemeinsames Geheimnis) auf Manage (Verwalten).
- Generieren Sie ein neues Secret und kopieren Sie es.
- Öffnen Sie
lib/constants.dart,
und ersetzen Sie den Wert vonappStoreSharedSecret
durch das gemeinsame Secret, das Sie gerade generiert haben.
Konfigurationsdatei für Konstanten
Bevor Sie fortfahren, müssen die folgenden Konstanten in der Datei lib/constants.dart
konfiguriert sein:
androidPackageId
: Paket-ID, die unter Android verwendet wird, z. B.com.example.dashclicker
appStoreSharedSecret
: Gemeinsames Secret für den Zugriff auf App Store Connect zur Durchführung der Kaufbestätigung.bundleId
: Die auf iOS verwendete Bundle-ID, z. B.com.example.dashclicker
Die restlichen Konstanten können Sie vorerst ignorieren.
10. Käufe bestätigen
Der allgemeine Ablauf zum Bestätigen von Käufen ist für iOS und Android ähnlich.
In beiden Stores erhält Ihre Anwendung ein Token, wenn ein Kauf erfolgt.
Dieses Token wird von der App an Ihren Backend-Dienst gesendet, der den Kauf dann wiederum mit dem bereitgestellten Token auf den Servern des jeweiligen Stores überprüft.
Der Back-End-Dienst kann dann entscheiden, ob er den Kauf speichert, und der Anwendung antworten, ob der Kauf gültig war oder nicht.
Wenn der Backend-Dienst die Validierung mit den Stores durchführt und nicht die Anwendung auf dem Gerät des Nutzers, können Sie verhindern, dass der Nutzer Zugriff auf Premium-Funktionen erhält, indem er z. B. die Systemuhr zurückstellt.
Flutter-Seite einrichten
Authentifizierung einrichten
Da Sie die Käufe an Ihren Backend-Dienst senden, müssen Sie dafür sorgen, dass der Nutzer beim Kauf authentifiziert wird. Die meiste Authentifizierungslogik ist bereits im Starterprojekt enthalten. Sie müssen nur dafür sorgen, dass auf der PurchasePage
die Schaltfläche „Anmelden“ angezeigt wird, wenn der Nutzer noch nicht angemeldet ist. Fügen Sie den folgenden Code am Anfang der Build-Methode von PurchasePage
ein:
lib/pages/purchase_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart'; // Add this import
import '../model/firebase_state.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart'; // And this one as well
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) { // Update from here
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
} // To here.
// ...
Endpunkt für die Anrufüberprüfung über die App aufrufen
Erstellen Sie in der App die Funktion _verifyPurchase(PurchaseDetails purchaseDetails)
, die den Endpunkt /verifypurchase
in Ihrem Dart-Backend über einen HTTP-POST-Aufruf aufruft.
Sende den ausgewählten Store (google_play
für den Play Store oder app_store
für den App Store), die serverVerificationData
und die productID
. Der Server gibt einen Statuscode zurück, der angibt, ob der Kauf bestätigt wurde.
Konfigurieren Sie in den App-Konstanten die Server-IP-Adresse auf die IP-Adresse Ihres lokalen Computers.
lib/logic/dash_purchases.dart
import 'dart:async';
import 'dart:convert'; // Add this import
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // And this import
import 'package:in_app_purchase/in_app_purchase.dart';
import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart'; // And this one
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier; // Add this line
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter, this.firebaseNotifier) { // Update this line
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Fügen Sie firebaseNotifier
beim Erstellen von DashPurchases
in main.dart:
hinzu.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Fügen Sie im FirebaseNotifier einen Getter für den Nutzer hinzu, damit Sie die Nutzer-ID an die Funktion zum Überprüfen des Kaufs übergeben können.
lib/logic/firebase_notifier.dart
Future<FirebaseFirestore> get firestore async {
var isInitialized = await _isInitialized.future;
if (!isInitialized) {
throw Exception('Firebase is not initialized');
}
return FirebaseFirestore.instance;
}
User? get user => FirebaseAuth.instance.currentUser; // Add this line
Future<void> load() async {
// ...
Fügen Sie der Klasse DashPurchases
die Funktion _verifyPurchase
hinzu. Diese async
-Funktion gibt einen booleschen Wert zurück, der angibt, ob der Kauf validiert wurde.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
Rufen Sie die Funktion _verifyPurchase
in _handlePurchase
auf, kurz bevor Sie den Kauf anwenden. Sie sollten den Kauf erst anwenden, wenn er bestätigt wurde. In einer Produktions-App können Sie dies weiter angeben, um beispielsweise ein Probeabo anzuwenden, wenn der Store vorübergehend nicht verfügbar ist. In diesem Beispiel wird der Kauf jedoch erst angewendet, wenn er bestätigt wurde.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList,
) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
In der App ist jetzt alles bereit, um die Käufe zu validieren.
Backend-Dienst einrichten
Richten Sie als Nächstes das Back-End zum Bestätigen von Käufen ein.
Kauf-Handler erstellen
Da der Bestätigungsablauf für beide Stores nahezu identisch ist, richten Sie eine abstrakte PurchaseHandler
-Klasse mit separaten Implementierungen für jeden Store ein.
Fügen Sie zuerst dem Ordner lib/
eine purchase_handler.dart
-Datei hinzu, in der Sie eine abstrakte PurchaseHandler
-Klasse mit zwei abstrakten Methoden zum Überprüfen von zwei verschiedenen Arten von Käufen definieren: Abos und Nicht-Abos.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
Wie Sie sehen, sind für jede Methode drei Parameter erforderlich:
userId:
Die ID des angemeldeten Nutzers, damit Sie Käufe dem Nutzer zuordnen können.productData:
Daten zum Produkt. Sie werden das gleich definieren.token:
Das Token, das dem Nutzer vom Store bereitgestellt wird.
Um die Verwendung dieser Kauf-Handler zu vereinfachen, fügen Sie außerdem eine verifyPurchase()
-Methode hinzu, die sowohl für Abos als auch für Nicht-Abos verwendet werden kann:
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
Jetzt können Sie verifyPurchase
für beide Fälle aufrufen, haben aber weiterhin separate Implementierungen.
Die Klasse ProductData
enthält grundlegende Informationen zu den verschiedenen käuflichen Produkten, einschließlich der Produkt-ID (manchmal auch als Artikelnummer bezeichnet) und der ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
Das ProductType
kann entweder ein Abo oder kein Abo sein.
lib/products.dart
enum ProductType { subscription, nonSubscription }
Schließlich wird die Liste der Produkte als Map in derselben Datei definiert.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
Definieren Sie als Nächstes einige Platzhalterimplementierungen für den Google Play Store und den Apple App Store. So gehts:
Erstellen Sie lib/google_play_purchase_handler.dart
und fügen Sie eine Klasse hinzu, die die gerade geschriebene PurchaseHandler
erweitert:
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(this.androidPublisher, this.iapRepository);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Derzeit wird true
für die Handler-Methoden zurückgegeben. Sie werden später darauf zurückkommen.
Wie Sie vielleicht bemerkt haben, wird im Konstruktor eine Instanz von IapRepository
verwendet. Der Kauf-Handler verwendet diese Instanz, um später Informationen zu Käufen in Firestore zu speichern. Für die Kommunikation mit Google Play verwenden Sie die bereitgestellte AndroidPublisherApi
.
Wiederholen Sie den Vorgang für den App-Shop-Handler. Erstellen Sie lib/app_store_purchase_handler.dart
und fügen Sie eine Klasse hinzu, die PurchaseHandler
erweitert:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(this.iapRepository);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Sehr gut! Sie haben jetzt zwei Kauf-Handler. Erstellen Sie als Nächstes den API-Endpunkt für die Kaufbestätigung.
Kauf-Handler verwenden
Öffnen Sie bin/server.dart
und erstellen Sie einen API-Endpunkt mit shelf_route
:
bin/server.dart
import 'dart:convert';
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({String userId, String source, ProductData productData, String token})
getPurchaseData(dynamic payload) {
if (payload case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
Der Code führt Folgendes aus:
- Definieren Sie einen POST-Endpunkt, der von der App aufgerufen wird, die Sie zuvor erstellt haben.
- Decodieren Sie die JSON-Nutzlast und extrahieren Sie die folgenden Informationen:
userId
: ID des angemeldeten Nutzerssource
: Verwendeter Store, entwederapp_store
odergoogle_play
.productData
: Wird aus dem zuvor erstelltenproductDataMap
abgerufen.token
: Enthält die Bestätigungsdaten, die an die Stores gesendet werden sollen.
- Rufen Sie die Methode
verifyPurchase
entweder fürGooglePlayPurchaseHandler
oder fürAppStorePurchaseHandler
auf, je nach Quelle. - Wenn die Bestätigung erfolgreich war, gibt die Methode ein
Response.ok
an den Client zurück. - Wenn die Überprüfung fehlschlägt, gibt die Methode einen
Response.internalServerError
an den Client zurück.
Nachdem Sie den API-Endpunkt erstellt haben, müssen Sie die beiden Kauf-Handler konfigurieren. Dazu müssen Sie die Dienstkontoschlüssel laden, die Sie im vorherigen Schritt erhalten haben, und den Zugriff auf die verschiedenen Dienste konfigurieren, einschließlich der Android Publisher API und der Firebase Firestore API. Erstellen Sie dann die beiden Kauf-Handler mit den unterschiedlichen Abhängigkeiten:
bin/server.dart
import 'dart:convert';
import 'dart:io'; // new
import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Android-Käufe bestätigen: Kauf-Handler implementieren
Fahren Sie dann mit der Implementierung des Google Play-Kaufhandlers fort.
Google stellt bereits Dart-Pakete für die Interaktion mit den APIs bereit, die Sie zum Bestätigen von Käufen benötigen. Sie haben sie in der Datei server.dart
initialisiert und verwenden sie jetzt in der Klasse GooglePlayPurchaseHandler
.
Handler für Käufe implementieren, die keine Abos sind:
lib/google_play_purchase_handler.dart
/// Handle non-subscription purchases (one time purchases).
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
Sie können den Handler für den Abo-Kauf auf ähnliche Weise aktualisieren:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order ID exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we don't know the user ID, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
Fügen Sie die folgende Methode hinzu, um das Parsen von Bestell-IDs zu erleichtern, sowie zwei Methoden zum Parsen des Kaufstatus.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
Ihre Google Play-Käufe sollten jetzt bestätigt und in der Datenbank gespeichert werden.
Fahren Sie dann mit App Store-Käufen für iOS fort.
iOS-Käufe bestätigen: Kauf-Handler implementieren
Für die Bestätigung von Käufen im App Store gibt es ein Drittanbieter-Dart-Paket namens app_store_server_sdk
, das den Vorgang vereinfacht.
Erstellen Sie zuerst die ITunesApi
-Instanz. Verwenden Sie die Sandbox-Konfiguration und aktivieren Sie das Logging, um die Fehlersuche zu erleichtern.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(ITunesEnvironment.sandbox(), loggingEnabled: true),
);
Im Gegensatz zu den Google Play-APIs verwendet der App Store für Abos und Nicht-Abos dieselben API-Endpunkte. Das bedeutet, dass Sie für beide Handler dieselbe Logik verwenden können. Führen Sie sie zusammen, damit sie dieselbe Implementierung aufrufen:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
// See next step
}
Implementieren Sie jetzt handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
print('Successfully verified purchase');
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(
NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0'),
),
type: product.type,
status: NonSubscriptionStatus.completed,
),
);
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(
SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0'),
),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0'),
),
status: SubscriptionStatus.active,
),
);
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
Ihre App Store-Käufe sollten jetzt überprüft und in der Datenbank gespeichert werden.
Backend ausführen
An diesem Punkt können Sie dart bin/server.dart
ausführen, um den /verifypurchase
-Endpunkt bereitzustellen.
$ dart bin/server.dart Serving at http://0.0.0.0:8080
11. Käufe im Blick behalten
Wir empfehlen, die Käufe Ihrer Nutzer im Backend-Dienst zu erfassen. Das liegt daran, dass Ihr Backend auf Ereignisse aus dem Store reagieren kann und daher weniger anfällig für veraltete Informationen aufgrund von Caching ist. Außerdem ist es weniger anfällig für Manipulationen.
Richten Sie zuerst die Verarbeitung von Store-Ereignissen im Backend mit dem Dart-Backend ein, das Sie erstellt haben.
Geschäftsereignisse im Backend verarbeiten
Shops können Ihr Backend über alle Abrechnungsereignisse informieren, z. B. wenn Abos verlängert werden. Sie können diese Ereignisse in Ihrem Backend verarbeiten, um die Käufe in Ihrer Datenbank auf dem neuesten Stand zu halten. In diesem Abschnitt richten Sie die Einrichtung sowohl für den Google Play Store als auch für den App Store ein.
Google Play Billing-Ereignisse verarbeiten
Google Play stellt Abrechnungsereignisse über ein sogenanntes Cloud Pub/Sub-Thema bereit. Das sind im Wesentlichen Nachrichtenwarteschlangen, in denen Nachrichten veröffentlicht und aus denen sie abgerufen werden können.
Da es sich um eine Google Play-spezifische Funktion handelt, fügen Sie sie in GooglePlayPurchaseHandler
ein.
Öffnen Sie zuerst lib/google_play_purchase_handler.dart
und fügen Sie den PubsubApi
-Import hinzu:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Übergeben Sie dann PubsubApi
an GooglePlayPurchaseHandler
und ändern Sie den Klassenkonstruktor, um Timer
so zu erstellen:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
Der Timer
ist so konfiguriert, dass die Methode _pullMessageFromPubSub
alle 10 Sekunden aufgerufen wird. Sie können die Dauer nach Belieben anpassen.
Erstellen Sie dann die _pullMessageFromPubSub
.
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(maxMessages: 1000);
final topicName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(ackIds: [id]);
final subscriptionName =
'projects/$googleCloudProjectId/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
Der Code, den Sie gerade hinzugefügt haben, kommuniziert alle zehn Sekunden mit dem Pub/Sub-Thema von Google Cloud und fragt nach neuen Nachrichten. Anschließend wird jede Nachricht in der Methode _processMessage
verarbeitet.
Mit dieser Methode werden die eingehenden Nachrichten decodiert und die aktualisierten Informationen zu jedem Kauf (Abonnements und Nicht-Abonnements) abgerufen. Bei Bedarf wird die vorhandene handleSubscription
- oder handleNonSubscription
-Methode aufgerufen.
Jede Nachricht muss mit der Methode _askMessage
bestätigt werden.
Fügen Sie als Nächstes der Datei server.dart
die erforderlichen Abhängigkeiten hinzu. Fügen Sie der Anmeldedatenkonfiguration PubsubApi.cloudPlatformScope hinzu:
bin/server.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub; // Add this import
final clientGooglePlay = await auth
.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // Add this line
]);
Erstellen Sie dann die PubsubApi-Instanz:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
Übergeben Sie sie schließlich an den GooglePlayPurchaseHandler
-Konstruktor:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // Add this line
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Google Play-Einrichtung
Sie haben den Code zum Empfangen von Abrechnungsereignissen aus dem Pub/Sub-Thema geschrieben, aber das Pub/Sub-Thema nicht erstellt und veröffentlichen auch keine Abrechnungsereignisse. Es ist an der Zeit, das einzurichten.
Erstellen Sie zuerst ein Pub/Sub-Thema:
- Legen Sie den Wert von
googleCloudProjectId
inconstants.dart
auf die ID Ihres Google Cloud-Projekts fest. - Rufen Sie in der Google Cloud Console die Cloud Pub/Sub-Seite auf.
- Achten Sie darauf, dass Sie sich in Ihrem Firebase-Projekt befinden, und klicken Sie auf + Thema erstellen.
- Geben Sie dem neuen Thema einen Namen, der mit dem für
googlePlayPubsubBillingTopic
inconstants.dart
festgelegten Wert identisch ist. In diesem Fall nennen wir ihnplay_billing
. Wenn Sie etwas anderes auswählen, müssen Sieconstants.dart
aktualisieren. Erstellen Sie das Thema. - Klicken Sie in der Liste Ihrer Pub/Sub-Themen auf das Dreipunkt-Menü für das gerade erstellte Thema und dann auf Berechtigungen ansehen.
- Wählen Sie in der Seitenleiste rechts Hauptkonto hinzufügen aus.
- Fügen Sie hier
google-play-developer-notifications@system.gserviceaccount.com
hinzu und weisen Sie ihr die Rolle Pub/Sub-Publisher zu. - Speichern Sie die Berechtigungsänderungen.
- Kopieren Sie den Themennamen des Themas, das Sie gerade erstellt haben.
- Öffnen Sie die Play Console noch einmal und wählen Sie Ihre App aus der Liste Alle Apps aus.
- Scrollen Sie nach unten zu Monetarisieren > Einrichtung der Monetarisierung.
- Geben Sie das vollständige Thema ein und speichern Sie Ihre Änderungen.
Alle Google Play-Abrechnungsereignisse werden jetzt in diesem Thema veröffentlicht.
App Store-Abrechnungsereignisse verarbeiten
Wiederholen Sie den Vorgang für die Abrechnungsereignisse im App Store. Es gibt zwei effektive Möglichkeiten, Aktualisierungen bei Käufen im App Store zu implementieren. Eine Möglichkeit ist die Implementierung eines Webhooks, den Sie Apple zur Verfügung stellen und der für die Kommunikation mit Ihrem Server verwendet wird. Die zweite Möglichkeit, die in diesem Codelab beschrieben wird, besteht darin, eine Verbindung zur App Store Server API herzustellen und die Aboinformationen manuell abzurufen.
In diesem Codelab wird die zweite Lösung behandelt, da Sie Ihren Server für das Internet freigeben müssten, um den Webhook zu implementieren.
In einer Produktionsumgebung sollten Sie idealerweise beides haben. Der Webhook zum Abrufen von Ereignissen aus dem App Store und die Server API, falls Sie ein Ereignis verpasst haben oder den Abostatus noch einmal überprüfen müssen.
Öffnen Sie zuerst lib/app_store_purchase_handler.dart
und fügen Sie die AppStoreServerAPI
-Abhängigkeit hinzu:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI; // Add this member
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // And this parameter
);
Ändern Sie den Konstruktor, um einen Timer hinzuzufügen, der die Methode _pullStatus
aufruft. Dieser Timer ruft die Methode _pullStatus
alle 10 Sekunden auf. Sie können die Dauer dieses Timers an Ihre Bedürfnisse anpassen.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(this.iapRepository, this.appStoreServerAPI) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Erstellen Sie dann die Methode _pullStatus
so:
lib/app_store_purchase_handler.dart
/// Request the App Store for the latest subscription status.
/// Updates all App Store subscriptions in the database.
/// NOTE: This code only handles when a subscription expires as example.
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where(
(element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore,
);
for (final purchase in appStoreSubscriptions) {
final status = await appStoreServerAPI.getAllSubscriptionStatuses(
purchase.orderId,
);
// Obtain all subscriptions for the order ID.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0,
);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(
SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate,
),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
),
);
}
}
}
}
Diese Methode funktioniert so:
- Ruft die Liste der aktiven Abos aus Firestore mithilfe von IapRepository ab.
- Für jede Bestellung wird der Abostatus von der App Store Server API angefordert.
- Ruft die letzte Transaktion für diesen Abo-Kauf ab.
- Prüft das Ablaufdatum.
- Aktualisiert den Abostatus in Firestore. Wenn das Abo abgelaufen ist, wird es entsprechend gekennzeichnet.
Fügen Sie schließlich den gesamten erforderlichen Code hinzu, um den Zugriff auf die App Store Server API zu konfigurieren:
bin/server.dart
import 'package:app_store_server_sdk/app_store_server_sdk.dart'; // Add this import
import 'package:firebase_backend_dart/constants.dart'; // And this one.
// add from here
final subscriptionKeyAppStore = File(
'assets/SubscriptionKey.p8',
).readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // Add this argument
),
};
App Store-Einrichtung
Richten Sie als Nächstes den App Store ein:
- Melden Sie sich in App Store Connect an und wählen Sie Nutzer und Zugriff aus.
- Klicken Sie auf Integrations > Keys > In-App Purchase (Integrationen > Schlüssel > In-App-Kauf).
- Tippe auf das Pluszeichen, um einen neuen hinzuzufügen.
- Geben Sie ihm einen Namen, z. B. „Codelab-Schlüssel“.
- Laden Sie die P8-Datei mit dem Schlüssel herunter.
- Kopieren Sie sie in den Assets-Ordner und geben Sie ihr den Namen
SubscriptionKey.p8
. - Kopieren Sie die Schlüssel-ID aus dem neu erstellten Schlüssel und legen Sie sie in der Datei
lib/constants.dart
für die KonstanteappStoreKeyId
fest. - Kopieren Sie die Aussteller-ID ganz oben in der Schlüsselliste und legen Sie sie in der Datei
lib/constants.dart
auf die KonstanteappStoreIssuerId
fest.
Käufe auf dem Gerät erfassen
Die sicherste Methode zum Erfassen von Käufen ist die serverseitige Erfassung, da der Client schwer zu schützen ist. Sie benötigen jedoch eine Möglichkeit, die Informationen an den Client zurückzugeben, damit die App auf die Informationen zum Abo-Status reagieren kann. Wenn Sie die Käufe in Firestore speichern, können Sie die Daten mit dem Client synchronisieren und automatisch auf dem neuesten Stand halten.
Sie haben IAPRepo bereits in die App aufgenommen. Das ist das Firestore-Repository, das alle Kaufdaten des Nutzers in List<PastPurchase> purchases
enthält. Das Repository enthält auch hasActiveSubscription,
, was „true“ ist, wenn ein Kauf mit productId storeKeySubscription
mit einem Status vorliegt, der nicht abgelaufen ist. Wenn der Nutzer nicht angemeldet ist, ist die Liste leer.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any(
(element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired,
);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
Die gesamte Kauflogik befindet sich in der Klasse DashPurchases
. Dort sollten Abos angewendet oder entfernt werden. Fügen Sie iapRepo
als Property in der Klasse hinzu und weisen Sie iapRepo
im Konstruktor zu. Fügen Sie als Nächstes direkt im Konstruktor einen Listener hinzu und entfernen Sie ihn in der Methode dispose()
. Anfangs kann der Listener einfach eine leere Funktion sein. Da IAPRepo
ein ChangeNotifier
ist und Sie notifyListeners()
jedes Mal aufrufen, wenn sich die Käufe in Firestore ändern, wird die Methode purchasesUpdate()
immer aufgerufen, wenn sich die gekauften Produkte ändern.
lib/logic/dash_purchases.dart
import '../repo/iap_repo.dart'; // Add this import
class DashPurchases extends ChangeNotifier {
DashCounter counter;
FirebaseNotifier firebaseNotifier;
StoreState storeState = StoreState.loading;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [];
IAPRepo iapRepo; // Add this line
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
final iapConnection = IAPConnection.instance;
// Add this.iapRepo as a parameter
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
Future<void> loadPurchases() async {
// Elided.
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate); // Add this line
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
Als Nächstes geben Sie IAPRepo
an den Konstruktor in main.dart.
an. Sie können das Repository mit context.read
abrufen, da es bereits in einem Provider
erstellt wurde.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(), // Add this line
),
lazy: false,
),
Schreiben Sie als Nächstes den Code für die Funktion purchaseUpdate()
. In dash_counter.dart,
wird der Multiplikator mit den Methoden applyPaidMultiplier
und removePaidMultiplier
auf 10 bzw. 1 gesetzt. Sie müssen also nicht prüfen, ob das Abo bereits angewendet wurde. Wenn sich der Abostatus ändert, aktualisieren Sie auch den Status des kaufbaren Produkts, damit auf der Kaufseite angezeigt werden kann, dass es bereits aktiv ist. Legen Sie die _beautifiedDashUpgrade
-Property basierend darauf fest, ob das Upgrade gekauft wurde.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable,
);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
Sie haben jetzt dafür gesorgt, dass der Abo- und Upgradestatus im Backend-Dienst immer aktuell ist und mit der App synchronisiert wird. Die App reagiert entsprechend und wendet die Abo- und Upgradefunktionen auf Ihr Dash-Clicker-Spiel an.
12. Fertig!
Herzlichen Glückwunsch!!! Sie haben das Codelab abgeschlossen. Den vollständigen Code für dieses Codelab finden Sie im Ordner .
Weitere Informationen finden Sie in den anderen Flutter-Codelabs.