Skip to content

Repository files navigation

iran_iap — Flutter in-app purchases for Cafe Bazaar and Myket

iran_iap

CI pub package License: MIT

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.

Why iran_iap?

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"]
Loading

The unselected billing SDK and its adapter are not compiled into the artifact. The repository includes an artifact scanner to enforce this boundary.

Features

  • 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 IapErrorCode values with optional native diagnostics.
  • Idempotent initialization and disposal.
  • Runtime capability reporting.
  • CLI commands for host checks, store-aware runs/builds, and artifact verification.

Platform support

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

Installation

Add the package:

dependencies:
  iran_iap: ^0.3.0

Then run:

flutter pub get

Android setup

1. Add JitPack

Both 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.

2. Add Myket manifest placeholders

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.

3. Use a compatible host activity

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 myket

Select a store at build time

The 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 -- --release

Place 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=myket

Store selection is compile-time state. Stop and rebuild the app when switching stores; a hot restart cannot change it.

Initialize the client

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.

Query products

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.

Start a purchase

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.

Open a payment URL

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.

Receive a payment callback

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.

Restore and consume purchases

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.

Configuration and capabilities

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.
}

Error handling

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.

Lifecycle

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.

Verify artifact isolation

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.aab

The 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.

Troubleshooting

No store is selected

Build with --store bazaar|myket through the CLI or pass a matching IRAN_IAP_STORE Dart define. Rebuild instead of hot restarting.

Native SDK dependency cannot be resolved

Confirm JitPack is present in the host Android repositories and is not blocked by a restrictive repository policy.

Bazaar reports activityUnavailable

Use FlutterFragmentActivity (or another ActivityResultRegistryOwner) for the host activity.

Myket initialization reports a configuration error

Provide a non-empty storePublicKey and all three manifest placeholders shown above, then run dart run iran_iap doctor --store myket.

The billing connection is lost

IapErrorCode.notInitialized resets the client's ready state. Call initialize() again before retrying the operation.

Example

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 bazaar

Real billing tests require a physical Android device with the selected store installed and signed in.

Contributing

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.

License

iran_iap is available under the MIT License. Native billing SDKs remain subject to their upstream terms; see THIRD_PARTY_NOTICES.md.

About

Build-time isolated Flutter in-app purchases for Cafe Bazaar and Myket

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages