> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appdna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Billing

> Google Play Billing purchases, entitlements, and restore

<Info>
  **Supported on:** Android SDK `1.0.33+`
</Info>

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 defaults to Google Play Billing, but the billing backend is selectable via the `billingProvider` 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).

```kotlin theme={null}
AppDNA.configure(
    context = applicationContext,
    apiKey = "adn_live_xxx",
    environment = Environment.PRODUCTION,
    // `billingProvider` is a field on AppDNAOptions, not a direct argument.
    // Optional — defaults to BillingProvider.StoreKit2 (Google Play Billing on Android)
    options = AppDNAOptions(billingProvider = BillingProvider.StoreKit2),
)
```

`BillingProvider` is a sealed class with these cases:

* `BillingProvider.StoreKit2` — the platform store (Google Play Billing on Android).
* `BillingProvider.RevenueCat` — carries no key; the host configures `Purchases` itself and forwards results.
* `BillingProvider.Adapty(apiKey)` — starts the Adapty bridge with the given publishable key.
* `BillingProvider.None` — no billing; purchases and entitlement reads fail fast rather than silently no-op.

<Note>
  RevenueCat and Adapty bridges exist on Android too (`integrations/RevenueCatBridge.kt`, `integrations/AdaptyBridge.kt`). Select one with `billingProvider`. When left at the default, all billing flows route through the native `NativeBillingManager` against Google Play.
</Note>

## Module Access

Access the billing module through the `AppDNA.billing` property:

```kotlin theme={null}
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):

```kotlin theme={null}
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

| Property         | Type      | Description                             |
| ---------------- | --------- | --------------------------------------- |
| `id`             | `String`  | Product identifier                      |
| `name`           | `String`  | Display name                            |
| `description`    | `String`  | Localized product description           |
| `formattedPrice` | `String`  | Localized price string (e.g., "\$9.99") |
| `priceMicros`    | `Long`    | Price in micros (e.g., 9990000)         |
| `currencyCode`   | `String`  | ISO 4217 currency code (e.g., "USD")    |
| `offerToken`     | `String?` | 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:

```kotlin theme={null}
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).

<Warning>
  Pass the foreground `Activity` directly — the SDK uses it to launch Google Play's billing activity. Calling from a Service or Application context throws.
</Warning>

### Promotional offers

Pass a `PurchaseOptions` to select a specific subscription offer token:

```kotlin theme={null}
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),
    )
}
```

<Info>
  Server-side receipt verification is performed automatically by the SDK. You do not need to send purchase tokens to your own server for validation.
</Info>

## 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:

```kotlin theme={null}
lifecycleScope.launch {
    val restoredProductIds = AppDNA.billing.restorePurchases()
    Log.d("Billing", "Restored ${restoredProductIds.size} products")
}
```

<Note>
  Restoring purchases queries Google Play for all active subscriptions and updates entitlements accordingly. This is useful after a reinstall or device change.
</Note>

## Entitlements

Retrieve the current user's entitlements (suspend function):

```kotlin theme={null}
lifecycleScope.launch {
    val entitlements = AppDNA.billing.getEntitlements()
    for (entitlement in entitlements) {
        Log.d("Billing", "${entitlement.productId}: status=${entitlement.status}")
    }
}
```

### Entitlement

| Property    | Type      | Description                             |
| ----------- | --------- | --------------------------------------- |
| `productId` | `String`  | Product identifier                      |
| `store`     | `String`  | Store identifier (e.g., `"play_store"`) |
| `status`    | `String`  | Status (e.g., `"active"`, `"expired"`)  |
| `expiresAt` | `String?` | ISO 8601 expiration timestamp           |
| `isTrial`   | `Boolean` | Whether this is a free trial            |
| `offerType` | `String?` | Offer type if applicable                |

## Check Active Subscription

Quickly check if the user has any active subscription:

```kotlin theme={null}
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):

```kotlin theme={null}
AppDNA.billing.onEntitlementsChanged { entitlements ->
    val activeIds = entitlements
        .filter { it.status == "active" }
        .map { it.productId }
    Log.d("Billing", "Active entitlements: $activeIds")
}
```

## AppDNABillingDelegate

All 5 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`).
* `onBillingUnavailable` fires 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.

**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:

```kotlin theme={null}
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

```kotlin theme={null}
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

| Property        | Type     | Description                        |
| --------------- | -------- | ---------------------------------- |
| `transactionId` | `String` | Google Play transaction identifier |
| `productId`     | `String` | Product identifier                 |
| `purchaseDate`  | `String` | ISO 8601 purchase timestamp        |
| `environment`   | `String` | `"production"` or `"sandbox"`      |

### PurchaseResult

The `PurchaseResult` sealed class represents all possible outcomes of a purchase:

| Variant                                 | Description                                  |
| --------------------------------------- | -------------------------------------------- |
| `PurchaseResult.Purchased(entitlement)` | Purchase succeeded                           |
| `PurchaseResult.Cancelled`              | User cancelled the purchase flow             |
| `PurchaseResult.Pending`                | Purchase is pending (e.g., awaiting payment) |
| `PurchaseResult.Unknown`                | Unknown result                               |
| `PurchaseResult.Failed(error)`          | Purchase failed with an error                |

## Auto-Tracked Events

The billing module automatically tracks the following events:

| Event                         | Triggered When                                                               |
| ----------------------------- | ---------------------------------------------------------------------------- |
| `billing_purchase_requested`  | A purchase call has been queued and is about to launch the Play Billing flow |
| `purchase_started`            | A purchase flow is initiated                                                 |
| `purchase_completed`          | A purchase completes successfully                                            |
| `purchase_failed`             | A purchase fails with an error                                               |
| `purchase_canceled`           | The user cancels the purchase flow                                           |
| `purchase_pending`            | A purchase enters pending state                                              |
| `purchase_restored`           | An individual purchase is restored                                           |
| `subscription_renewed`        | An auto-renewing subscription successfully renews                            |
| `subscription_canceled`       | A subscription is cancelled (access may continue until period end)           |
| `subscription_renewal_failed` | A renewal attempt fails (payment issue)                                      |

<Note>
  These events include the product identifier and relevant metadata. They are tracked regardless of whether you implement `AppDNABillingDelegate`.
</Note>

<Info>
  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.
</Info>

## Full Example

```kotlin theme={null}
import ai.appdna.sdk.AppDNA
import ai.appdna.sdk.AppDNABillingDelegate
import ai.appdna.sdk.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

* Configure [Onboarding](/sdks/android/onboarding) flows to guide new users
* Present [Paywalls](/sdks/android/paywall) with integrated purchase handling
* Learn about [Offline Support](/sdks/android/offline) for billing resilience
