aptoide_iap_android 0.1.5 copy "aptoide_iap_android: ^0.1.5" to clipboard
aptoide_iap_android: ^0.1.5 copied to clipboard

PlatformAndroid

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'),
          ),
        ),
      );
}
0
likes
160
points
198
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for the Aptoide Android Billing SDK. Supports in-app purchases, subscriptions, and free trials via the Aptoide Connect platform.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on aptoide_iap_android

Packages that implement aptoide_iap_android