Skip to main content
Supported on: iOS SDK 1.0.61+ · Android SDK 1.0.33+ · Flutter SDK 1.0.3+
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.

Configuration

Set the billing provider when configuring the SDK:

AppDNABillingProvider Options

Omitted = storeKit2 (AppDNA’s own billing); none disables billing. With none, a purchase or restore fails with errorType providerNotAvailable and the message “No billing provider configured”. The setting applies on both iOS and Android. An adapty provider with an empty key is refused and the SDK falls back to the default storeKit2 (iOS logs a warning; Android falls back silently).
iOS: store purchases belong to the Apple ID, so signing out (AppDNA.reset()) does not hide them — while no user is identified, entitlements include every purchase on the Apple ID. Once a user is identified, entitlements and restore include only that user’s purchases (tagged with the user’s appAccountToken), plus untagged older purchases only for the first user ever identified on the device.

Who owns the store transaction

Exactly one component finishes (iOS) or acknowledges (Android) a store transaction. AppDNA does so only when you let it own billing. Choose revenueCat when RevenueCat is your purchase SDK. AppDNA then never finishes or acknowledges a transaction, an SDK paywall tap calls onPaywallPurchaseFailed with errorType: 'providerNotAvailable', and purchases reach AppDNA through the RevenueCat webhook. Paywall price display keeps working (product metadata is still read from the store). Recipe — keep AppDNA paywalls with RevenueCat. Catch the failure and start the purchase with RevenueCat yourself, then dismiss the paywall:
Under revenueCat or adapty, use the provider’s own entitlement API (RevenueCat CustomerInfo, Adapty profile) as the source of truth — AppDNA.billing.getEntitlements() reads the store and can differ from the provider’s mapping (e.g. promotional grants). Do not track purchase or subscription events yourself with AppDNA.track — use custom event names for UI analytics; the provider webhook is the purchase source.
The Android SDK now sends a billing_owner value on its verify/restore calls; the iOS SDK sends its StoreKit 2 purchases to /billing/verify in the background under storeKit2 only, with billing_owner: "sdk". An app with a connected RevenueCat or Adapty integration that still runs an older Android SDK on storeKit2 (Flutter ≤ 1.0.19) gets its verifications refused (409 provider_owned) until it updates.
Adapty: connect the Adapty webhook to add server-side renewal and refund data once its mapping is enabled; the SDK keeps sending device renewal/cancel events. Google Play credentials (storeKit2 only). With billingProvider: storeKit2, Android purchases need Google Play credentials in AppDNA (Settings → Store Credentials), otherwise they cannot be verified and Play refunds them after 3 days. Apps whose billingProvider is revenueCat or adapty do not add them. See Android Billing → Google Play credentials and notifications for the setup, including “Secure your Play notifications” (the Pub/Sub push service account). Consumables. Mark consumable products as Consumable in Monetization → Products; on Android the SDK consumes them immediately, so they can be bought again at once.

Billing Module

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

Get Products

Fetch product information for one or more product identifiers:
Returns List<ProductInfo>.

ProductInfo

Purchase a Product

Initiate a purchase for a product:
Returns a PurchaseResult.

PurchaseResult

Handling a failed purchase

A user cancellation does not throw: purchase returns a PurchaseResult whose status is cancelled (both platforms; the iOS plugin decides it from the typed error — the native billingErrorType is userCancelled — not from the message text, so every userCancelled failure returns cancelled and never throws). Any other failed AppDNA.billing.purchase, a pending purchase included, throws PlatformException(code: 'PURCHASE_ERROR') (on Android, code: 'NO_ACTIVITY' when no Activity is in the foreground). Its details['errorType'] is the native billingErrorType, one of the closed set userCancelled, productNotFound, verificationFailed, networkError, serverError, pending, providerNotAvailable, unknown:
purchase or restorePurchases called before configure completes fails with errorType unknown and the message “AppDNA SDK not configured yet — call configure() first”. A paywall purchase or restore tap in that window, or after AppDNA.shutdown(), fails with the same unknown error (see Paywalls → Error Handling).

Transaction Map Keys

The transaction payload delivered to onPurchaseCompleted (and AppDNAPaywallDelegate.onPaywallPurchaseCompleted) is a Map<String, dynamic> with exactly these four keys, on both platforms: There is no original transaction id, store, price or currency in this map. Grant idempotently by transactionId.
The offerToken parameter on purchase(...) is optional. On Android, pass the offer token from ProductInfo.offerToken to select a specific subscription offer. On iOS, this parameter is ignored. On iOS, an introductory offer set up in App Store Connect is applied by StoreKit itself; Apple promotional offers (which need a server signature) are not supported by the SDK, and AppDNA does not sign them.
With storeKit2 on Android, server-side purchase verification is performed automatically by the SDK (it needs Google Play credentials in AppDNA). You do not need to send receipts to your own server for validation.

Restore Purchases

Restore previously purchased products (e.g., after reinstalling the app or switching devices):
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.

Restore can fail

A failed restore throws PlatformException('RESTORE_ERROR') whose details['errorType'] carries the error type. Entitlements are left unchanged and onRestoreCompleted does not fire. The codes are exact on Android. On iOS a provider error can surface as unknown — treat any RESTORE_ERROR as a failure.
On Android, AppDNA.reset() and identify with a different user clear the cached entitlements (the first identify after an anonymous session keeps them); they come back from the next refresh or entitlement snapshot.

Check Active Subscription

