aptoide_iap_android

pub version license: MIT

Flutter plugin for the Aptoide Android Billing SDK.
Integrates in-app purchases (consumables & non-consumables) and subscriptions (including free trials) for apps distributed via Aptoide Connect.


Platform support

Android iOS Web

Minimum Android SDK: 21


Installation

# pubspec.yaml
dependencies:
  aptoide_iap_android: ^0.1.0
flutter pub get

Android project setup

In your project-level build.gradle, add the JitPack repository:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url "https://jitpack.io" }  // required by Aptoide Billing SDK
    }
}

The plugin automatically adds the Aptoide Billing SDK dependency to your app.
No additional build.gradle changes are needed in your app module.


Setup

1. Get your public key

Log in to Aptoide Connect → Your App → IAB Key.

2. Initialize once at app startup

Initialize before any UI interaction, ideally in main() or your root widget's initState.
Subscribe to streams before calling initialize to avoid missing the first events.

import 'package:aptoide_iap_android/aptoide_iap_android.dart';

const _publicKey = 'YOUR_APTOIDE_CONNECT_PUBLIC_KEY';

Future<void> setupBilling() async {
  // Subscribe to real-time purchase updates first
  AptoideIapAndroid.purchasesUpdatedStream.listen((event) async {
    if (event.billingResult.isOk) {
      for (final purchase in event.purchases) {
        await _handlePurchase(purchase);
      }
    } else {
      print('Purchase error: ${event.billingResult.debugMessage}');
    }
  });

  // Connect
  final result = await AptoideIapAndroid.initialize(publicKey: _publicKey);

  if (result.isOk) {
    // Check for purchases that weren't processed in a previous session
    await _checkPendingConsumables();
    await _checkActiveSubscriptions();
  }
}

The 4-step integration

Step 1 — Connection

// Connect
final BillingResult result = await AptoideIapAndroid.initialize(
  publicKey: 'YOUR_KEY',
);

// Check readiness before making purchases
final bool ready = await AptoideIapAndroid.isReady;

// Disconnect when done (e.g. app shutdown)
await AptoideIapAndroid.endConnection();

Listen to connection state changes:

AptoideIapAndroid.billingStateStream.listen((event) {
  if (event.state == BillingConnectionState.connected) {
    print('Billing ready');
  } else {
    print('Billing disconnected');
  }
});

Step 2 — Query products

Always fetch product details from Aptoide Connect and use the returned prices in your UI.
This is mandatory for app review.

// One-time in-app products
final QueryProductDetailsResult result =
    await AptoideIapAndroid.queryProductDetails(
  productIds: ['coins_100', 'remove_ads'],
  productType: ProductType.inapp,
);

for (final product in result.productDetailsList) {
  final price = product.oneTimePurchaseOfferDetails?.formattedPrice ?? '';
  print('${product.title} — $price');
}

// Subscriptions
final QueryProductDetailsResult subResult =
    await AptoideIapAndroid.queryProductDetails(
  productIds: ['premium_monthly'],
  productType: ProductType.subs,
);

for (final product in subResult.productDetailsList) {
  final phase =
      product.subscriptionOfferDetails?.first.pricingPhases.first;
  print('${product.title} — ${phase?.formattedPrice}/${phase?.billingPeriod}');
}

// Handle products that failed to load
for (final unfetched in result.unfetchedProductList) {
  print('Failed to load ${unfetched.productId}: ${unfetched.responseCode}');
}

Step 3 — Launch a purchase

// One-time purchase
await AptoideIapAndroid.launchBillingFlow(
  productId: 'coins_100',
  productType: ProductType.inapp,
  obfuscatedAccountId: currentUserId,   // optional, but recommended
  developerPayload: 'order-ref-abc123', // optional, for your own tracking
);

// Subscription with free trial
final freeTrialSupported =
    await AptoideIapAndroid.isFeatureSupported(FeatureType.freeTrials) == 0;

await AptoideIapAndroid.launchBillingFlow(
  productId: 'premium_monthly',
  productType: ProductType.subs,
  obfuscatedAccountId: currentUserId, // required for free trials
  freeTrial: freeTrialSupported,
);

The purchase result is delivered via purchasesUpdatedStream (not from the launchBillingFlow return value).


Step 4 — Process & consume purchases

