Skip to main content
The AppDNA billing module provides a unified interface for in-app purchases, subscription management, and entitlement checking in React Native. It delegates all purchase logic to the native iOS and Android SDKs.

Configuration

Set the billing provider when configuring the SDK:
import { AppDNA } from '@appdna/react-native-sdk';

await AppDNA.configure("adn_live_xxx", "production", {
  billingProvider: "storeKit2",
});

BillingProvider Options

ValueDescription
'storeKit2'Native StoreKit 2 (iOS) / Google Play (Android)
'revenueCat'RevenueCat integration
'none'Disable the billing module entirely
You must set the billingProvider in AppDNAOptions before accessing AppDNA.billing. If set to 'none', billing module methods will throw errors.

Module Access

Access the billing module through the AppDNA.billing property:
const billing = AppDNA.billing;
You can also import the standalone AppDNABilling class:
import { AppDNABilling } from '@appdna/react-native-sdk';

Get Products

Fetch product information from the App Store or Google Play:
const products = await AppDNA.billing.getProducts([
  "premium_monthly",
  "premium_annual",
]);

for (const product of products) {
  console.log(`${product.name}: ${product.displayPrice}`);
}

ProductInfo

PropertyTypeDescription
idstringProduct identifier
namestringLocalized product name
descriptionstringLocalized product description
displayPricestringFormatted price string (e.g., “$9.99”)
pricenumberNumeric price value
offerTokenstring | undefinedOffer token (Android only)

Purchase a Product

Initiate a purchase with an optional offer token:
const result = await AppDNA.billing.purchase("premium_monthly", "offer-token");

switch (result.status) {
  case "purchased":
    console.log("Purchase successful!", result.entitlement);
    break;
  case "cancelled":
    console.log("User cancelled the purchase");
    break;
  case "pending":
    console.log("Purchase is pending (e.g., parental approval)");
    break;
  case "unknown":
    console.log("Unknown purchase status");
    break;
}

PurchaseResult

PropertyTypeDescription
status'purchased' | 'cancelled' | 'pending' | 'unknown'Outcome of the purchase
entitlementEntitlement | undefinedThe entitlement granted, if purchase succeeded
Server-side receipt verification is performed automatically by the native SDK. You do not need to send receipts to your own server for validation.

Restore Purchases

Restore previously purchased products (e.g., after a device change or reinstall):
const entitlements = await AppDNA.billing.restorePurchases();
console.log(`Restored ${entitlements.length} entitlements`);
Returns an array of Entitlement objects that were restored.

Entitlements

Retrieve the current user’s entitlements:
const entitlements = await AppDNA.billing.getEntitlements();

for (const ent of entitlements) {
  console.log(`${ent.productId}: status=${ent.status}, trial=${ent.isTrial}`);
}

Entitlement

PropertyTypeDescription
productIdstringProduct that granted this entitlement
storestringStore that processed the purchase
statusstringCurrent entitlement status
expiresAtstring | nullISO 8601 expiration date, if applicable
isTrialbooleanWhether this is a trial period
offerTypestring | nullType of promotional offer applied

Check Active Subscription

Quickly check if the user has any active subscription:
const 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):
const unsubscribe = AppDNA.billing.onEntitlementsChanged((entitlements) => {
  const active = entitlements.filter((e) => e.status === "active");
  console.log(`Active entitlements: ${active.length}`);
});

// Later, when you no longer need the listener:
unsubscribe();
Always call the unsubscribe function when the component unmounts to prevent memory leaks.

AppDNABillingDelegate

For more granular control, implement the AppDNABillingDelegate interface:
interface AppDNABillingDelegate {
  onPurchaseCompleted(productId: string, transaction: Record<string, unknown>): void;
  onPurchaseFailed(productId: string, error: Error): void;
  onEntitlementsChanged(entitlements: Entitlement[]): void;
  onRestoreCompleted(restoredProducts: string[]): void;
}

Example Implementation

AppDNA.billing.setDelegate({
  onPurchaseCompleted(productId, transaction) {
    console.log("Purchase completed:", productId);
    unlockPremiumFeatures();
  },

  onPurchaseFailed(productId, error) {
    console.log(`Purchase failed: ${productId} -- ${error.message}`);
    showErrorAlert(error);
  },

  onEntitlementsChanged(entitlements) {
    const active = entitlements.filter((e) => e.status === "active");
    console.log(`Entitlements updated: ${active.length} active`);
  },

  onRestoreCompleted(restoredProducts) {
    console.log(`Restored ${restoredProducts.length} products`);
  },
});

NativeEventEmitter Events

The SDK emits the following billing events through React Native’s NativeEventEmitter:
EventPayloadTriggered When
onBillingPurchaseCompleted{ productId: string, transaction: object }A purchase completes successfully
onBillingPurchaseFailed{ productId: string, error: string }A purchase fails with an error
onBillingEntitlementsChanged{ entitlements: Entitlement[] }Entitlements are updated
onBillingRestoreCompleted{ restoredProducts: string[] }A restore operation completes
You do not need to subscribe to NativeEventEmitter events directly. The delegate interface and onEntitlementsChanged listener handle event bridging for you.

Auto-Tracked Events

The SDK automatically tracks the following billing-related events:
EventTriggered When
purchase_startedA purchase flow is initiated
purchase_completedA purchase completes successfully
purchase_failedA purchase fails with an error
purchase_canceledThe user cancels the purchase dialog
restore_startedA restore operation begins
restore_completedA restore operation completes
These events include the product identifier and relevant metadata. They are tracked regardless of whether you implement AppDNABillingDelegate.

Full Example

import { useEffect, useState } from 'react';
import { AppDNA } from '@appdna/react-native-sdk';
import type { Entitlement, ProductInfo } from '@appdna/react-native-sdk';

function SubscriptionScreen() {
  const [products, setProducts] = useState<ProductInfo[]>([]);
  const [hasAccess, setHasAccess] = useState(false);

  useEffect(() => {
    async function loadProducts() {
      const items = await AppDNA.billing.getProducts([
        "premium_monthly",
        "premium_annual",
      ]);
      setProducts(items);

      const active = await AppDNA.billing.hasActiveSubscription();
      setHasAccess(active);
    }

    loadProducts();

    const unsubscribe = AppDNA.billing.onEntitlementsChanged((entitlements) => {
      const active = entitlements.some((e) => e.status === "active");
      setHasAccess(active);
    });

    return () => unsubscribe();
  }, []);

  async function handlePurchase(productId: string) {
    const result = await AppDNA.billing.purchase(productId);
    if (result.status === "purchased") {
      setHasAccess(true);
    }
  }

  async function handleRestore() {
    const restored = await AppDNA.billing.restorePurchases();
    console.log(`Restored ${restored.length} entitlements`);
  }

  return (
    <View>
      {products.map((product) => (
        <Button
          key={product.id}
          title={`${product.name} - ${product.displayPrice}`}
          onPress={() => handlePurchase(product.id)}
        />
      ))}
      <Button title="Restore Purchases" onPress={handleRestore} />
    </View>
  );
}