Build-time isolated in-app purchases for Cafe Bazaar and Myket. Use one Dart API while shipping only the native billing SDK selected for each Android artifact.
Multi-store wrappers commonly place every provider SDK in every APK or AAB.
iran_iap selects the provider at build time instead:
flowchart LR
App["Flutter app"] --> API["iran_iap API"]
API --> Store{"Build selection"}
Store -->|bazaar| Bazaar["Poolakey SDK"]
Store -->|myket| Myket["Myket Billing SDK"]
The unselected billing SDK and its adapter are not compiled into the artifact. The repository includes an artifact scanner to enforce this boundary.
- One store-agnostic API for products, purchases, subscriptions, and consumption.
- Build-time Cafe Bazaar/Myket source-set and dependency isolation.
- Typed purchase cancellation through
PurchaseCancelled. - Stable
IapErrorCodevalues with optional native diagnostics. - Idempotent initialization and disposal.
- Runtime capability reporting.
- CLI commands for host checks, store-aware runs/builds, and artifact verification.
| Platform | Cafe Bazaar | Myket |
|---|---|---|
| Android | Supported | Supported |
| iOS, macOS, Linux, Windows, web | Not supported | Not supported |
Requirements:
- Flutter
>=3.44.0 - Dart
>=3.12.0 <4.0.0 - Android
minSdk 24 - Java 17 or newer
Add the package:
dependencies:
iran_iap: ^0.3.0Then run:
flutter pub getBoth native billing SDKs are resolved from JitPack. Add the repository once in
the host project's android/build.gradle.kts:
allprojects {
repositories {
google()
mavenCentral()
maven {
url = uri("https://jitpack.io")
content {
includeGroup("com.github.cafebazaar.Poolakey")
includeGroup("com.github.myketstore")
}
}
}
}Use the equivalent maven { url 'https://jitpack.io' } syntax in Groovy
projects.
Myket builds require these values in
android/app/build.gradle.kts:
android {
defaultConfig {
manifestPlaceholders["marketApplicationId"] = "ir.mservices.market"
manifestPlaceholders["marketBindAddress"] =
"ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders["marketPermission"] =
"ir.mservices.market.BILLING"
}
}It is safe to keep the placeholders in every build; they are inert when the Myket SDK is not selected.
Cafe Bazaar purchase flows use Android's Activity Result API. Make the host
activity extend FlutterFragmentActivity:
package com.example.app
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()Check the host configuration at any time:
dart run iran_iap doctor --store bazaar
dart run iran_iap doctor --store myketThe bundled CLI supplies both the Dart define and matching Gradle property:
dart run iran_iap run --store bazaar
dart run iran_iap build apk --store bazaar -- --release
dart run iran_iap build appbundle --store myket -- --releasePlace Flutter-specific options after --; options before it belong to the
iran_iap CLI.
The CLI returns 0 on success, 64 for invalid invocation syntax, and 66
when a required project or artifact cannot be found. Exit status 70 indicates
that the installed verifier asset is unavailable. Build/run commands forward
Flutter's exit status; doctor and verification failures return a non-zero status
suitable for CI.
Standard Flutter commands also work:
flutter run --dart-define=IRAN_IAP_STORE=bazaar
flutter build appbundle --release --dart-define=IRAN_IAP_STORE=myketStore selection is compile-time state. Stop and rebuild the app when switching stores; a hot restart cannot change it.
Cafe Bazaar defaults to backend verification and does not require a public key:
final iap = IranIap();
await iap.initialize();Myket requires the store-provided RSA public key:
final iap = IranIap(
config: const IranIapConfig(storePublicKey: 'YOUR_PUBLIC_KEY'),
);
await iap.initialize();The public key is verification material, not a private server secret. Never put private keys, API secrets, or backend credentials in a Flutter application.
For optional Cafe Bazaar client-side signature checking:
final iap = IranIap(
config: const IranIapConfig(
storePublicKey: 'YOUR_BAZAAR_PUBLIC_KEY',
bazaarSecurityMode: BazaarSecurityMode.localVerification,
),
);Backend verification remains the authorization boundary for valuable entitlements.
final products = await iap.queryProducts(
{'coin_pack', 'premium_monthly'},
type: IapProductType.inApp,
);
for (final product in products) {
print('${product.title}: ${product.price}');
}IapProduct.price is localized display text. Do not parse it for accounting or
entitlement decisions.
final outcome = await iap.purchase(
const IapPurchaseRequest(
productId: 'coin_pack',
type: IapProductType.inApp,
payload: 'order-correlation-id',
),
);
switch (outcome) {
case PurchaseCompleted():
// Send purchase evidence to a trusted backend before granting access.
break;
case PurchaseCancelled():
// Cancellation is expected user behavior, not an exception.
break;
}Cafe Bazaar dynamic pricing is available through
IapPurchaseRequest.dynamicPriceToken. Myket rejects that option with
IapErrorCode.featureUnavailable.
Pass a hosted HTTPS payment URL generated by your trusted backend to the
client. iran_iap opens it with the Android handler selected by the user; do
not log or persist payment URLs because they can contain sensitive checkout
data. This flow does not require initialize() because it is independent of
store billing.
await iap.openPaymentUrl(
const IapUrlPaymentRequest(
paymentUrl: 'https://payments.example/checkout?session=CHECKOUT_SESSION',
),
);This feature works in both store builds and supports any HTTPS payment gateway.
It confirms only that Android opened the URL: generic payment URLs cannot
reliably report completion, cancellation, or verification back to the app.
The URL must not include embedded credentials. Invalid URLs throw
PaymentConfigurationException.
For a complete client-side flow, start a payment session with a unique,
backend-generated state value and configure the gateway return URL to match
your app link. One session can be active per IranIap instance. The plugin
persists the session on Android so a callback received after activity or process
recreation can be recovered with recoverPaymentResult().
final result = await iap.startPayment(
IapUrlPaymentRequest(paymentUrl: checkoutUrlFromYourBackend),
callbackConfig: PaymentCallbackConfig(
scheme: 'https',
host: 'pay.example.com',
path: '/payment/complete',
expectedState: stateFromYourBackend,
),
);
switch (result.status) {
case PaymentStatus.success:
// Send result.transactionId and the server-side order ID to your backend.
break;
case PaymentStatus.failed || PaymentStatus.cancelled || PaymentStatus.timedOut:
// Show an appropriate retry or cancellation state.
break;
}Set the callback intent filter in the consuming app's
android/app/src/main/AndroidManifest.xml; a plugin cannot safely declare the
merchant's host on its behalf. For an HTTPS Android App Link, use a verified
host and publish assetlinks.json for the app signing certificate:
<activity android:name=".MainActivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="pay.example.com"
android:path="/payment/complete" />
</intent-filter>
</activity>A custom scheme uses the same filter with android:scheme="yourapp" and its
configured host/path. Callback matching is exact for scheme, host, path, and
the single state parameter; unexpected callbacks do not complete the active
session. PaymentResult is only a client-side redirect outcome—never grant an
entitlement or mark a payment settled until a trusted backend verifies it with
the payment provider. The plugin does not perform provider-specific settlement
verification, refunds, or webhook handling.
Query currently owned products or subscriptions:
final purchases = await iap.queryPurchases(
type: IapProductType.inApp,
);After the backend has verified and durably recorded a consumable purchase:
await iap.consume(purchase);Subscriptions cannot be consumed. A purchase returned by one store cannot be consumed by a client built for the other store.
| Option | Default | Behavior |
|---|---|---|
storePublicKey |
null |
Required by Myket and Bazaar local verification. |
bazaarSecurityMode |
serverVerification |
Enables or disables Poolakey's local RSA check. |
enableSubscriptions |
true |
Requests subscription support for Cafe Bazaar. |
After initialization, inspect iap.capabilities before exposing optional UI:
if (iap.capabilities.supportsSubscriptions) {
// Show subscription products.
}Operational failures throw IapException. Branch on its stable code, not on
provider-specific messages:
try {
await iap.initialize();
} on IapException catch (error) {
switch (error.code) {
case IapErrorCode.storeNotInstalled:
// Prompt the user to install the selected store.
break;
case IapErrorCode.serviceUnavailable:
// Offer a retry.
break;
default:
// Record a redacted diagnostic and show a safe fallback.
break;
}
}nativeCode, nativeMessage, nativeExceptionType, and details are
diagnostic fields. Do not use them as the business-logic contract, and do not
log purchase tokens, receipts, signatures, or user secrets.
Only one asynchronous billing operation may run at a time. Overlapping calls
fail with IapErrorCode.operationInProgress.
Create one client for the lifetime of the owning service or feature. Repeated
initialize() calls share the same connection. Dispose it when finished:
await iap.dispose();A disposed client cannot be reused; create a new IranIap instance instead.
After building an APK or AAB, scan it for the unselected billing SDK:
dart run iran_iap verify --store bazaar build/app/outputs/flutter-apk/app-release.apk
dart run iran_iap verify --store myket build/app/outputs/bundle/release/app-release.aabThe verifier requires Python 3. It is a conservative release guard, not a formal proof; keep the store's real-device billing tests in the release process.
Build with --store bazaar|myket through the CLI or pass a matching
IRAN_IAP_STORE Dart define. Rebuild instead of hot restarting.
Confirm JitPack is present in the host Android repositories and is not blocked by a restrictive repository policy.
Use FlutterFragmentActivity (or another ActivityResultRegistryOwner) for the
host activity.
Provide a non-empty storePublicKey and all three manifest placeholders shown
above, then run dart run iran_iap doctor --store myket.
IapErrorCode.notInitialized resets the client's ready state. Call
initialize() again before retrying the operation.
The example/ app demonstrates initialization, product lookup,
purchase, owned-purchase queries, cancellation, errors, and consumption. Run it
with a configured test product and store account:
dart run iran_iap run --project-dir example --store bazaarReal billing tests require a physical Android device with the selected store installed and signed in.
See CONTRIBUTING.md for setup and validation commands. Architecture and release details are in the architecture guide and release checklist.
Security issues should be reported through GitHub's private security advisory flow as described in SECURITY.md.
iran_iap is available under the MIT License. Native billing SDKs
remain subject to their upstream terms; see
THIRD_PARTY_NOTICES.md.