store_purchase_kit 1.0.0 copy "store_purchase_kit: ^1.0.0" to clipboard
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:

  1. The host app creates an orderId from your backend.
  2. The host app asks the SDK to open the Google Play or Apple Store purchase sheet.
  3. The store returns a purchase/transaction to the SDK.
  4. The host app sends purchaseToken, productId, orderId, platform, and raw to your backend.
  5. The backend verifies the purchase with the Google Play Developer API or App Store Server API.
  6. The backend stores the entitlement/order with an idempotency key.
  7. 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 purchaseToken with the Google Play Developer API.
  • iOS: verify the transaction with the App Store Server API or App Store Server Notifications.
  • Check that orderId, userId, and productId match the order previously created by your backend.
  • Store the entitlement idempotently. orderId is 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:

  1. The native host embeds a Flutter engine.
  2. The native host calls a Dart bridge through host_store_purchase_kit/method.
  3. The Dart bridge calls StorePurchase.instance.
  4. The Dart bridge sends events back to the native host through host_store_purchase_kit/event.
  5. The native host verifies the purchase with your backend.
  6. The native host calls the Dart bridge again to finishPurchase, acknowledge, or consume.

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 productId in the app, store console, and backend.
  • Verify purchases with the store from your backend before granting entitlement.
  • Use orderId as 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.
0
likes
135
points
10
downloads

Documentation

API reference

Publisher

verified publisherit36vn.com

Weekly Downloads

Enterprise Flutter Store Purchase Library using Native StoreKit2 and Google Billing with MethodChannel/EventChannel by IT36VN.com

Homepage

Topics

#purchase #billing #storekit #google-play #payment

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on store_purchase_kit

Packages that implement store_purchase_kit