Skip to main content

Myket In-App Purchase Guide for Flutter

Flutter
Sample project

For a complete, runnable Myket Flutter in-app billing implementation, see the official sample project on GitHub.

The myket_iap plugin is the official Flutter plugin for integrating Myket In-App Billing (IAP) into Android applications. It handles service initialization, inventory and SKU queries, purchase flows, and consumable or non-consumable product management.

Note: This plugin supports Android only.

Features​

  • Minimal Configuration: Requires straightforward configuration in your Android project.
  • Product Types Support: Supports one-time purchases, including consumable items (such as coins or credits) and non-consumable items (such as premium upgrades or ad removal).
  • Inventory & Pricing Queries: Query user-owned products and SKU metadata (price, title, description) directly from Myket.
  • Multi-Store Support: Switch between app stores using Gradle product flavors and manifest placeholders without altering Dart code.
  • Standard Error Handling: Provides standardized result codes with descriptive messages.
  • Current Platform Compatibility: Compatible with Flutter 3+, Android Gradle Plugin 8+, Kotlin 2+, and Android SDK 34+.

Prerequisites​

  1. A developer account on the Myket Developer Portal.
  2. An application registered in your Myket developer panel with a reserved package name.
  3. The application's RSA Public Key from the Myket developer console.
  4. Product identifiers (SKUs) defined in your application console on Myket.
  5. The Myket App Store installed on the test device or emulator.

Installation​

Add myket_iap to your Flutter project's dependencies:

flutter pub add myket_iap

Or add it directly to your pubspec.yaml:

dependencies:
flutter:
sdk: flutter
myket_iap: ^2.0.1

Then run:

flutter pub get

Android Setup​

1. Add Myket Maven Repository​

The native billing library is hosted on Myket's Maven repository. Ensure that https://maven.myket.ir is present in your Android repository configuration.

For modern Flutter projects (android/settings.gradle.kts):​

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven("https://maven.myket.ir")
}
}

For Groovy DSL (android/build.gradle or settings.gradle):​

allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://maven.myket.ir' }
}
}

2. Configure Manifest Placeholders​

Add the required manifest placeholders to your application's defaultConfig in android/app/build.gradle.kts or android/app/build.gradle.

Kotlin DSL (android/app/build.gradle.kts):​

android {
...
defaultConfig {
...
val marketApplicationId = "ir.mservices.market"
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders.putAll(
mapOf(
"marketApplicationId" to marketApplicationId,
"marketBindAddress" to marketBindAddress,
"marketPermission" to "$marketApplicationId.BILLING"
)
)
}
}

Groovy DSL (android/app/build.gradle):​

android {
...
defaultConfig {
...
def marketApplicationId = "ir.mservices.market"
def marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders += [
marketApplicationId: "${marketApplicationId}",
marketBindAddress : "${marketBindAddress}",
marketPermission : "${marketApplicationId}.BILLING"
]
}
}

3. Multi-Store & Flavor Support​

If you distribute your app across multiple Android stores (e.g., Myket, Cafe Bazaar), configure Gradle productFlavors in android/app/build.gradle or build.gradle.kts:

android {
...
flavorDimensions "store"
productFlavors {
myket {
dimension "store"
def marketApplicationId = "ir.mservices.market"
def marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders = [
marketApplicationId: "${marketApplicationId}",
marketBindAddress : "${marketBindAddress}",
marketPermission : "${marketApplicationId}.BILLING"
]
}
bazaar {
dimension "store"
def marketApplicationId = "com.farsitel.bazaar"
def marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
manifestPlaceholders = [
marketApplicationId: "${marketApplicationId}",
marketBindAddress : "${marketBindAddress}",
marketPermission : "${marketApplicationId}.permission.PAY_THROUGH_BAZAAR"
]
}
}
}

4. Migration Guide (from 1.0.x to 1.1.0+)​

