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

# iOS Billing

> StoreKit 2 purchases, entitlements, and restore

<Info>
  **Supported on:** iOS SDK `1.0.61+`
</Info>

The AppDNA billing module provides a unified interface for in-app purchases, subscription management, and entitlement checking. It supports StoreKit 2 natively, with optional integrations for RevenueCat and Adapty.

## Configuration

Set the billing provider when configuring the SDK:

```swift theme={null}
AppDNA.configure(
    apiKey: "adn_live_xxx",
    environment: .production,
    options: AppDNAOptions(billingProvider: .storeKit2)
)
```

### BillingProvider Options

| Value              | Description                             |
| ------------------ | --------------------------------------- |
| `.storeKit2`       | Native StoreKit 2 integration (default) |
| `.revenueCat`      | RevenueCat integration                  |
| `.adapty(apiKey:)` | Adapty integration with your API key    |
| `.none`            | Disable the billing module entirely     |

<Warning>
  You must set the `billingProvider` in `AppDNAOptions` before accessing `AppDNA.billing`. If set to `.none`, billing module methods will throw errors.
</Warning>

## Module Access

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

```swift theme={null}
let billing = AppDNA.billing
```

## Get Products

Fetch product information from the App Store:

```swift theme={null}
let products = try await AppDNA.billing.getProducts([
    "premium_monthly",
    "premium_annual"
])

for product in products {
    print("\(product.displayName): \(product.displayPrice)")
}
```

### ProductInfo

| Property       | Type                | Description                             |
| -------------- | ------------------- | --------------------------------------- |
| `id`           | `String`            | Product identifier                      |
| `displayName`  | `String`            | Localized product name                  |
| `description`  | `String`            | Localized product description           |
| `price`        | `Decimal`           | Numeric price value                     |
| `displayPrice` | `String`            | Formatted price string (e.g., "\$9.99") |
| `subscription` | `SubscriptionInfo?` | Subscription details, if applicable     |

## Purchase a Product

Initiate a purchase:

```swift theme={null}
// Basic purchase
let transaction = try await AppDNA.billing.purchase("premium_monthly")
print("Purchased: \(transaction.productId), txn: \(transaction.transactionId)")

// With promotional offer
let options = PurchaseOptions(promotionalOffer: myOffer)
let transaction = try await AppDNA.billing.purchase("premium_monthly", options: options)
```

The optional `options` parameter accepts a `PurchaseOptions` value for configuring promotional offers or other purchase modifiers.

### TransactionInfo

| Property        | Type     | Description                                             |
| --------------- | -------- | ------------------------------------------------------- |
| `transactionId` | `String` | App Store transaction identifier                        |
| `productId`     | `String` | Purchased product identifier                            |
| `purchaseDate`  | `Date`   | Date of the purchase                                    |
| `environment`   | `String` | Transaction environment (e.g., "Production", "Sandbox") |

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

## Restore Purchases

Restore previously purchased products (e.g., after a device change or reinstall):

```swift theme={null}
let restoredProductIds = try await AppDNA.billing.restorePurchases()
print("Restored \(restoredProductIds.count) products")
```

Returns an array of `String` product identifiers that were restored.

## Entitlements

Retrieve the current user's entitlements:

```swift theme={null}
let entitlements = await AppDNA.billing.getEntitlements()

for entitlement in entitlements {
    print("\(entitlement.identifier): active=\(entitlement.isActive)")
}
```

### Entitlement

| Property     | Type     | Description                                 |
| ------------ | -------- | ------------------------------------------- |
| `identifier` | `String` | Entitlement identifier                      |
| `isActive`   | `Bool`   | Whether the entitlement is currently active |
| `expiresAt`  | `Date?`  | Expiration date, if applicable              |
| `productId`  | `String` | Product that granted this entitlement       |

## Check Active Subscription

Quickly check if the user has any active subscription:

```swift theme={null}
let hasSubscription = await AppDNA.billing.hasActiveSubscription()

if hasSubscription {
    // Unlock premium features
}
```

## Listen for Entitlement Changes

Register a callback to be notified when entitlements change (e.g., subscription renewal, expiration, or new purchase):

```swift theme={null}
AppDNA.billing.onEntitlementsChanged { entitlements in
    let activeIds = entitlements
        .filter { $0.isActive }
        .map { $0.identifier }
    print("Active entitlements: \(activeIds)")
}
```

## AppDNABillingDelegate

All 4 methods on this delegate fire from the active billing bridge (`StoreKit2Bridge`, `RevenueCatBridge`, or `AdaptyBridge`). 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 `NotificationCenter`).

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

```swift theme={null}
protocol AppDNABillingDelegate: AnyObject {
    func onPurchaseCompleted(productId: String, transaction: TransactionInfo)
    func onPurchaseFailed(productId: String, error: Error)
    func onEntitlementsChanged(entitlements: [Entitlement])
    func onRestoreCompleted(restoredProducts: [String])
}
```

### Example Implementation

```swift theme={null}
class BillingHandler: AppDNABillingDelegate {
    func onPurchaseCompleted(productId: String, transaction: TransactionInfo) {
        print("Purchase completed: \(productId)")
        // Update UI, unlock features
    }

    func onPurchaseFailed(productId: String, error: Error) {
        print("Purchase failed: \(productId) — \(error.localizedDescription)")
        // Show error to user
    }

    func onEntitlementsChanged(entitlements: [Entitlement]) {
        let active = entitlements.filter { $0.isActive }
        print("Entitlements updated: \(active.count) active")
    }

    func onRestoreCompleted(restoredProducts: [String]) {
        print("Restored \(restoredProducts.count) products")
    }
}
```

## Auto-Tracked Events

The SDK automatically tracks the following billing-related events:

| Event                | Triggered When                                        |
| -------------------- | ----------------------------------------------------- |
| `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 dialog                  |
| `purchase_pending`   | A purchase is waiting for approval (e.g., Ask to Buy) |
| `purchase_restored`  | A previously purchased product is restored            |

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

## Full Example

```swift theme={null}
import AppDNASDK

class SubscriptionManager: AppDNABillingDelegate {
    func loadProducts() async {
        do {
            let products = try await AppDNA.billing.getProducts([
                "premium_monthly",
                "premium_annual"
            ])
            // Display products in UI
            updateUI(with: products)
        } catch {
            print("Failed to load products: \(error)")
        }
    }

    func purchaseMonthly() async {
        do {
            let transaction = try await AppDNA.billing.purchase("premium_monthly")
            print("Success: \(transaction.transactionId)")
        } catch {
            print("Purchase error: \(error)")
        }
    }

    func checkAccess() async {
        let hasAccess = await AppDNA.billing.hasActiveSubscription()
        if hasAccess {
            unlockPremiumFeatures()
        }
    }

    // MARK: - AppDNABillingDelegate

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

    func onPurchaseFailed(productId: String, error: Error) {
        showError(error)
    }

    func onEntitlementsChanged(entitlements: [Entitlement]) {
        refreshAccessState(entitlements)
    }

    func onRestoreCompleted(restoredProducts: [String]) {
        showRestoredAlert(count: restoredProducts.count)
    }
}
```
