aptoide_iap_android
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
Useful links
- Aptoide Billing SDK docs
- Release notes
- Purchase validation (server-to-server)
- Migration from Google Play Billing
- Example app