Check whether the user has an active subscription or an active one-time product (for example a lifetime unlock) — on both platforms:
Returns bool.

Get Entitlements

Retrieve all current entitlements for the user:
Returns List<Entitlement>.

Entitlement

Real-Time Entitlement Changes

Stream

Listen for entitlement changes in real time via the com.appdna.sdk/entitlements EventChannel:
Returns Stream<List<Entitlement>>.

Callback

Alternatively, register a callback for entitlement changes:

AppDNABillingDelegate

Implement the AppDNABillingDelegate abstract class for fine-grained billing lifecycle callbacks:
The 5 methods semantics:
  • onPurchaseCompleted(productId, transaction) — fires after the store completes a purchase (paywall, direct AppDNA.billing.purchase(...), or transaction queue retry). The transaction map shape is documented above. On Android, onPurchaseCompleted may also fire for a purchase whose verification failed earlier or that was made outside the app — at app start, at identify, or when Play reports it as already owned after its purchase call has ended; such a report has no paywall (an empty paywall_id, ""). Every late purchase_completed (Android rescues, iOS interrupted / Ask-to-Buy purchases) reaches onPurchaseCompleted possibly later, at least once: at launch, when you set a billing delegate, or after identify. On iOS a purchase made on another device of the same Apple ID may also be reported here, and a late iOS purchase made while another user was signed in is delivered when that user signs in. Grant idempotently by transaction['transactionId'] (a crash can re-deliver once).
  • onPurchaseFailed(productId, error) — fires on store-side failure (user cancel, payment declined, network). The error is a typed Object (usually Map<String, dynamic> from native with message + type keys, or a raw String).
  • onEntitlementsChanged(entitlements) — fires on both platforms, exactly when the AppDNA.billing.onEntitlementsChanged stream emits: on Android whenever the active entitlement set changes (purchase, restore, renewal, expiry, refund, and AppDNA.reset(), which fires an empty list when it was not already empty); on iOS whenever a product appears or disappears or its isActive / expiresAt changes — a purchase, restore, identify, sign-out (AppDNA.reset(): the device’s StoreKit entitlements, which belong to the Apple ID, without the signed-out user’s server-only ones — not an empty list. This holds under every provider of the published plugin — StoreKit 2, and RevenueCat or Adapty, whose SDKs the published plugin does not link, so the SDK reads StoreKit itself. Only a source build of the iOS SDK with RevenueCat or Adapty linked in skips this check, see iOS billing), renewal, expiry, refund or revocation (checked on Transaction.updates, on each return to the foreground and at the earliest known expiry; an unchanged state, including the first check after a relaunch, does not fire). Delivers a list of entitlement maps, each shaped like an Entitlement (productId / store / status / expiresAt / isTrial / offerType) — parse each with Entitlement.fromMap. For strongly-typed Entitlement objects you can also call AppDNA.billing.getEntitlements() from inside the callback, or subscribe to the AppDNA.billing.onEntitlementsChanged stream instead.
  • onRestoreCompleted(restoredProductIds) — fires when a direct AppDNA.billing.restorePurchases() call resolves. Empty list means no prior purchases on file.
  • onBillingUnavailable() — fires when billing is permanently unavailable (Play Services missing or broken). Android-only — it never fires on iOS. Hide paywalls / disable purchase UI when it fires.
Late purchases and the delivery queue. A purchase that completes outside a live purchase() call — an Android purchase rescued at app start, identify, restore, or a late ITEM_ALREADY_OWNED from Play for a purchase call that has already ended; an iOS interrupted or Ask-to-Buy purchase — is queued and delivered through onPurchaseCompleted. Setting any billing delegate with AppDNA.billing.setDelegate(...) drains the queue (a Dart override cannot be detected, so every delegate counts as receiving purchases) — so implement onPurchaseCompleted in any delegate you set. The queue is also drained after identify and at app start. A callback that returns (or times out) counts as delivered. The queue keeps 100 entries for 30 days; past either, a manual grant from the dashboard / support is needed. Consumables. Consumed immediately. A consumable whose verification failed during the purchase and that is completed later (app start, identify, restore) is delivered through onPurchaseCompleted — at once when a billing delegate is set, otherwise queued until one is set or after identify. Grant idempotently by transactionId. Known: transaction carries no quantity (a quantity > 1 under-grants), and after a reinstall an earlier acknowledged purchase is not reported again. Overlap with AppDNAPaywallDelegate. When a purchase originates from a paywall, the paywall delegate fires onPaywallPurchaseStarted / onPaywallPurchaseCompleted / onPaywallPurchaseFailed. The billing delegate ALSO fires onPurchaseCompleted / onPurchaseFailed for that same purchase — they’re independent observers. Register both if you want paywall-specific UI (banners, confetti) AND a global purchase log; register only the billing delegate if you only care about the eventual state.

Example Implementation

Full Example

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.

Auto-Tracked Events

The SDK automatically tracks the following billing events. You don’t need to call AppDNA.track(...) for any of them. Android-only events (fired by Play Billing’s subscription lifecycle): These events flow into the AppDNA Console exactly like any custom event you track yourself, and feed retention / monetization dashboards out of the box.

Revenue the SDK cannot see

Supported on: Flutter plugin 1.0.21+
If some of your revenue never passes through the SDK — billing run by your own server, a web checkout, a seat sold by a salesperson — those users are invisible to the paying-user meter unless you say otherwise:
Safe to call on every launch; a user counted by both a purchase and a report is billed once. See Who counts as a paying user for what counts, what it costs and the rules we hold ourselves to.