Supported on: iOS SDK
1.0.61+ · Android SDK 1.0.33+ · Flutter SDK 1.0.3+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:
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.
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 throughAppDNA.billing, which returns an AppDNABilling instance:
Get Products
Fetch product information for one or more product identifiers:List<ProductInfo>.
ProductInfo
Purchase a Product
Initiate a purchase for a product: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
Thetransaction 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):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 throwsPlatformException('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.
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:bool.
Get Entitlements
Retrieve all current entitlements for the user:List<Entitlement>.
Entitlement
Real-Time Entitlement Changes
Stream
Listen for entitlement changes in real time via thecom.appdna.sdk/entitlements EventChannel:
Stream<List<Entitlement>>.
Callback
Alternatively, register a callback for entitlement changes:AppDNABillingDelegate
Implement theAppDNABillingDelegate abstract class for fine-grained billing lifecycle callbacks:
onPurchaseCompleted(productId, transaction)— fires after the store completes a purchase (paywall, directAppDNA.billing.purchase(...), or transaction queue retry). Thetransactionmap shape is documented above. On Android,onPurchaseCompletedmay also fire for a purchase whose verification failed earlier or that was made outside the app — at app start, atidentify, or when Play reports it as already owned after its purchase call has ended; such a report has no paywall (an emptypaywall_id,""). Every latepurchase_completed(Android rescues, iOS interrupted / Ask-to-Buy purchases) reachesonPurchaseCompletedpossibly later, at least once: at launch, when you set a billing delegate, or afteridentify. 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 bytransaction['transactionId'](a crash can re-deliver once).onPurchaseFailed(productId, error)— fires on store-side failure (user cancel, payment declined, network). Theerroris a typedObject(usuallyMap<String, dynamic>from native withmessage+typekeys, or a rawString).onEntitlementsChanged(entitlements)— fires on both platforms, exactly when theAppDNA.billing.onEntitlementsChangedstream emits: on Android whenever the active entitlement set changes (purchase, restore, renewal, expiry, refund, andAppDNA.reset(), which fires an empty list when it was not already empty); on iOS whenever a product appears or disappears or itsisActive/expiresAtchanges — 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 onTransaction.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 anEntitlement(productId/store/status/expiresAt/isTrial/offerType) — parse each withEntitlement.fromMap. For strongly-typedEntitlementobjects you can also callAppDNA.billing.getEntitlements()from inside the callback, or subscribe to theAppDNA.billing.onEntitlementsChangedstream instead.onRestoreCompleted(restoredProductIds)— fires when a directAppDNA.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.
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
Auto-Tracked Events
The SDK automatically tracks the following billing events. You don’t need to callAppDNA.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+
