Skip to main content
Supported on: Android SDK 1.0.33+
The AppDNA billing module provides a unified interface for in-app purchases, subscription management, and entitlement checking on Android. It integrates with Google Play Billing Library 7.0.0 and handles purchase acknowledgment, restore, and entitlement state synchronization automatically.

Configuration

The Android SDK uses Google Play Billing exclusively — there is no billingProvider configuration option. You enable billing simply by calling AppDNA.configure(...) from your Application.onCreate() and ensuring the Play Billing dependency is on your classpath (the SDK pulls it in transitively).
AppDNA.configure(
    context = applicationContext,
    apiKey = "adn_live_xxx",
    environment = Environment.PRODUCTION,
)
RevenueCat and Adapty bridges are iOS-only. On Android, all billing flows route through the native NativeBillingManager against Google Play. If you have a multi-platform app and use one of those providers on iOS, your Android implementation will exchange the same product IDs but go directly through Play.

Module Access

Access the billing module through the AppDNA.billing property:
val billing = AppDNA.billing

Get Products

Retrieve product details for one or more product IDs (suspend function — call from a coroutine, or use the Future overload from Java):
import androidx.lifecycle.lifecycleScope

lifecycleScope.launch {
    val products = AppDNA.billing.getProducts(listOf("premium_monthly", "premium_yearly"))
    // Returns List<ProductInfo>
    for (product in products) {
        Log.d("Billing", "${product.name}: ${product.formattedPrice}")
    }
}
Java-friendly variant: AppDNA.billing.getProductsFuture(productIds): CompletableFuture<List<ProductInfo>>.

ProductInfo

PropertyTypeDescription
idStringProduct identifier
nameStringDisplay name
descriptionStringLocalized product description
formattedPriceStringLocalized price string (e.g., “$9.99”)
priceMicrosLongPrice in micros (e.g., 9990000)
currencyCodeStringISO 4217 currency code (e.g., “USD”)
offerTokenString?Offer token for the subscription offer

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:
import androidx.lifecycle.lifecycleScope

lifecycleScope.launch {
    try {
        val transaction = AppDNA.billing.purchase(
            activity = this@MyActivity,
            productId = "premium_monthly",
        )
        Log.d("Billing", "Purchased: ${transaction.productId}")
    } catch (e: Exception) {
        Log.e("Billing", "Purchase failed", e)
    }
}
The Google Play purchase result also fires through your registered AppDNABillingDelegate (see below).
Pass the foreground Activity directly — the SDK uses it to launch Google Play’s billing activity. Calling from a Service or Application context throws.

Promotional offers

Pass a PurchaseOptions to select a specific subscription offer token:
import ai.appdna.sdk.billing.PurchaseOptions

lifecycleScope.launch {
    val products = AppDNA.billing.getProducts(listOf("premium_monthly"))
    val product = products.firstOrNull() ?: return@launch
    val offerToken = product.offerToken

    val transaction = AppDNA.billing.purchase(
        activity = this@MyActivity,
        productId = "premium_monthly",
        options = PurchaseOptions(offerToken = offerToken),
    )
}
Server-side receipt verification is performed automatically by the SDK. You do not need to send purchase tokens to your own server for validation.

Restore Purchases

Restore previously purchased subscriptions and one-time products (e.g., after a device change or reinstall). Suspend function — returns List<String> of restored product IDs:
lifecycleScope.launch {
    val restoredProductIds = AppDNA.billing.restorePurchases()
    Log.d("Billing", "Restored ${restoredProductIds.size} products")
}
Restoring purchases queries Google Play for all active subscriptions and updates entitlements accordingly. This is useful after a reinstall or device change.

Entitlements

Retrieve the current user’s entitlements (suspend function):
lifecycleScope.launch {
    val entitlements = AppDNA.billing.getEntitlements()
    for (entitlement in entitlements) {
        Log.d("Billing", "${entitlement.productId}: status=${entitlement.status}")
    }
}

Entitlement

PropertyTypeDescription
productIdStringProduct identifier
storeStringStore identifier (e.g., "play_store")
statusStringStatus (e.g., "active", "expired")
expiresAtString?ISO 8601 expiration timestamp
isTrialBooleanWhether this is a free trial
offerTypeString?Offer type if applicable

Check Active Subscription

Quickly check if the user has any active subscription:
val hasSubscription = AppDNA.billing.hasActiveSubscription()

if (hasSubscription) {
    // Unlock premium features
}

Listen for Entitlement Changes

Register a listener to be notified when entitlements change (e.g., subscription renewal, expiration, or new purchase):
AppDNA.billing.onEntitlementsChanged { entitlements ->
    val activeIds = entitlements
        .filter { it.status == "active" }
        .map { it.productId }
    Log.d("Billing", "Active entitlements: $activeIds")
}

AppDNABillingDelegate

All 4 methods on this delegate fire from the active billing bridge (Google Play NativeBillingManager). Register via AppDNA.billingDelegate = yourHandler.
  • onPurchaseCompleted and onPurchaseFailed fire for every purchase regardless of entry point — paywall-driven OR direct AppDNA.billing.purchase(...) calls.
  • onRestoreCompleted fires after every successful restore.
  • onEntitlementsChanged fires whenever entitlements change (also broadcast via the local EntitlementCache).
Overlap with 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:
import ai.appdna.sdk.AppDNABillingDelegate
import ai.appdna.sdk.TransactionInfo
import ai.appdna.sdk.billing.Entitlement
// purchase() takes the foreground Activity directly — actual signature is
// `suspend fun purchase(activity: Activity, productId: String, options: PurchaseOptions? = null): TransactionInfo`

