Supported on: Android SDK
1.0.33+Configuration
The Android SDK defaults to Google Play Billing, but the billing backend is selectable via thebillingProvider configuration option (default BillingProvider.StoreKit2, which maps to Google Play Billing on Android). You enable billing by calling AppDNA.configure(...) from your Application.onCreate() and ensuring the Play Billing dependency is on your classpath (the SDK pulls it in transitively).
BillingProvider is a sealed class with these cases:
BillingProvider.StoreKit2— the default (omitted =StoreKit2): AppDNA’s own billing against Google Play.BillingProvider.RevenueCat— carries no key; RevenueCat is your purchase SDK and owns every purchase.BillingProvider.Adapty(apiKey)— Adapty is your purchase SDK and owns every purchase. When the Adapty SDK is on the classpath, AppDNA activates it with this key —Adapty.activate(context, AdaptyConfig)(Adapty 2.11 and later, the only form in 4.x), orAdapty.activate(context, apiKey)on an older 2.x; do not activate Adapty yourself as well, or it is activated twice. An empty key is refused: Adapty is not activated and an error is logged.BillingProvider.None— billing disabled; purchases and restores fail withproviderNotAvailable(“No billing provider configured”).
Who owns a purchase
Exactly one component finishes a store transaction. On Android the SDK acknowledges (or consumes) a Play purchase only whenbillingProvider is StoreKit2:
Choose
RevenueCat when RevenueCat is your purchase SDK. AppDNA then never finishes or acknowledges a purchase; a tap on a plan in an AppDNA paywall calls onPaywallPurchaseFailed(errorType = "providerNotAvailable", productId = …) (and emits purchase_failed with error_type: "providerNotAvailable") without opening the Play sheet; and purchases reach AppDNA through the RevenueCat webhook. Recipe: catch that callback, start the purchase with RevenueCat yourself (Purchases.sharedInstance.purchase(...)), then dismiss the paywall. Adapty behaves the same on Android: start the purchase with Adapty from the callback.
The SDK’s internal RevenueCat and Adapty bridge classes are not usable on Android — they are never used for purchases, and their forwarding methods are not public (the Adapty bridge only activates Adapty with your key). Under
RevenueCat / Adapty the SDK only reads your Play purchases locally (read-only entitlements) and never writes to the store. Use the provider’s own entitlement API (CustomerInfo for RevenueCat, Adapty’s profile access levels) as the source of truth; the SDK’s store-derived entitlements can differ (e.g. promotional grants). If the provider does not acknowledge a purchase within 3 days, Play refunds it — that is the provider’s contract.Module Access
Access the billing module through theAppDNA.billing property:
Get Products
Retrieve product details for one or more product IDs (suspend function — call from a coroutine, or use theFuture overload from Java):
AppDNA.billing.getProductsFuture(productIds): CompletableFuture<List<ProductInfo>>.
ProductInfo
Purchase a Subscription
Initiate a purchase flow for a specific product.purchase is a suspend function that returns a TransactionInfo on success and requires the current Activity to host the Play Billing flow:
AppDNABillingDelegate (see below).
Promotional offers
Pass aPurchaseOptions to select a specific subscription offer token:
Server-side receipt verification is performed automatically by the SDK. You do not need to send purchase tokens to your own server for validation. With
billingProvider: StoreKit2, Android purchases need Google Play credentials in AppDNA (else Play refunds them after 3 days) — see Google Play credentials and notifications. Apps whose billingProvider is RevenueCat or Adapty do not add them.PurchaseOptions.appAccountToken is supported: the purchase is verified and granted to the signed-in caller unless another user already owns it.
Consumables
Mark consumable products as Consumable in the console (Monetization → Products). The SDK consumes a consumable immediately after it is verified and reported, so it can be bought again at once.purchase_completed carries is_consumable.
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 delivering billing delegate is set (setDelegate(delegate), or setDelegate(delegate, deliversPurchases = true)), otherwise queued and delivered when one is set or after identify. Grant idempotently by transactionId (a crash can re-deliver once). The queue keeps 100 entries for 30 days; past either, a manual grant from the dashboard / support is needed. Known: TransactionInfo has no quantity (a quantity above 1 under-grants), and after a reinstall an earlier acknowledged purchase is not reported again.
Purchase errors
A failedAppDNA.billing.purchase throws one of the exceptions below (the Java purchaseFuture completes exceptionally with it). Call billingErrorType(e) to map any of them to one of userCancelled, productNotFound, verificationFailed, networkError, serverError, pending, providerNotAvailable, unknown:
providerNotAvailable—billingProviderisRevenueCat,AdaptyorNone(BillingError.ProviderNotAvailable; underNonethe message is “No billing provider configured”). Start the purchase with your provider instead.verificationFailed— AppDNA’s server could not verify the purchase;purchase_failed.reasoncarries the server’s code (for examplestore_credentials_missing) when present.- A purchase started while the Play connection is failing fails within 30 s with
serverError. purchase/restorePurchasescalled beforeconfigurehas set billing up, or afterAppDNA.shutdown(), fail withunknownand the message “AppDNA SDK not configured yet — call configure() first”. A paywall purchase tap in the same window — before billing is ready (including betweenconfigure()and the end of billing setup) or aftershutdown()— fails the same way: onepurchase_failedwithreason: "not_configured"anderror_type: "unknown", andonPaywallPurchaseFailedwitherrorType == "unknown"(notproviderNotAvailable). A paywall restore tap in that window fails with the sameunknownerror (onPaywallRestoreFailed, onepurchase_restore_failedwithreason: "not_configured"), withoutonPaywallRestoreStarted. See A tap before billing is ready.item_already_owned— a re-tap of an owned item that could not be completed; see Re-buying an owned item.
Restore Purchases
Restore previously purchased subscriptions and one-time products (e.g., after a device change or reinstall). Suspend function — returnsList<String> of restored product IDs:
Restoring purchases queries Google Play for all active subscriptions and updates entitlements accordingly. This is useful after a reinstall or device change.
Restore can fail
restorePurchases() throws on failure (the Java restorePurchasesAsync() completes exceptionally). Entitlements are left unchanged, onRestoreCompleted does not fire, and purchase_restore_failed is tracked once billing has initialised (a call before that throws the “AppDNA SDK not configured yet” unknown error untracked; a CancellationException from your own cancelled coroutine is rethrown untracked). The error is a billingErrorType value:
reset() and identify with a user different from the one already identified clear the cached entitlements (the first identify after an anonymous session keeps them); they come back from the next refresh or entitlement update.
Entitlements
Retrieve the current user’s entitlements (suspend function):Entitlement
Check Active Subscription
Quickly check if the user has any active entitlement.hasActiveSubscription() counts active one-time products (a lifetime unlock) as well as subscriptions, on both platforms:
Listen for Entitlement Changes
Register a listener to be notified when entitlements change (e.g., subscription renewal, expiration, or new purchase):AppDNABillingDelegate
All 5 methods on this delegate fire from the active billing bridge (Google PlayNativeBillingManager). Register via AppDNA.billingDelegate = yourHandler.
onPurchaseCompletedandonPurchaseFailedfire for every purchase regardless of entry point — paywall-driven OR directAppDNA.billing.purchase(...)calls.onPurchaseCompletedmay also fire later, at least once — at app start, onsetDelegate, or afteridentify— for a purchase whose verification failed earlier or that was made outside the app (a Play Store resubscribe, a promo code, a pending purchase that completed while the app was closed). Such a report has no paywall (paywall_idis""). Grant idempotently bytransaction.transactionId.onRestoreCompletedfires after every successful restore.onEntitlementsChangedfires whenever entitlements change (also broadcast via the localEntitlementCache).onBillingUnavailablefires once when the billing connection gives up after exhausting all reconnect attempts (Play Services missing or permanently broken). Hide paywalls / disable purchase UI when this fires.
AppDNAPaywallDelegate: when a purchase comes from a paywall, both AppDNAPaywallDelegate.onPaywallPurchaseCompleted AND AppDNABillingDelegate.onPurchaseCompleted fire. Each represents a different layer (paywall lifecycle vs platform billing). Pick one as your source of truth for your analytics — they will not double-count if you only listen on one.
For more granular control, implement the AppDNABillingDelegate interface:
Late purchases and the delivery queue
Purchase reports that did not resolve a livepurchase() call (rescued purchases, pending purchases approved later, a cancelled purchase() call) are kept in a durable queue and delivered through onPurchaseCompleted when a billing delegate is set, after identify, or once billing has initialised during configure’s background bootstrap. Any billing delegate drains the queue, so implement onPurchaseCompleted — or register a delegate that should not receive queued purchases with:
setDelegate(delegate) is kept and delivers queued purchases. A callback that returns (even after its timeout) counts as delivered.
Example Implementation
Data Types
TransactionInfo
PurchaseResult
ThePurchaseResult sealed class represents all possible outcomes of a purchase:
Auto-Tracked Events
The billing module automatically tracks the following events:These events include the product identifier and relevant metadata. They are tracked regardless of whether you implement
AppDNABillingDelegate.Under
billingProvider: StoreKit2 the SDK acknowledges purchases (and consumes consumables) after server verification, as Google Play requires within 3 days; an unacknowledged purchase is refunded by Play. Under RevenueCat / Adapty / None the SDK never acknowledges. Re-tapping an owned non-consumable or subscription emits one purchase_restored with reason: "item_already_owned" (no price), not a second conversion.Re-buying an owned item
When Play answers a purchase with “item already owned”, the SDK looks at the owned purchase for that product (it never runs a full restore):
Do not grant from either failure. A purchase whose verification failed is retried at the next return to the foreground (app start included),
identify, restore or refreshEntitlementCache() (see Consumables for how a late completion is delivered). A consumable that was consumed silently is bought again once while the purchase screen is still open; if the sheet cannot be relaunched, the purchase also ends Failed("item_already_owned") with the same purchase_failed.
Google Play credentials and notifications
Needed when AppDNA handles purchases (billingProvider: StoreKit2); apps on RevenueCat or Adapty do not add Play credentials.
- In the console open Settings → Store Credentials, and in the Google Play Console card paste your Google Cloud service account (with access to your app in the Play Console) into Service Account JSON and your app’s Package Name.
- Click Save Credentials; the page then shows the Google Play Webhook URL — copy it.
- In Google Cloud → Pub/Sub, create a topic for Play Real-time developer notifications, select it in the Play Console (Monetization setup), and add a push subscription on that topic whose endpoint is the AppDNA webhook URL, with authentication enabled.
is_trial: true, price 0) from the subscription’s current offer phase reported by Play (offerPhase). A fallback that reads the offer’s first phase through Play’s monetization API is not enabled; it would need the “View app information” permission for the service account and would degrade gracefully on a permission error. Known: a purchase reported after its trial ended is reported with is_trial: false at full price.
Full Example
Next Steps
- Configure Onboarding flows to guide new users
- Present Paywalls with integrated purchase handling
- Learn about Offline Support for billing resilience
Revenue the SDK cannot see
Supported on: Android SDK
1.0.55+
