Skip to main content
The AppDNA Flutter SDK provides a billing module for managing in-app purchases, subscriptions, entitlement verification, and purchase restoration. All billing logic is handled by the native iOS and Android SDKs through the com.appdna.sdk/billing MethodChannel and the com.appdna.sdk/entitlements EventChannel.

Billing Module

Access the billing module through AppDNA.billing, which returns an AppDNABilling instance:
final billing = AppDNA.billing;

Get Products

Fetch product information for one or more product identifiers:
final products = await AppDNA.billing.getProducts(["premium_monthly"]);

for (final product in products) {
  print("${product.name}: ${product.displayPrice}");
}
Returns List<ProductInfo>.

ProductInfo

PropertyTypeDescription
idStringProduct identifier
nameStringLocalized product name
descriptionStringLocalized product description
displayPriceStringFormatted price string (e.g., “$9.99/mo”)
pricedoubleNumeric price value
offerTokenString?Platform-specific offer token (Android)

Purchase a Product

Initiate a purchase for a product:
final result = await AppDNA.billing.purchase(
  "premium_monthly",
  offerToken: "offer-xxx",
);

switch (result.status) {
  case 'purchased':
    print("Purchase successful!");
    if (result.entitlement != null) {
      print("Entitlement: ${result.entitlement!.productId}");
    }
    break;
  case 'cancelled':
    print("User cancelled");
    break;
  case 'pending':
    print("Purchase pending approval");
    break;
  default:
    print("Unknown status: ${result.status}");
}
Returns a PurchaseResult.

PurchaseResult

PropertyTypeDescription
statusStringOne of: purchased, cancelled, pending, unknown
entitlementEntitlement?The entitlement granted by the purchase, if any
The offerToken parameter is optional. On Android, pass the offer token from ProductInfo.offerToken to select a specific subscription offer. On iOS, this parameter is ignored.

Restore Purchases

Restore previously purchased products (e.g., after reinstalling the app or switching devices):
final entitlements = await AppDNA.billing.restorePurchases();

for (final ent in entitlements) {
  print("Restored: ${ent.productId} (${ent.status})");
}
Returns List<Entitlement>.
Apple requires that all apps with in-app purchases include a restore mechanism. Call restorePurchases() when the user taps a “Restore Purchases” button.

Check Active Subscription

Check whether the user has an active subscription:
final active = await AppDNA.billing.hasActiveSubscription();

if (active) {
  // Unlock premium features
}
Returns bool.

Get Entitlements

Retrieve all current entitlements for the user:
final ents = await AppDNA.billing.getEntitlements();

for (final ent in ents) {
  print("${ent.productId}: ${ent.status}, trial: ${ent.isTrial}");
}
Returns List<Entitlement>.

Entitlement

PropertyTypeDescription
productIdStringThe product identifier
storeStringThe store that granted the entitlement
statusStringEntitlement status (e.g., “active”, “expired”)
expiresAtString?ISO 8601 expiration date, if applicable
isTrialboolWhether the entitlement is from a free trial
offerTypeString?The type of offer applied (e.g., “introductory”)

Real-Time Entitlement Changes

Stream

Listen for entitlement changes in real time via the com.appdna.sdk/entitlements EventChannel:
AppDNA.billing.onEntitlementsChanged.listen((List<Entitlement> entitlements) {
  print("Entitlements changed: ${entitlements.length} active");
  // Update UI to reflect new entitlement state
});
Returns Stream<List<Entitlement>>.

Callback

Alternatively, register a callback for entitlement changes:
AppDNA.billing.onEntitlementsChangedCallback((List<Entitlement> entitlements) {
  print("Entitlements updated");
});

AppDNABillingDelegate

Implement the AppDNABillingDelegate abstract class for fine-grained billing lifecycle callbacks:
abstract class AppDNABillingDelegate {
  void onPurchaseCompleted(String productId, Map<String, dynamic> transaction);
  void onPurchaseFailed(String productId, String error);
  void onEntitlementsChanged(List<Entitlement> entitlements);
  void onRestoreCompleted(List<String> restoredProducts);
}

Example Implementation

class MyBillingHandler implements AppDNABillingDelegate {
  @override
  void onPurchaseCompleted(String productId, Map<String, dynamic> transaction) {
    print("Purchased: $productId");
    // Unlock premium features
  }

  @override
  void onPurchaseFailed(String productId, String error) {
    print("Purchase failed: $productId -- $error");
    // Show error message to user
  }

  @override
  void onEntitlementsChanged(List<Entitlement> entitlements) {
    print("Entitlements updated: ${entitlements.length}");
    // Refresh UI
  }

  @override
  void onRestoreCompleted(List<String> restoredProducts) {
    print("Restored ${restoredProducts.length} products");
  }
}

Full Example

import 'package:appdna_sdk/appdna_sdk.dart';
import 'package:flutter/material.dart';

class SubscriptionPage extends StatefulWidget {
  const SubscriptionPage({super.key});

  @override
  State<SubscriptionPage> createState() => _SubscriptionPageState();
}

class _SubscriptionPageState extends State<SubscriptionPage> {
  List<ProductInfo> _products = [];
  bool _isSubscribed = false;

  @override
  void initState() {
    super.initState();
    _loadProducts();
    _checkSubscription();

    // Listen for entitlement changes
    AppDNA.billing.onEntitlementsChanged.listen((entitlements) {
      setState(() {
        _isSubscribed = entitlements.any((e) => e.status == "active");
      });
    });
  }

  Future<void> _loadProducts() async {
    final products = await AppDNA.billing.getProducts([
      "premium_monthly",
      "premium_yearly",
    ]);
    setState(() => _products = products);
  }

  Future<void> _checkSubscription() async {
    final active = await AppDNA.billing.hasActiveSubscription();
    setState(() => _isSubscribed = active);
  }

  Future<void> _purchase(ProductInfo product) async {
    final result = await AppDNA.billing.purchase(
      product.id,
      offerToken: product.offerToken,
    );
    if (result.status == 'purchased') {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("Purchase successful!")),
      );
    }
  }

  Future<void> _restore() async {
    final restored = await AppDNA.billing.restorePurchases();
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text("Restored ${restored.length} purchases")),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Subscription"),
        actions: [
          TextButton(
            onPressed: _restore,
            child: const Text("Restore"),
          ),
        ],
      ),
      body: ListView(
        children: _products.map((product) {
          return ListTile(
            title: Text(product.name),
            subtitle: Text(product.description),
            trailing: Text(product.displayPrice),
            onTap: () => _purchase(product),
          );
        }).toList(),
      ),
    );
  }
}
Always test purchases using sandbox/test accounts. On iOS, use StoreKit Testing in Xcode or a Sandbox Apple ID. On Android, use license testing accounts configured in the Google Play Console.