In-App-Käufe in eine Flutter-App integrieren
Integrieren Sie In-App-Käufe mit dem in_app_purchase-Plugin in eine Flutter-App. Richten Sie Apple und Google ein, prüfen Sie Transaktionen, stellen Sie Käufe wieder her und testen Sie sicher.
Eine Flutter-App, die unter iOS oder Android digitale Güter oder Abonnements verkauft, muss Zahlungen in der Regel über Apples In-App-Kaufsystem oder Google Play Billing abwickeln. Stripe und eigene Checkout-Lösungen sind dafür nicht zulässig, und beide Stores behalten bei jedem Verkauf eine Provision ein.
Wenn Ihre App im Web bereits Zahlungen über Stripe abwickelt, liegt es nahe, diesen Checkout im mobilen Build wiederzuverwenden. Bei digitalen Gütern endet dieser Plan jedoch meist mit einer Ablehnung durch App Review.
Apples Standardprovision beträgt 30 %. Über das App Store Small Business Program zahlen Entwickler, die im Vorjahr Erlöse von bis zu 1 Million USD erzielt haben, stattdessen 15 %, und zwar sowohl für kostenpflichtige Apps als auch für In-App-Käufe. Google hat keinen einheitlichen Satz; laut der Seite zu den Servicegebühren qualifizieren sich 99 % der gebührenpflichtigen Entwickler für 15 % oder weniger. Für physische Güter und für Dienstleistungen, die außerhalb der App genutzt werden, bleibt Stripe das richtige Werkzeug. Die Regeln sind in Apples App Store Review Guidelines zu In-App-Käufen und in Googles Play-Zahlungsrichtlinie festgelegt. In der EU verpflichtet der Digital Markets Act die Stores, alternative Zahlungsmöglichkeiten zuzulassen; Apple beschreibt seine EU-Mechanismen auf seiner DMA-Entwicklerseite. Im US-Storefront erlauben Apples Richtlinien mittlerweile außerdem Buttons und Links zu externen Kaufoptionen. Die Regeln für externe Zahlungslinks unterscheiden sich je nach Region und ändern sich laufend. Lesen Sie daher die aktuellen Richtlinien beider Stores, bevor Sie Ihren Ablauf entwerfen.
Dieser Artikel behandelt das offizielle Plugin in_app_purchase von Anfang bis Ende: Produkttypen, Store-Einrichtung, eine funktionsfähige Service-Klasse, die Fehlerquellen, durch die Käufe verloren gehen, sowie das Testen. Wenn Sie neu in Flutter sind, beginnen Sie mit dem Flutter-Leitfaden für neue Entwickler.
Das Wichtigste in Kürze
- Digitale Güter, die innerhalb von iOS- und Android-Apps verkauft werden, müssen in der Regel über die Abrechnungssysteme von Apple und Google laufen. Stripe deckt physische Güter und außerhalb der App genutzte Dienstleistungen ab.
buyConsumableundbuyNonConsumablegeben einFuture<bool>zurück, das lediglich anzeigt, dass die Anfrage gestartet wurde. Das eigentliche Ergebnis kommt alsPurchaseDetailsüber denpurchaseStream.- Wird
completePurchasefür einen Kauf, bei dempendingCompletePurchasetrue ist, nicht aufgerufen, erstattet Google Play ihn nach drei Tagen, und der App Store lässt die Transaktion unabgeschlossen. - Apples Review Guidelines verlangen einen Wiederherstellungsmechanismus für wiederherstellbare Käufe.
- Schalten Sie Inhalte erst frei, nachdem Ihr Backend
serverVerificationDataüber die App Store Server API bzw. die Google Play Developer API validiert hat.
Verbrauchsartikel, Nicht-Verbrauchsartikel und Abonnements
Das Flutter-Plugin in_app_purchase unterstützt drei Produkttypen: Verbrauchsartikel (Consumables), Nicht-Verbrauchsartikel (Non-Consumables) und Abonnements. Verbrauchsartikel verwenden buyConsumable. Nicht-Verbrauchsartikel und Abonnements verwenden beide buyNonConsumable. Der Typ bestimmt außerdem, was Ihre App sich merken muss.
| Typ | Kaufmethode | Beispiel | Was Ihre App verwalten muss | Wiederherstellbar? |
|---|---|---|---|---|
| Verbrauchsartikel | buyConsumable | 100 Münzen | Guthaben, gespeichert in Ihrem Backend | Nein |
| Nicht-Verbrauchsartikel | buyNonConsumable | Premium-Freischaltung | Berechtigungs-Flag | Ja |
| Abonnement | buyNonConsumable | Monatliches Pro | Ablaufdatum und Verlängerungsstatus, aus serverseitiger Verifizierung | Ja |
Verbrauchsartikel sind der einzige Typ, den der Store nicht für Sie speichert. Sobald ein Münzpaket verbraucht ist, liegt der einzige Nachweis über das Guthaben in Ihrem eigenen Backend. Eine Berechtigung für einen Nicht-Verbrauchsartikel lässt sich jederzeit über eine Wiederherstellung neu aufbauen. Der Status eines Abonnements ändert sich im Laufe der Zeit durch Verlängerungen, Ausläufe und Erstattungen. Lesen Sie ihn daher aus verifizierten Serverdaten aus und nicht aus einem lokal zwischengespeicherten Flag.
Die Store-Einrichtung ist der Großteil der Arbeit
Der größte Aufwand bei der Integration von In-App-Käufen in Flutter entfällt auf App Store Connect und die Play Console. Der Dart-Teil ist kurz. Solange nicht beide Stores konfiguriert sind, liefert queryProductDetails keine Ergebnisse, egal wie korrekt Ihr Code ist.
Apple (App Store Connect):
- Unterzeichnen Sie die Vereinbarung für kostenpflichtige Apps (Paid Apps Agreement) und hinterlegen Sie Bank- und Steuerdaten.
- Legen Sie jeden In-App-Kauf an, jeweils mit einer Produkt-ID wie
premium_unlock. Abonnements legen Sie innerhalb einer Abonnementgruppe an. - Vervollständigen Sie die Metadaten, sodass jedes Produkt den Status „Bereit zur Einreichung“ (Ready to Submit) erreicht.
Google (Play Console):
- Laden Sie einen Build in einen Test-Track hoch. Der interne Track genügt.
- Legen Sie die Produkte an. Verwenden Sie dieselben IDs wie bei Apple oder ordnen Sie sie im Code zu.
- Aktivieren Sie sie. Google weist darauf hin, dass Produkte veröffentlicht sein müssen, bevor sie getestet werden können.
Wenn queryProductDetails Ihre IDs in notFoundIDs auflistet, liegt die Ursache fast immer in der Store-Konfiguration und nicht im Dart-Code. Typische Ursachen sind eine abweichende ID, ein inaktives Produkt, eine nicht unterzeichnete Paid-Apps-Vereinbarung oder ein fehlender Build in einem Play-Test-Track.
Flutter-Code für In-App-Käufe: Stream, Kauf, Abschluss
Der Ablauf für In-App-Käufe in Flutter umfasst vier Schritte: beim Start den purchaseStream abonnieren, isAvailable prüfen, Produkte mit queryProductDetails laden und anschließend eine Kaufmethode aufrufen. Jedes Ergebnis wird im Stream-Listener verarbeitet.
Fügen Sie die Abhängigkeit hinzu:
dependencies:
in_app_purchase: ^3.3.1
Das Paket in_app_purchase unterstützt Android SDK 24+, iOS 13+ und macOS 10.15+. Die Implementierungen für StoreKit und Play Billing sind endorsed Federated Packages, die automatisch eingebunden werden. Sie müssen sie also nur dann selbst hinzufügen, wenn Sie deren plattformspezifische Klassen verwenden. Auf Apple-Plattformen ist StoreKit 2 der Standard auf Geräten, die es unterstützen.
buyConsumable und buyNonConsumable geben keinen Kauf zurück. Ein häufiger Fehler sieht so aus:
// Wrong: the Future resolves to a bool, not a purchase
final result = await iap.buyNonConsumable(purchaseParam: param);
if (result) unlockPremium(); // true only means the sheet opened
Die korrekte Variante behandelt den bool-Wert als „Anfrage gestartet“ und erledigt die gesamte Freischaltung im Stream-Listener:
final started = await iap.buyNonConsumable(purchaseParam: param);
if (!started) showError('Could not start purchase');
// Unlocking happens in the purchaseStream handler
Hier ist der vollständige Service, der in main() vor runApp abonniert wird. Die README des Plugins empfiehlt, das Abonnement im initState Ihrer App einzurichten. Die Einrichtung in main() setzt noch einen Schritt früher an, sodass bereits ein Listener existiert, bevor das Plugin überhaupt Updates ausgeben kann.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
class PurchaseService {
PurchaseService._();
static final instance = PurchaseService._();
static const coins100 = 'coins_100';
static const premium = 'premium_unlock';
final InAppPurchase _iap = InAppPurchase.instance;
StreamSubscription<List<PurchaseDetails>>? _sub;
void start() {
_sub ??= _iap.purchaseStream.listen(
_onPurchases,
onDone: () => _sub?.cancel(),
onError: (Object e) {/* log */},
);
}
Future<List<ProductDetails>> loadProducts() async {
if (!await _iap.isAvailable()) return [];
final res = await _iap.queryProductDetails({coins100, premium});
if (res.notFoundIDs.isNotEmpty) {/* store setup problem */}
return res.productDetails;
}
Future<bool> buy(ProductDetails p) {
final param = PurchaseParam(productDetails: p);
return p.id == coins100
? _iap.buyConsumable(purchaseParam: param)
: _iap.buyNonConsumable(purchaseParam: param);
}
Future<void> restore() => _iap.restorePurchases();
Future<void> _onPurchases(List<PurchaseDetails> purchases) async {
for (final p in purchases) {
var finish = true;
switch (p.status) {
case PurchaseStatus.pending:
// show "waiting for approval"
break;
case PurchaseStatus.purchased:
case PurchaseStatus.restored:
final valid = await _verifyOnServer(p); // null = unreachable
if (valid == null) {
finish = false; // redelivered next launch
} else if (valid) {
await _deliver(p);
}
case PurchaseStatus.error:
// show p.error?.message
break;
case PurchaseStatus.canceled:
// no error UI
break;
}
if (finish && p.pendingCompletePurchase) {
await _iap.completePurchase(p);
}
}
}
Future<bool?> _verifyOnServer(PurchaseDetails p) async {
// POST p.verificationData.serverVerificationData,
// p.productID and platform to your backend
return true;
}
Future<void> _deliver(PurchaseDetails p) async {/* credit coins / set flag */}
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
PurchaseService.instance.start();
runApp(const MyApp());
}
_verifyOnServer ist ein Platzhalter für Ihren Backend-Aufruf. Die Methode gibt null zurück, wenn der Server nicht erreichbar ist. Der Kauf bleibt dann unabgeschlossen und wird beim nächsten Start erneut zugestellt. Das ist allerdings nur dann unbedenklich, wenn das Problem innerhalb des dreitägigen Zeitfensters von Android behoben wird.
| Status | Was die App tut | completePurchase aufrufen? |
|---|---|---|
pending | Einen Wartezustand anzeigen (z. B. bei einer Kaufanfrage/Ask to Buy oder einer verzögerten Zahlungsmethode) | Nein, auf den endgültigen Status warten |
purchased | Serverseitig verifizieren, dann bereitstellen | Ja, wenn pendingCompletePurchase |
restored | Verifizieren, dann die Berechtigung erneut gewähren | Ja, wenn pendingCompletePurchase |
error | error.message anzeigen | Ja, wenn pendingCompletePurchase |
canceled | Ohne Meldung zur Paywall zurückkehren | Ja, wenn pendingCompletePurchase |
Was bei In-App-Käufen schiefgeht
Vier häufige Fehler führen zu verlorenen Käufen oder abgelehnten Apps: ein fehlender Wiederherstellen-Button, ein zu spät gestartetes Stream-Abonnement, ein nie aufgerufenes completePurchase und blindes Vertrauen in den Client.
Fehlender Wiederherstellen-Button. Apples Richtlinie 3.1.1 schreibt vor, dass Apps eine Möglichkeit bieten müssen, alle wiederherstellbaren In-App-Käufe wiederherzustellen. Wiederhergestellte Käufe treffen mit PurchaseStatus.restored im purchaseStream ein, der Button muss also lediglich restorePurchases() aufrufen. Ihr bestehender Handler gewährt dann den Zugriff. Platzieren Sie den Button auf der Paywall und in den Einstellungen:
TextButton(
onPressed: PurchaseService.instance.restore,
child: const Text('Restore Purchases'),
)
Wenn eine Push-Benachrichtigung oder ein Marketing-Link die Paywall öffnet, leiten Sie den Aufruf mit Deep Linking in Flutter weiter, damit der Wiederherstellen-Button stets nur einen Tipp entfernt ist.
Zu spätes Lauschen. Ein Kauf, der abgeschlossen wird, während Ihre App geschlossen ist – etwa eine genehmigte Kaufanfrage (Ask to Buy) oder eine langsame Zahlung –, wird beim nächsten App-Start über den Stream zugestellt. Wird der Listener auf einem Screen registriert, den der Nutzer nie erreicht, bleibt dieses Update unverarbeitet. Der Nutzer erhält nicht, wofür er bezahlt hat, und unter Android wird ein nie abgeschlossener Kauf nach drei Tagen erstattet.
Kein Abschluss. Ist bei einem Kauf pendingCompletePurchase true und wird completePurchase nie aufgerufen, erstattet Google Play den Kauf nach drei Tagen, und der App Store lässt die Transaktion unabgeschlossen.
Vertrauen in den Client. Schalten Sie Inhalte nicht auf Grundlage des Status frei, den Ihre App empfängt. Senden Sie verificationData.serverVerificationData an Ihr Backend und validieren Sie die Daten dort: auf Apple-Plattformen mit der App Store Server API, unter Android mit der Google Play Developer API. Unter StoreKit 2 ist serverVerificationData eine signierte JWS-Transaktion. Ihr Server sollte die Signatur prüfen, die Transaktion über die App Store Server API abrufen oder beides tun. Die Verifizierungsanfrage ist ein gewöhnlicher Backend-Aufruf. Falls Ihr Backend GraphQL verwendet, zeigt Consuming GraphQL APIs with Flutter, wie Sie die Anbindung umsetzen.
Paywall-Design und verwaltete Abonnement-SDKs wie RevenueCat sind separate Entscheidungen, die auf diesem Ablauf aufbauen.
Wie testet man Käufe ohne echtes Geld?
Apple und Google stellen jeweils eine Testumgebung für In-App-Käufe bereit. Apple bietet Sandbox-Accounts und eine StoreKit-Konfigurationsdatei in Xcode. Google bietet Lizenztester in einem Play-Test-Track.
- Apple-Sandbox-Tester: Legen Sie Sandbox-Accounts in App Store Connect an und melden Sie sich auf einem Gerät mit einem davon an. Sandbox-Käufe laufen über Ihre echten Produkte in App Store Connect, ohne dass jemandem etwas berechnet wird.
- StoreKit-Konfigurationsdatei: Mit einer StoreKit-Konfigurationsdatei in Xcode können Sie Produkte lokal definieren und Käufe im iOS-Simulator testen, noch bevor App Store Connect vollständig eingerichtet ist.
- Play-Lizenztester: Öffnen Sie in der Play Console „Einstellungen > Lizenztests“ (Settings > License testing) und fügen Sie Google-Accounts hinzu. Testkäufe von Lizenztestern werden deren Accounts nicht belastet. Googles Leitfaden zum Testen der Abrechnung warnt, dass Tester in einem Test-Track, die keine Lizenztester sind, mit echtem Geld bezahlen.
Testen Sie vor der Einreichung auf beiden Plattformen eine Neuinstallation inklusive Wiederherstellung.
Ab in die Produktion
Für einen korrekten Ablauf von In-App-Käufen in Flutter gilt: Konfigurieren Sie beide Stores, abonnieren Sie den purchaseStream, bevor irgendetwas anderes ausgeführt wird, verifizieren Sie jeden Kauf auf Ihrem Server und schließen Sie jeden Kauf ab, bei dem pendingCompletePurchase gesetzt ist. Legen Sie als Nächstes in jeder Konsole ein Produkt an, testen Sie es mit einem Sandbox-Account und einem Lizenztester und vergewissern Sie sich, dass es in productDetails und nicht in notFoundIDs erscheint.
FAQs
Wie führe ich unter Android mit in_app_purchase ein Upgrade oder Downgrade eines Abonnements durch?
Rufen Sie buyNonConsumable mit einem GooglePlayPurchaseParam auf, der einen ChangeSubscriptionParam enthält. Übergeben Sie diesem die bisherigen GooglePlayPurchaseDetails des Nutzers und optional einen ReplacementMode wie ReplacementMode.withTimeProration. Diese Klassen stammen aus in_app_purchase_android; importieren Sie dieses Paket also, wenn Sie sie benötigen. Unter iOS entfällt dieser Schritt. Abonnements innerhalb derselben Abonnementgruppe ersetzen einander bereits, sodass der App Store den Wechsel übernimmt.
Funktioniert das Plugin in_app_purchase mit Flutter Web?
Nein. Das Plugin benötigt einen Store im Hintergrund und unterstützt ausschließlich den App Store (unter iOS und macOS) sowie Google Play (unter Android). Hinter einem Flutter-Web-Build steht kein Store, daher benötigen Verkäufe im Web einen Web-Zahlungsdienstleister wie Stripe. Die Plattformbeschränkung für Stripe gilt für digitale Güter, die innerhalb von iOS- und Android-Apps verkauft werden, nicht für Käufe im Web.
Was ist der Unterschied zwischen in_app_purchase und purchases_flutter von RevenueCat?
in_app_purchase übernimmt ausschließlich die Verbindung zum Store. Serverseitige Verifizierung, Speicherung von Berechtigungen und Nachverfolgung des Abonnementstatus bleiben Ihrem eigenen Backend überlassen. purchases_flutter von RevenueCat kapselt StoreKit und Google Play Billing zusammen mit dem gehosteten Backend von RevenueCat, das serverseitige Belegvalidierung, Nachverfolgung des Abonnementstatus und Webhooks ergänzt. Beide Pakete laufen weiterhin über die Abrechnung von Apple und Google, sodass die Stores in beiden Fällen dieselbe Provision einbehalten.
Was passiert mit bestehenden Abonnenten, wenn ich den Preis eines Abonnements erhöhe?
Das hängt vom Store, der Region und der Höhe der Erhöhung ab. Manche Erhöhungen erfordern die Zustimmung des Abonnenten; stimmt er nicht zu, wird das Abonnement nicht verlängert. Beide Stores erlauben bestimmte Erhöhungen außerdem allein mit vorheriger Benachrichtigung. Unter iOS zeigt StoreKit standardmäßig ein Zustimmungs-Sheet zur Preisänderung innerhalb Ihrer App an; die README von in_app_purchase erklärt, wie Sie dieses auf einen günstigeren Zeitpunkt verschieben. Unter Android informiert Google Play die Abonnenten per E-Mail und Benachrichtigung über die Änderung, sodass Sie dafür keinen Dart-Code benötigen.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k