Future<void> _handlePurchase(Purchase purchase) async {
  // 1. Validate server-side (strongly recommended)
  //    https://docs.catappult.io/docs/iap-validators-server-to-server-check-client
  final valid = await myServer.validatePurchase(
    purchaseToken: purchase.purchaseToken,
    packageName: purchase.packageName,
  );
  if (!valid) return;

  // 2. Deliver the item to the user
  await myStore.grantItem(purchase.products.first);

  // 3. Consume to allow re-purchase (consumables) or activate (subscriptions)
  final ConsumeResult result = await AptoideIapAndroid.consumePurchase(
    purchaseToken: purchase.purchaseToken,
  );

  if (!result.billingResult.isOk) {
    print('Consume failed: ${result.billingResult.debugMessage}');
  }
}

Handle purchases from previous sessions on startup:

Future<void> _checkPendingConsumables() async {
  final result = await AptoideIapAndroid.queryPurchases(ProductType.inapp);
  for (final purchase in result.purchases) {
    await _handlePurchase(purchase);
  }
}

Future<void> _checkActiveSubscriptions() async {
  final result = await AptoideIapAndroid.queryPurchases(ProductType.subs);
  // purchases present = active or pending subscriptions
  // purchases absent  = subscription expired; revoke access
  final activeIds = result.purchases.map((p) => p.products).expand((i) => i).toSet();
  await myStore.syncSubscriptions(activeIds);
}

Complete API reference

AptoideIapAndroid (static)

Method / Property Return type Description
initialize({publicKey}) Future<BillingResult> Connect to the Aptoide Billing service
endConnection() Future<void> Disconnect and release resources
isReady Future<bool> Whether the client is connected
queryProductDetails({productIds, productType}) Future<QueryProductDetailsResult> Fetch product metadata & prices
launchBillingFlow({productId, productType, …}) Future<BillingResult> Start the Aptoide purchase UI
queryPurchases(productType) Future<QueryPurchasesResult> List active/pending purchases
consumePurchase({purchaseToken}) Future<ConsumeResult> Consume / acknowledge a purchase
isFeatureSupported(FeatureType) Future<int> 0 = supported
purchasesUpdatedStream Stream<PurchaseUpdateEvent> Real-time purchase events
billingStateStream Stream<BillingStateEvent> Connect / disconnect events

Enums

Type Values
ProductType inapp, subs
FeatureType freeTrials, obfuscatedAccountId
BillingConnectionState connected, disconnected

Key BillingResponseCode constants

Constant Value Meaning
ok 0 Success
userCanceled 1 User dismissed the purchase UI
serviceUnavailable 2 Network problem
itemAlreadyOwned 7 Non-consumable already purchased
tooManyRequests 1429 Rate limit exceeded — use exponential back-off

Important guidelines

Rule Reason
Initialize in the root widget / Application, not inside an Activity Context lifecycle issues if the Activity is destroyed
Always consume purchases after delivery Unconsumed purchases are auto-refunded after 48 hours
Display API-provided prices Mandatory for Aptoide app review approval
Avoid SDK calls on the main thread This plugin handles threading internally
Validate purchases server-side before delivery Prevents fraud
Use exponential back-off on 1429 errors Avoids being blocked by rate limiting

Testing without an Aptoide Connect account

Use the sandbox credentials provided in the official docs:

applicationId : com.appcoins.sample
IAB_KEY       : MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEt94j9rt0UvpkZ2jPMZZ16y
                UrBOtjpIQCWi/F3HN0+iwSAeEJyDw7xIKfNTEc0msm+m6ud1kJpLK3oCsK61syZ8bYQ
                lNZkUxTaWNof1nMnbw3Xu5nuYMuowmzDqNMWg5jNooy6oxwIgVcdvbyGi5RIlxqbo2vS
                AwpbAAZE2HbUrysKhLME7IOrdRR8MQbSbKEy/9MtfKz0uZCJGi9h+dQb0b69H7Yo+/BN
                /ayBSJzOPlaqmiHK5lZsnZhK+ixpB883fr+PgSczU7qGoktqoe6Fs+nhk9bLElljCs5ZI
                l9/NmOSteipkbplhqLY7KwapDmhrtBgrTetmnW9PU/eCWQIDAQAB

apitoide_android_iap