store_purchase_kit 1.0.0
store_purchase_kit: ^1.0.0 copied to clipboard
Enterprise Flutter Store Purchase Library using Native StoreKit2 and Google Billing with MethodChannel/EventChannel by IT36VN.com
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.