aptoide_iap_android 0.1.5
aptoide_iap_android: ^0.1.5 copied to clipboard
Flutter plugin for the Aptoide Android Billing SDK. Supports in-app purchases, subscriptions, and free trials via the Aptoide Connect platform.
example/lib/main.dart
import 'dart:async';
import 'package:aptoide_iap_android/aptoide_iap_android.dart';
import 'package:flutter/material.dart';
// ---------------------------------------------------------------------------
// Replace with your Aptoide Connect public key and product IDs.
// For quick testing you can use the sandbox credentials from the docs:
// applicationId : com.appcoins.sample
// IAB_KEY : MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEt9...
// ---------------------------------------------------------------------------
const _publicKey = 'YOUR_APTOIDE_CONNECT_PUBLIC_KEY';
const _inappProductIds = ['coins_100', 'remove_ads'];
const _subsProductIds = ['premium_monthly', 'premium_yearly'];
void main() {
runApp(const AptoideIapExampleApp());
}
class AptoideIapExampleApp extends StatelessWidget {
const AptoideIapExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aptoide IAP Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF004BFF)),
useMaterial3: true,
),
home: const StorePage(),
);
}
}
// ---------------------------------------------------------------------------
// StorePage — demonstrates all 4 integration steps
// ---------------------------------------------------------------------------
class StorePage extends StatefulWidget {
const StorePage({super.key});
@override
State<StorePage> createState() => _StorePageState();
}
class _StorePageState extends State<StorePage> {
bool _connected = false;
bool _loading = false;
String _status = 'Not connected';
List<ProductDetails> _inappProducts = [];
List<ProductDetails> _subsProducts = [];
List<Purchase> _pendingPurchases = [];
StreamSubscription<PurchaseUpdateEvent>? _purchaseSub;
StreamSubscription<BillingStateEvent>? _stateSub;
@override
void initState() {
super.initState();
_setupListeners();
_connect();
}
@override
void dispose() {
_purchaseSub?.cancel();
_stateSub?.cancel();
AptoideIapAndroid.endConnection();
super.dispose();
}
// ---- Step 1 : connect ----
void _setupListeners() {
// Subscribe BEFORE initialize so no event is missed
_purchaseSub = AptoideIapAndroid.purchasesUpdatedStream.listen(
_onPurchaseUpdated,
onError: (e) => _setStatus('Purchase stream error: $e'),
);
_stateSub = AptoideIapAndroid.billingStateStream.listen((event) {
setState(() {
_connected = event.state == BillingConnectionState.connected;
_status = _connected ? 'Connected' : 'Disconnected';
});
});
}
Future<void> _connect() async {
setState(() => _loading = true);
try {
final result =
await AptoideIapAndroid.initialize(publicKey: _publicKey);
if (result.isOk) {
await _loadProducts();
await _checkPendingPurchases();
} else {
_setStatus('Connect failed: ${result.debugMessage}');
}
} catch (e) {
_setStatus('Error: $e');
} finally {
setState(() => _loading = false);
}
}
// ---- Step 2 : query products ----
Future<void> _loadProducts() async {
final inappResult = await AptoideIapAndroid.queryProductDetails(
productIds: _inappProductIds,
productType: ProductType.inapp,
);
final subsResult = await AptoideIapAndroid.queryProductDetails(
productIds: _subsProductIds,
productType: ProductType.subs,
);
setState(() {
_inappProducts = inappResult.productDetailsList;
_subsProducts = subsResult.productDetailsList;
});
}
Future<void> _checkPendingPurchases() async {
final consumables =
await AptoideIapAndroid.queryPurchases(ProductType.inapp);
final subs =
await AptoideIapAndroid.queryPurchases(ProductType.subs);
final pending = [
...consumables.purchases,
...subs.purchases,
].where((p) => p.isPurchased).toList();
if (pending.isNotEmpty) {
setState(() => _pendingPurchases = pending);
_setStatus('${pending.length} pending purchase(s) found');
}
}
// ---- Step 3 : launch purchase ----
Future<void> _buy(ProductDetails product) async {
setState(() => _loading = true);
try {
// Check free-trial eligibility for subscriptions
bool freeTrial = false;
if (product.productType == ProductType.subs) {
final ftSupported =
await AptoideIapAndroid.isFeatureSupported(FeatureType.freeTrials);
freeTrial = ftSupported == 0;
}
await AptoideIapAndroid.launchBillingFlow(
productId: product.productId,
productType: product.productType,
obfuscatedAccountId: 'user_demo_123',
freeTrial: freeTrial,
);
} catch (e) {
_setStatus('Launch error: $e');
} finally {
setState(() => _loading = false);
}
}
// ---- Step 4 : process & consume ----
void _onPurchaseUpdated(PurchaseUpdateEvent event) {
if (!event.billingResult.isOk) {
_setStatus(
'Purchase error (${event.billingResult.responseCode}): ${event.billingResult.debugMessage}');
return;
}
for (final purchase in event.purchases) {
_deliverAndConsume(purchase);
}
}
Future<void> _deliverAndConsume(Purchase purchase) async {
// In production: validate server-side BEFORE delivering
// https://docs.catappult.io/docs/iap-validators-server-to-server-check-client
_setStatus('Delivering ${purchase.products.join(", ")}…');
final result = await AptoideIapAndroid.consumePurchase(
purchaseToken: purchase.purchaseToken,
);
if (result.billingResult.isOk) {
_setStatus('Purchase delivered & consumed!');
setState(() =>
_pendingPurchases.removeWhere((p) => p.purchaseToken == purchase.purchaseToken));
} else {
_setStatus('Consume failed: ${result.billingResult.debugMessage}');
}
}
void _setStatus(String msg) {
if (mounted) setState(() => _status = msg);
}
// ---- UI ----
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Aptoide IAP Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
Icon(
_connected ? Icons.cloud_done : Icons.cloud_off,
color: _connected ? Colors.green : Colors.red,
),
const SizedBox(width: 12),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
// Status banner
_StatusCard(message: _status),
const SizedBox(height: 16),
// Pending purchases
if (_pendingPurchases.isNotEmpty) ...[
const _SectionHeader('Pending Purchases'),
..._pendingPurchases.map(
(p) => _PurchaseTile(
purchase: p,
onConsume: () => _deliverAndConsume(p),
),
),
const SizedBox(height: 16),
],
// In-app products
const _SectionHeader('In-App Products'),
if (_inappProducts.isEmpty)
const _EmptyHint('No products loaded')
else
..._inappProducts.map(
(p) => _ProductTile(product: p, onBuy: () => _buy(p)),
),
const SizedBox(height: 16),
// Subscriptions
const _SectionHeader('Subscriptions'),
if (_subsProducts.isEmpty)
const _EmptyHint('No subscriptions loaded')
else
..._subsProducts.map(
(p) => _ProductTile(product: p, onBuy: () => _buy(p)),
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Small reusable widgets
// ---------------------------------------------------------------------------
class _StatusCard extends StatelessWidget {
final String message;
const _StatusCard({required this.message});
@override
Widget build(BuildContext context) => Card(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
const Icon(Icons.info_outline, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(message,
style: Theme.of(context).textTheme.bodyMedium),
),
],
),
),
);
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader(this.title);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(title,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
);
}
class _EmptyHint extends StatelessWidget {
final String text;
const _EmptyHint(this.text);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(text,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.grey)),
);
}
class _ProductTile extends StatelessWidget {
final ProductDetails product;
final VoidCallback onBuy;
const _ProductTile({required this.product, required this.onBuy});
String get _price {
if (product.productType == ProductType.inapp) {
return product.oneTimePurchaseOfferDetails?.formattedPrice ?? '—';
}
final phases =
product.subscriptionOfferDetails?.firstOrNull?.pricingPhases;
return phases?.firstOrNull?.formattedPrice ?? '—';
}
@override
Widget build(BuildContext context) => Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: Icon(
product.productType == ProductType.subs
? Icons.autorenew
: Icons.shopping_bag_outlined,
color: Theme.of(context).colorScheme.primary,
),
title: Text(product.title.isNotEmpty
? product.title
: product.productId),
subtitle: Text(product.description.isNotEmpty
? product.description
: product.productType.name),
trailing: FilledButton(
onPressed: onBuy,
child: Text(_price),
),
),
);
}
class _PurchaseTile extends StatelessWidget {
final Purchase purchase;
final VoidCallback onConsume;
const _PurchaseTile({required this.purchase, required this.onConsume});
@override
Widget build(BuildContext context) => Card(
color: Colors.orange.shade50,
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: const Icon(Icons.pending_actions, color: Colors.orange),
title: Text(purchase.products.join(', ')),
subtitle: Text('Token: ${purchase.purchaseToken.substring(0, 12)}…'),
trailing: TextButton(
onPressed: onConsume,
child: const Text('Consume'),
),
),
);
}