StorePurchaseKit
StorePurchaseKit is a Flutter in-app purchase SDK powered by Google Play Billing on Android and StoreKit2 on iOS.
The SDK supports three host app types:
- Flutter host apps.
- Native iOS host apps with an embedded Flutter engine.
- Native Android host apps with an embedded Flutter engine.
Production Flow
Recommended production flow:
- The host app creates an
orderIdfrom your backend. - The host app asks the SDK to open the Google Play or Apple Store purchase sheet.
- The store returns a purchase/transaction to the SDK.
- The host app sends
purchaseToken,productId,orderId,platform, andrawto your backend. - The backend verifies the purchase with the Google Play Developer API or App Store Server API.
- The backend stores the entitlement/order with an idempotency key.
- The SDK finishes/acknowledges the store transaction only after backend verification succeeds.
Do not unlock premium access from local callbacks alone. Your backend should be the final source of truth.
Backend Verify API
Example app-to-backend request:
POST /purchases/verify
Content-Type: application/json
{
"purchaseToken": "...",
"productId": "vip_month",
"orderId": "ORDER_001",
"platform": "android",
"raw": {}
}
Your backend should:
- Android: verify
purchaseTokenwith the Google Play Developer API. - iOS: verify the transaction with the App Store Server API or App Store Server Notifications.
- Check that
orderId,userId, andproductIdmatch the order previously created by your backend. - Store the entitlement idempotently.
orderIdis the recommended idempotency key. - Return HTTP 200 only after verification and entitlement persistence succeed.
1. Flutter Host App
Install
dependencies:
store_purchase_kit: latest
http: latest
Backend Client
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:store_purchase_kit/store_purchase_kit.dart';
class MyBackendClient implements BackendVerificationClient {
@override
Future<bool> verifyPurchase({
required String purchaseToken,
required String productId,
required String orderId,
required String platform,
required Map<String, dynamic>? rawPayload,
}) async {
final response = await http.post(
Uri.parse("https://api.yourserver.com/purchases/verify"),
headers: {"content-type": "application/json"},
body: jsonEncode({
"purchaseToken": purchaseToken,
"productId": productId,
"orderId": orderId,
"platform": platform,
"raw": rawPayload,
}),
);
return response.statusCode == 200;
}
}
Initialize
await StorePurchase.instance.initialize(
config: PurchaseConfig.production(
backendVerificationClient: MyBackendClient(),
requireBackendVerification: true,
autoFinishTransactions: true,
),
callback: MyPurchaseCallback(),
);
Sandbox:
await StorePurchase.instance.initialize(
config: PurchaseConfig.sandbox(
backendVerificationClient: MyBackendClient(),
),
callback: MyPurchaseCallback(),
);
Callback
class MyPurchaseCallback implements PurchaseCallback {
@override
void onLoading() {}
@override
void onPending() {}
@override
void onPurchased(PurchaseResult result) {
// The store returned a transaction. The SDK will start backend verification.
}
@override
void onVerifyWaiting() {}
@override
void onCompleted(PurchaseResult result) {
// Backend verification succeeded and the SDK finished/acknowledged the transaction.
}
@override
void onCancelled() {}
@override
void onFailed(PurchaseError error) {}
@override
void onRestored(List<PurchaseResult> result) {}
}
Purchase
await StorePurchase.instance.purchase(
productId: "vip_month",
orderId: "ORDER_001",
userId: "USER_001",
extra: {
"productType": "inapp",
},
);
Android subscription:
extra: {
"productType": "subs",
}
Restore
final purchases = await StorePurchase.instance.restore();
Manual Finish
By default, autoFinishTransactions: true, so the SDK finishes/acknowledges the transaction after backend verification succeeds.
If the host app wants to finish transactions manually:
await StorePurchase.instance.initialize(
config: PurchaseConfig.production(
backendVerificationClient: MyBackendClient(),
autoFinishTransactions: false,
),
callback: MyPurchaseCallback(),
);
After backend verification succeeds:
await StorePurchase.instance.finishPurchase(
transactionId: result.transactionId,
);
Android consumable:
await StorePurchase.instance.consume(
purchaseToken: result.purchaseToken,
);
Android non-consumable/subscription:
await StorePurchase.instance.acknowledge(
purchaseToken: result.purchaseToken,
);
Native Host Architecture
For native iOS or Android host apps, do not call the internal store_purchase_kit/method channel directly. That channel is used by the Dart wrapper to call the native plugin.
Correct architecture:
- The native host embeds a Flutter engine.
- The native host calls a Dart bridge through
host_store_purchase_kit/method. - The Dart bridge calls
StorePurchase.instance. - The Dart bridge sends events back to the native host through
host_store_purchase_kit/event. - The native host verifies the purchase with your backend.
- The native host calls the Dart bridge again to
finishPurchase,acknowledge, orconsume.
Dart Bridge For Native Hosts
Create a Dart entrypoint in the Flutter module/app embedded by the native host:
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:store_purchase_kit/store_purchase_kit.dart';
const _hostMethod = MethodChannel("host_store_purchase_kit/method");
const _hostEvent = MethodChannel("host_store_purchase_kit/event");
void main() {
WidgetsFlutterBinding.ensureInitialized();
_hostMethod.setMethodCallHandler((call) async {
final args = Map<String, dynamic>.from(call.arguments as Map? ?? {});
switch (call.method) {
case "initialize":
await StorePurchase.instance.initialize(
config: PurchaseConfig.production(
requireBackendVerification: false,
autoFinishTransactions: false,
),
callback: _NativeHostPurchaseCallback(),
);
return true;
case "purchase":
await StorePurchase.instance.purchase(
productId: args["productId"] as String,
orderId: args["orderId"] as String,
userId: args["userId"] as String,
extra: Map<String, dynamic>.from(args["extra"] as Map? ?? {}),
);
return true;
case "restore":
await StorePurchase.instance.restore();
return true;
case "finishPurchase":
await StorePurchase.instance.finishPurchase(
transactionId: args["transactionId"] as String,
);
return true;
case "acknowledge":
await StorePurchase.instance.acknowledge(
purchaseToken: args["token"] as String,
);
return true;
case "consume":
await StorePurchase.instance.consume(
purchaseToken: args["token"] as String,
);
return true;
}
throw PlatformException(
code: "not_implemented",
message: "Unsupported method: ${call.method}",
);
});
runApp(const SizedBox.shrink());
}
class _NativeHostPurchaseCallback implements PurchaseCallback {
Future<void> _emit(String name, Map<String, dynamic> payload) {
return _hostEvent.invokeMethod(name, payload);
}
@override
void onLoading() {
_emit("purchaseEvent", {"status": "loading"});
}
@override
void onPending() {
_emit("purchaseEvent", {"status": "pending"});
}
@override
void onPurchased(PurchaseResult result) {
_emit("purchaseEvent", {
"status": "purchased",
...result.toMap(),
"raw": result.raw,
});
}
@override
void onVerifyWaiting() {
_emit("purchaseEvent", {"status": "verifyWaiting"});
}
@override
void onCompleted(PurchaseResult result) {
_emit("purchaseEvent", {
"status": "completed",
...result.toMap(),
"raw": result.raw,
});
}
@override
void onCancelled() {
_emit("purchaseEvent", {"status": "cancelled"});
}
@override
void onFailed(PurchaseError error) {
_emit("purchaseEvent", {
"status": "failed",
"code": error.code.name,
"message": error.message,
});
}
@override
void onRestored(List<PurchaseResult> result) {
_emit("purchaseEvent", {
"status": "restored",
"data": result.map((e) => e.toMap()).toList(),
});
}
}
In native host mode, backend verification lives in Swift/Kotlin, so the bridge uses requireBackendVerification: false and autoFinishTransactions: false.
2. Native iOS Host App
Build Framework
Build the Flutter framework from the Flutter module/app that contains the Dart bridge:
flutter build ios-framework --no-codesign -t lib/store_purchase_kit.dart
Add the generated frameworks to Xcode:
- Add
Flutter.xcframework,App.xcframework, and the required plugin frameworks. - Set embedded frameworks to
Embed & Sign. - The iOS target must be iOS 15 or newer because the SDK uses StoreKit2.
Swift Bridge
import Flutter
import UIKit
final class PurchaseBridge: NSObject {
private let engine = FlutterEngine(name: "store_purchase_engine")
private lazy var methodChannel = FlutterMethodChannel(
name: "host_store_purchase_kit/method",
binaryMessenger: engine.binaryMessenger
)
private lazy var eventChannel = FlutterMethodChannel(
name: "host_store_purchase_kit/event",
binaryMessenger: engine.binaryMessenger
)
override init() {
super.init()
engine.run()
eventChannel.setMethodCallHandler { [weak self] call, result in
if call.method == "purchaseEvent",
let event = call.arguments as? [String: Any] {
self?.handlePurchaseEvent(event)
}
result(nil)
}
methodChannel.invokeMethod("initialize", arguments: nil)
}
func purchaseVip(orderId: String, userId: String) {
methodChannel.invokeMethod("purchase", arguments: [
"productId": "vip_month",
"orderId": orderId,
"userId": userId,
"extra": [
"productType": "inapp"
]
])
}
}
Verify With Backend In Swift
private func handlePurchaseEvent(_ event: [String: Any]) {
switch event["status"] as? String {
case "purchased":
Task {
let ok = await verifyPurchaseWithBackend(event)
if ok, let transactionId = event["transactionId"] as? String {
methodChannel.invokeMethod("finishPurchase", arguments: [
"transactionId": transactionId
])
}
}
case "pending", "cancelled", "failed", "restored":
break
default:
break
}
}
func verifyPurchaseWithBackend(_ event: [String: Any]) async -> Bool {
guard let url = URL(string: "https://api.yourserver.com/purchases/verify") else {
return false
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"purchaseToken": event["purchaseToken"] ?? "",
"productId": event["productId"] ?? "",
"orderId": event["orderId"] ?? "",
"platform": event["platform"] ?? "ios",
"raw": event["raw"] ?? [:]
])
do {
let (_, response) = try await URLSession.shared.data(for: request)
return (response as? HTTPURLResponse)?.statusCode == 200
} catch {
return false
}
}
Restore From Native iOS
methodChannel.invokeMethod("restore", arguments: nil)
3. Native Android Host App
Build AAR
Build the Android artifact from the Flutter module/app that contains the Dart bridge:
flutter build aar
If you use a Flutter module, integrate the Maven output generated by flutter build aar. If you copy the AAR manually:
repositories {
flatDir {
dirs("libs")
}
}
dependencies {
implementation(name = "store_purchase_kit", ext = "aar")
}
Kotlin Bridge
import android.content.Context
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel
class PurchaseBridge(
context: Context
) {
private val engine = FlutterEngine(context)
private val methodChannel = MethodChannel(
engine.dartExecutor.binaryMessenger,
"host_store_purchase_kit/method"
)
private val eventChannel = MethodChannel(
engine.dartExecutor.binaryMessenger,
"host_store_purchase_kit/event"
)
init {
engine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
eventChannel.setMethodCallHandler { call, result ->
if (call.method == "purchaseEvent") {
@Suppress("UNCHECKED_CAST")
handlePurchaseEvent(call.arguments as Map<String, Any?>)
}
result.success(null)
}
methodChannel.invokeMethod("initialize", null)
}
fun purchaseVip(orderId: String, userId: String) {
methodChannel.invokeMethod(
"purchase",
mapOf(
"productId" to "vip_month",
"orderId" to orderId,
"userId" to userId,
"extra" to mapOf(
"productType" to "inapp"
)
)
)
}
}
Android subscription:
"extra" to mapOf(
"productType" to "subs"
)
Verify With Backend In Kotlin
private fun handlePurchaseEvent(event: Map<String, Any?>) {
when (event["status"] as? String) {
"purchased" -> verifyThenFinish(event)
"pending" -> Unit
"cancelled" -> Unit
"failed" -> Unit
"restored" -> Unit
}
}
private fun verifyThenFinish(event: Map<String, Any?>) {
lifecycleScope.launch {
val ok = verifyPurchaseWithBackend(event)
if (ok) {
methodChannel.invokeMethod(
"finishPurchase",
mapOf("transactionId" to event["transactionId"])
)
}
}
}
Example backend request with OkHttp:
suspend fun verifyPurchaseWithBackend(event: Map<String, Any?>): Boolean {
val json = JSONObject().apply {
put("purchaseToken", event["purchaseToken"])
put("productId", event["productId"])
put("orderId", event["orderId"])
put("platform", event["platform"] ?: "android")
put("raw", JSONObject(event["raw"] as? Map<*, *> ?: emptyMap<Any, Any>()))
}
val request = Request.Builder()
.url("https://api.yourserver.com/purchases/verify")
.post(json.toString().toRequestBody("application/json".toMediaType()))
.build()
return withContext(Dispatchers.IO) {
okHttpClient.newCall(request).execute().use { response ->
response.code == 200
}
}
}
Consume/Acknowledge On Android
finishPurchase acknowledges an Android purchase by transactionId.
Consumable product:
methodChannel.invokeMethod(
"consume",
mapOf("token" to event["purchaseToken"])
)
Non-consumable/subscription:
methodChannel.invokeMethod(
"acknowledge",
mapOf("token" to event["purchaseToken"])
)
Restore From Native Android
methodChannel.invokeMethod("restore", null)
Native Bridge Channel Reference
The native host calls the Dart bridge through:
host_store_purchase_kit/method
| Method | Arguments | Description |
|---|---|---|
initialize |
null |
Initializes StorePurchase in the Dart bridge |
purchase |
productId, orderId, userId, extra |
Opens the store purchase flow |
restore |
null |
Restores purchases |
finishPurchase |
transactionId |
Finishes a StoreKit transaction or acknowledges an Android purchase |
acknowledge |
token |
Acknowledges an Android purchase |
consume |
token |
Consumes an Android purchase |
The Dart bridge sends events back to the native host through:
host_store_purchase_kit/event
Event method:
purchaseEvent
Statuses:
| Status | Description |
|---|---|
loading |
The SDK started a purchase |
purchased |
The store returned a purchase/transaction; the native host should verify with the backend |
verifyWaiting |
Verification is waiting/in progress |
completed |
The transaction has been completed |
pending |
The purchase is pending |
cancelled |
The user cancelled the purchase |
failed |
The SDK or store returned an error |
restored |
Restore purchases completed |
Production Checklist
- Create products/subscriptions in App Store Connect and Google Play Console.
- Use the same
productIdin the app, store console, and backend. - Verify purchases with the store from your backend before granting entitlement.
- Use
orderIdas an idempotency key. - Do not store card data or sensitive payment data in the app.
- Do not finish/acknowledge a transaction if backend verification fails.
- Test with sandbox, TestFlight, and Google Play internal testing before release.
- Monitor App Store Server Notifications and Google Real-time Developer Notifications for refunds, renewals, and cancellations.