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
| Property | Type | Description |
|---|
id | String | Product identifier |
name | String | Localized product name |
description | String | Localized product description |
displayPrice | String | Formatted price string (e.g., “$9.99/mo”) |
price | double | Numeric price value |
offerToken | String? | 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
| Property | Type | Description |
|---|
status | String | One of: purchased, cancelled, pending, unknown |
entitlement | Entitlement? | 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
| Property | Type | Description |
|---|
productId | String | The product identifier |
store | String | The store that granted the entitlement |
status | String | Entitlement status (e.g., “active”, “expired”) |
expiresAt | String? | ISO 8601 expiration date, if applicable |
isTrial | bool | Whether the entitlement is from a free trial |
offerType | String? | 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.