interface AppDNABillingDelegate {
    fun onPurchaseCompleted(productId: String, transaction: TransactionInfo) {}
    fun onPurchaseFailed(productId: String, error: Throwable) {}
    fun onEntitlementsChanged(entitlements: List<Entitlement>) {}
    fun onRestoreCompleted(restoredProducts: List<String>) {}

    /**
     * Fires once when the SDK gives up on connecting to Play Billing (Play
     * Services missing or permanently broken on this device). Hide paywalls
     * / disable purchase UI when this fires so users don't tap a dead button.
     */
    fun onBillingUnavailable() {}
}
All methods have default empty implementations — override only the ones you need.

Example Implementation

class BillingHandler : AppDNABillingDelegate {

    override fun onPurchaseCompleted(productId: String, transaction: TransactionInfo) {
        Log.d("Billing", "Purchase completed: $productId")
        // Update UI, unlock features
    }

    override fun onPurchaseFailed(productId: String, error: Throwable) {
        Log.e("Billing", "Purchase failed: $productId${error.message}", error)
        // Show error to user
    }

    override fun onEntitlementsChanged(entitlements: List<Entitlement>) {
        val active = entitlements.filter { it.status == "active" }
        Log.d("Billing", "Entitlements updated: ${active.size} active")
    }

    override fun onRestoreCompleted(restoredProducts: List<String>) {
        Log.d("Billing", "Restored ${restoredProducts.size} products")
    }

    override fun onBillingUnavailable() {
        Log.w("Billing", "Play Billing unavailable — hide purchase UI")
        // Hide paywalls, disable purchase buttons
    }
}

Data Types

TransactionInfo

PropertyTypeDescription
transactionIdStringGoogle Play transaction identifier
productIdStringProduct identifier
purchaseDateStringISO 8601 purchase timestamp
environmentString"production" or "sandbox"

PurchaseResult

The PurchaseResult sealed class represents all possible outcomes of a purchase:
VariantDescription
PurchaseResult.Purchased(entitlement)Purchase succeeded
PurchaseResult.CancelledUser cancelled the purchase flow
PurchaseResult.PendingPurchase is pending (e.g., awaiting payment)
PurchaseResult.UnknownUnknown result
PurchaseResult.Failed(error)Purchase failed with an error

Auto-Tracked Events

The billing module automatically tracks the following events:
EventTriggered When
billing_purchase_requestedA purchase call has been queued and is about to launch the Play Billing flow
purchase_startedA purchase flow is initiated
purchase_completedA purchase completes successfully
purchase_failedA purchase fails with an error
purchase_canceledThe user cancels the purchase flow
purchase_pendingA purchase enters pending state
purchase_restoredAn individual purchase is restored
subscription_renewedAn auto-renewing subscription successfully renews
subscription_canceledA subscription is cancelled (access may continue until period end)
subscription_renewal_failedA renewal attempt fails (payment issue)
These events include the product identifier and relevant metadata. They are tracked regardless of whether you implement AppDNABillingDelegate.
The SDK automatically acknowledges purchases within 3 days as required by Google Play. If a purchase is not acknowledged within this window, Google Play will automatically refund it. The NativeBillingManager also handles ITEM_ALREADY_OWNED errors by automatically triggering a restore flow.

Full Example

import ai.appdna.sdk.AppDNA
import ai.appdna.sdk.billing.AppDNABillingDelegate
import ai.appdna.sdk.billing.TransactionInfo
import ai.appdna.sdk.billing.Entitlement
import kotlinx.coroutines.runBlocking

class SubscriptionManager : AppDNABillingDelegate {

    suspend fun loadProducts() {
        try {
            val products = AppDNA.billing.getProducts(
                listOf("premium_monthly", "premium_annual")
            )
            // Display products in UI
            updateUi(products)
        } catch (e: Exception) {
            Log.e("Billing", "Failed to load products", e)
        }
    }

    suspend fun purchaseMonthly(activity: Activity) {
        try {
            val transaction = AppDNA.billing.purchase(
                activity = activity,
                productId = "premium_monthly",
            )
            unlockPremiumFeatures()
            Log.d("Billing", "Purchased ${transaction.productId}")
        } catch (e: Exception) {
            showError(e)
        }
    }

    suspend fun checkAccess() {
        if (AppDNA.billing.hasActiveSubscription()) {
            unlockPremiumFeatures()
        }
    }

    // AppDNABillingDelegate

    override fun onPurchaseCompleted(productId: String, transaction: TransactionInfo) {
        unlockPremiumFeatures()
    }

    override fun onPurchaseFailed(productId: String, error: Throwable) {
        showError(error)
    }

    override fun onBillingUnavailable() {
        // Play Billing unavailable — hide premium upsells
    }

    override fun onEntitlementsChanged(entitlements: List<Entitlement>) {
        refreshAccessState(entitlements)
    }

    override fun onRestoreCompleted(restoredProducts: List<String>) {
        showRestoredAlert(restoredProducts.size)
    }

    private fun updateUi(products: List<Any>) { /* ... */ }
    private fun unlockPremiumFeatures() { /* ... */ }
    private fun showError(error: Throwable) { /* ... */ }
    private fun refreshAccessState(entitlements: List<Entitlement>) { /* ... */ }
    private fun showRestoredAlert(count: Int) { /* ... */ }
}

Next Steps