If you are upgrading from 1.0.6 or earlier:

  1. Remove manual permissions from android/app/src/main/AndroidManifest.xml:

    <!-- REMOVE THIS -->
    <uses-permission android:name="ir.mservices.market.BILLING" />
  2. Remove <queries> declaration from android/app/src/main/AndroidManifest.xml:

    <!-- REMOVE THIS -->
    <queries>
    <package android:name="ir.mservices.market" />
    <intent>
    <action android:name="ir.mservices.market.InAppBillingService.BIND" />
    <data android:mimeType="*/*" />
    </intent>
    </queries>
  3. Add the manifest placeholders in your build.gradle / build.gradle.kts as described in Step 2. The plugin merges required permissions and package queries automatically.

Step-by-Step Usage Guide​

All methods on MyketIAP are static and return Futures.

Step 1: Import the Package​

import 'package:myket_iap/myket_iap.dart';
import 'package:myket_iap/util/iab_result.dart';
import 'package:myket_iap/util/purchase.dart';
import 'package:myket_iap/util/inventory.dart';
import 'package:myket_iap/util/sku_details.dart';
import 'package:myket_iap/util/constants.dart';

Step 2: Initialize Billing Service (init)​

Initialize the billing service early in your application lifecycle (for example, in your stateful widget's initState):


void initState() {
super.initState();
_initializeBilling();
}

Future<void> _initializeBilling() async {
const String myketRsaKey = "YOUR_RSA_KEY_FROM_MYKET_PANEL";

IabResult? result = await MyketIAP.init(
rsaKey: myketRsaKey,
enableDebugLogging: true, // Set to false in production
);

if (result != null && result.isSuccess()) {
print("Billing setup successful: ${result.mMessage}");
// Proceed to query inventory
_checkInventory();
} else {
print("Billing setup failed: ${result?.mMessage}");
}
}
warning

Do not invoke any purchase or inventory operations until MyketIAP.init completes successfully (result.isSuccess() is true).

Step 3: Query Inventory & Product Details (queryInventory)​

Query purchases already owned by the user, and optionally fetch SKU details (price, title, description):

Future<void> _checkInventory() async {
final Map<dynamic, dynamic> response = await MyketIAP.queryInventory(
querySkuDetails: true,
skus: ["gas", "premium_upgrade"],
);

IabResult? result = response[MyketIAP.RESULT];
Inventory? inventory = response[MyketIAP.INVENTORY];

if (result != null && result.isSuccess() && inventory != null) {
// 1. Inspect SKU details and pricing
SkuDetails? gasDetails = inventory.mSkuMap["gas"];
if (gasDetails != null) {
print("Gas Price: ${gasDetails.mPrice}");
print("Gas Title: ${gasDetails.mTitle}");
}

// 2. Check owned purchases for non-consumable entitlements
Purchase? premiumPurchase = inventory.mPurchaseMap["premium_upgrade"];
if (premiumPurchase != null) {
print("User owns premium upgrade: ${premiumPurchase.mOrderId}");
// Restore premium feature
}

// 3. Recover consumable items bought but not yet consumed (e.g., after an app crash)
Purchase? gasPurchase = inventory.mPurchaseMap["gas"];
if (gasPurchase != null) {
await _consumeProduct(gasPurchase);
}
} else {
print("Failed to query inventory: ${result?.mMessage}");
}
}

Step 4: Launch Purchase Flow (launchPurchaseFlow)​

Initiate the checkout dialog for a product:

Future<void> _buyProduct(String sku) async {
// Developer payload can be used for server-side verification and fraud prevention
const String developerPayload = "custom_payload_or_order_token";

final Map<dynamic, dynamic> response = await MyketIAP.launchPurchaseFlow(
sku: sku,
payload: developerPayload,
);

IabResult? result = response[MyketIAP.RESULT];
Purchase? purchase = response[MyketIAP.PURCHASE];

if (result != null && result.isSuccess() && purchase != null) {
print("Purchase successful for SKU: ${purchase.mSku}");
print("Order ID: ${purchase.mOrderId}");
print("Purchase Token: ${purchase.mToken}");

// If the product is consumable, consume it immediately
if (purchase.mSku == "gas") {
await _consumeProduct(purchase);
} else {
// Non-consumable product (e.g. permanent unlock)
_unlockFeature();
}
} else {
print("Purchase cancelled or failed: ${result?.mMessage}");
}
}

Step 5: Consume Purchases (consume)​

For consumable products (such as virtual currency, coins, extra lives), you must call consume after purchase. Once consumed, the item can be purchased again:

Future<void> _consumeProduct(Purchase purchase) async {
final Map<dynamic, dynamic> response = await MyketIAP.consume(purchase: purchase);

IabResult? result = response[MyketIAP.RESULT];
Purchase? consumedPurchase = response[MyketIAP.PURCHASE];

if (result != null && result.isSuccess()) {
print("Item consumed successfully: ${consumedPurchase?.mSku}");
// Credit virtual items / rewards to the user
} else {
print("Failed to consume item: ${result?.mMessage}");
}
}
danger

Never consume non-consumable products. Consuming an item permanently removes it from the user's active inventory on Myket, causing loss of ownership.

Step 6: Check Purchase Status (getPurchase)​

To verify whether a specific SKU is already owned:

Future<void> _checkPurchaseStatus(String sku) async {
final Map<dynamic, dynamic> response = await MyketIAP.getPurchase(
sku: sku,
querySkuDetails: true,
);

IabResult? result = response[MyketIAP.RESULT];
Purchase? purchase = response[MyketIAP.PURCHASE];

if (result != null && result.isSuccess() && purchase != null) {
print("SKU $sku is owned: ${purchase.mOrderId}");
} else {
print("SKU $sku is not owned or query failed.");
}
}

Step 7: Dispose Resources (dispose)​

Release underlying services, receivers, and memory resources when the billing helper is no longer needed (e.g., in your widget's dispose()):


void dispose() {
MyketIAP.dispose();
super.dispose();
}
Dispose Future Behavior in 2.0.1

In published version 2.0.1, dispose releases the native Android resources but does not complete the MethodChannel Future callback. Avoid indefinitely awaiting dispose() during widget or app destruction, or attach a .timeout(const Duration(seconds: 2)) if necessary.

Security Best Practices​

  1. Verify Developer Payload: Always pass a unique, cryptographically secure identifier or session token in payload and verify that purchase.mDeveloperPayload matches what you expect.
tip

The developerPayload string must not exceed 800 characters.

  1. Server-Side Signature Verification: For critical transactions, send purchase.mOriginalJson and purchase.mSignature to your secure backend to verify authenticity with your RSA Public Key using Myket's billing verification API.
  2. Do Not Commit Raw RSA Keys: Avoid committing unencrypted RSA public keys into public version control.
  3. Prevent Duplicate Delivery with Purchase Tokens: Deduplicate every transaction by storing purchase.mToken in your database before fulfilling orders.

Data Models & API Reference​

MyketIAP​

MethodParametersReturn TypeDescription
initrequired String rsaKey, bool enableDebugLogging = falseFuture<IabResult?>Initializes the IAB helper and establishes connection to the billing service.
launchPurchaseFlowrequired String sku, String? payloadFuture<Map>Launches the in-app billing checkout flow on Myket.
consumerequired Purchase purchaseFuture<Map>Consumes an owned item so it can be re-purchased.
queryInventorybool querySkuDetails = false, List<String>? skusFuture<Map>Queries all owned purchases and optionally queries SKU metadata.
getPurchaserequired String sku, bool querySkuDetails = falseFuture<Map>Queries ownership and details for a single SKU.
disposeNoneFutureCloses service connections and releases native resources.

IabResult​

Represents the result of a billing operation:

  • int mResponse: Response code (see Response Codes).
  • String mMessage: Descriptive message detailing the outcome.
  • bool isSuccess(): Returns true if mResponse == Constants.BILLING_RESPONSE_RESULT_OK.
  • bool isFailure(): Returns true if the operation failed.

Purchase​

Represents an in-app purchase:

  • String mSku: Product identifier.
  • String mOrderId: Unique transaction ID generated by Myket.
  • String mPackageName: Package name of the application.
  • int mPurchaseTime: Timestamp (epoch milliseconds) of the purchase.
  • int mPurchaseState: Purchase state (0 = purchased, 1 = cancelled, 2 = refunded).
  • String? mDeveloperPayload: Additional developer-supplied payload.
  • String mToken: Unique token identifying the purchase.
  • String mOriginalJson: Raw JSON string returned from the billing service.
  • String mSignature: Cryptographic signature for signature verification.

SkuDetails​

Contains product metadata configured in the Myket developer console:

  • String mSku: The product SKU.
  • String mTitle: Product title/name.
  • String mPrice: Localized formatted price string (e.g. ۱۰,۰۰۰ ریال).
  • String? mDescription: Product description.
  • String mType: Item type (inapp or subs).

Inventory​

Container for queried purchases and SKU details:

  • Map<String, Purchase> mPurchaseMap: Map of product SKU to Purchase object.
  • Map<String, SkuDetails> mSkuMap: Map of product SKU to SkuDetails object.

Response & Error Codes​

Accessible via the Constants class:

Billing Response Codes​

CodeConstantMeaning
0BILLING_RESPONSE_RESULT_OKSuccess.
1BILLING_RESPONSE_RESULT_USER_CANCELEDUser cancelled the checkout flow.
3BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLEBilling service unavailable or Myket app not installed.
4BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLERequested SKU is not available for purchase.
5BILLING_RESPONSE_RESULT_DEVELOPER_ERRORInvalid arguments provided to the API.
6BILLING_RESPONSE_RESULT_ERRORFatal error during the purchase process.
7BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNEDFailure to purchase because the item is already owned.
8BILLING_RESPONSE_RESULT_ITEM_NOT_OWNEDFailure to consume because the item is not owned.

IAB Helper Error Codes​

CodeConstantMeaning
-1001IABHELPER_REMOTE_EXCEPTIONRemote exception while communicating with Myket.
-1002IABHELPER_BAD_RESPONSEBad or malformed response received from billing service.
-1003IABHELPER_VERIFICATION_FAILEDPurchase signature verification failed.
-1004IABHELPER_SEND_INTENT_FAILEDFailed to send checkout intent to Myket.
-1005IABHELPER_USER_CANCELLEDUser cancelled the purchase flow.
-1008IABHELPER_UNKNOWN_ERRORUnknown error occurred.
-1010IABHELPER_INVALID_CONSUMPTIONAttempted to consume an invalid or unowned item.

Example Application​

A complete example implementing a sample game ("Trivial Drive") with gas purchases (consumable) and premium membership (non-consumable) is included in the example directory.

Subscription support

The subscription flow in the sample does not mean Myket currently supports subscriptions. Myket does not currently support subscription products; do not use the subscription example for a real product.

To run the example app:

git clone https://github.com/myketstore/billing-sample-flutter.git
cd billing-sample-flutter/example
flutter pub get
flutter run

Troubleshooting​

1. BILLING_UNAVAILABLE (Code 3)​

  • Ensure the Myket Store application is installed and updated on the test device.
  • Ensure the user is logged into their Myket account.

2. ITEM_UNAVAILABLE (Code 4)​

  • Verify that the SKU ID passed to launchPurchaseFlow exactly matches the SKU ID created in the Myket Developer Panel.
  • Ensure the product is set to "Active" / published in the Myket developer console.
  • Confirm your application's applicationId (package name) matches the registered app on Myket.

3. Verification Failed (Code -1003)​

  • Ensure you passed the correct RSA Public Key to MyketIAP.init().