Skip to main content
Supported on: iOS SDK 1.0.61+ · Android SDK 1.0.33+ · React Native SDK 1.0.4+
The AppDNA React Native SDK provides a billing module for managing in-app purchases, subscriptions, entitlement verification, and purchase restoration. All billing logic is handled by the native iOS and Android SDKs through the native AppDNA module’s billing bridge and the AppDNA_entitlements_changed event emitted on NativeEventEmitter.

Configuration

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

await AppDNA.configure({
  apiKey: 'adn_live_xxx',
  env: 'production',
  options: { billingProvider: 'storeKit2' },
});

AppDNABillingProvider Options

ValueDescription
'storeKit2'Native StoreKit 2 (iOS) / Play Billing (Android) — default
'revenueCat'RevenueCat integration
'adapty'Adapty integration (pass API key via adaptyApiKey option)
'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.

Billing Module

Access the billing module through AppDNA.billing, which returns an AppDNABilling instance:
const billing = AppDNA.billing;

Get Products

Fetch product information for one or more product identifiers:
const products = await AppDNA.billing.getProducts(['premium_monthly']);

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

ProductInfo

PropertyTypeDescription
idstringProduct identifier
namestringLocalized product name
descriptionstringLocalized product description
displayPricestringFormatted price string (e.g., “$9.99/mo”)
pricenumberNumeric price value
currency`stringnull`ISO-4217 currency code (e.g., USD)
offerToken`stringnull`Platform-specific offer token (Android)
subscription`SubscriptionInfonull`Subscription details (period, free trial, intro offer), if the product is a subscription

SubscriptionInfo

PropertyTypeDescription
periodstringISO-8601 period (e.g., P1M, P1Y)
periodDescriptionstringLocalized period label (e.g., “Monthly”, “Annual”)
freeTrialPeriod`stringnull`ISO-8601 trial period, if a free trial is configured
introductoryOffer`IntroductoryOffernull`Introductory price, if configured (price, period, type)

Purchase a Product

Initiate a purchase for a product:
const result = await AppDNA.billing.purchase('premium_monthly', {
  offerToken: 'offer-xxx',
});

switch (result.status) {
  case 'purchased':
    console.log('Purchase successful!');
    if (result.entitlement) {
      console.log(`Entitlement: ${result.entitlement.productId}`);
    }
    break;
  case 'cancelled':
    console.log('User cancelled');
    break;
  case 'pending':
    console.log('Purchase pending approval');
    break;
  default:
    console.log(`Unknown status: ${result.status}`);
}
Returns a PurchaseResult.

PurchaseResult

PropertyTypeDescription
statusstringOne of: purchased, cancelled, pending, unknown
entitlement`Entitlementnull`The entitlement granted by the purchase, if any

Transaction Object Keys

The transaction payload delivered to onPurchaseCompleted (and AppDNAPaywallDelegate.onPaywallPurchaseCompleted) is a Record<string, unknown> with the following store-verified keys:
KeyTypeDescription
transactionIdstringApp Store / Play Store transaction identifier
productIdstringPurchased product identifier
purchaseDatestringISO-8601 date of the purchase
originalTransactionId`stringnull`Original transaction ID for renewals; equals transactionId for first purchase
environmentstringProduction or Sandbox
storestringapp_store or play_store
currency`stringnull`ISO-4217 currency code (e.g., USD) when known
price`numbernull`Numeric price paid (in currency units)
// Inside onPurchaseCompleted:
const txnId = transaction.transactionId as string;
const originalTxnId = transaction.originalTransactionId as string | null;
const isRenewal = originalTxnId != null && originalTxnId !== txnId;
The offerToken option on purchase(...) is optional. On Android, pass the offer token from ProductInfo.offerToken to select a specific subscription offer. On iOS, this parameter is ignored. For Apple promotional offers (iOS), the SDK auto-applies any offer associated with the product in App Store Connect.
Server-side receipt verification is performed automatically by the SDK. You do not need to send receipts to your own server for validation.

Restore Purchases

Restore previously purchased products (e.g., after reinstalling the app or switching devices):
const entitlements = await AppDNA.billing.restorePurchases();

for (const ent of entitlements) {
  console.log(`Restored: ${ent.productId} (${ent.status})`);
}
Returns Entitlement[].
Apple requires that all apps with in-app purchases include a restore mechanism. Call restorePurchases() when the user taps a “Restore Purchases” button.

Check Active Subscription

Check whether the user has an active subscription:
const active = await AppDNA.billing.hasActiveSubscription();

if (active) {
  // Unlock premium features
}
Returns boolean.

Get Entitlements

Retrieve all current entitlements for the user:
const ents = await AppDNA.billing.getEntitlements();

for (const ent of ents) {
  console.log(`${ent.productId}: ${ent.status}, trial: ${ent.isTrial}`);
}
Returns Entitlement[].

Entitlement

PropertyTypeDescription
productIdstringThe product identifier
storestringThe store that granted the entitlement
statusstringEntitlement status (e.g., “active”, “expired”)
expiresAt`stringnull`ISO 8601 expiration date, if applicable
isTrialbooleanWhether the entitlement is from a free trial
offerType`stringnull`The type of offer applied (e.g., “introductory”)

Real-Time Entitlement Changes

Subscription callback

Listen for entitlement changes in real time. The SDK bridges the native AppDNA_entitlements_changed event on NativeEventEmitter into a typed callback subscription:
const unsubscribe = AppDNA.billing.onEntitlementsChanged((entitlements) => {
  console.log(`Entitlements changed: ${entitlements.length} active`);
  // Update UI to reflect new entitlement state
});

// Later, when you no longer need updates:
unsubscribe();
onEntitlementsChanged returns an unsubscribe function. Call it on component unmount to avoid leaks.

Raw event channel (advanced)

If you prefer to subscribe to the underlying NativeEventEmitter directly:
import { NativeEventEmitter, NativeModules } from 'react-native';

const emitter = new NativeEventEmitter(NativeModules.AppDNA);
const sub = emitter.addListener('AppDNA_entitlements_changed', (payload: { entitlements: Entitlement[] }) => {
  console.log('Entitlements updated:', payload.entitlements);
});

// Cleanup:
sub.remove();

AppDNABillingDelegate

Implement the AppDNABillingDelegate interface for fine-grained billing lifecycle callbacks:
interface AppDNABillingDelegate {
  onPurchaseCompleted(productId: string, transaction: Record<string, unknown>): void;
  onPurchaseFailed(productId: string, error: Error): void;
  onEntitlementsChanged(entitlements: string[]): void;
  onRestoreCompleted(restoredProductIds: string[]): void;
}
The 4 methods semantics:
  • onPurchaseCompleted(productId, transaction) — fires after the store completes a purchase (paywall, direct AppDNA.billing.purchase(...), or transaction queue retry). The transaction map shape is documented above.
  • onPurchaseFailed(productId, error) — fires on store-side failure (user cancel, payment declined, network). The error is a standard JS Error; the error.message typically contains a JSON-encoded { "message": "...", "type": "..." } blob from native or a plain string.
  • onEntitlementsChanged(entitlements) — fires whenever the active entitlement set changes (purchase, restore, renewal, expiry, refund). Delivers an array of product IDs (the SKUs your store is configured with). For full Entitlement objects with status/expiry/trial, call AppDNA.billing.getEntitlements() from inside the callback, or subscribe to AppDNA.billing.onEntitlementsChanged(...) instead.
  • onRestoreCompleted(restoredProductIds) — fires when a direct AppDNA.billing.restorePurchases() call resolves. Empty array means no prior purchases on file.
Overlap with AppDNAPaywallDelegate. When a purchase originates from a paywall, the paywall delegate fires onPaywallPurchaseStarted / onPaywallPurchaseCompleted / onPaywallPurchaseFailed. The billing delegate ALSO fires onPurchaseCompleted / onPurchaseFailed for that same purchase — they’re independent observers. Register both if you want paywall-specific UI (banners, confetti) AND a global purchase log; register only the billing delegate if you only care about the eventual state.

Example Implementation

const billingHandler: AppDNABillingDelegate = {
  onPurchaseCompleted(productId, transaction) {
    console.log(`Purchased: ${productId}`);
    // Unlock premium features
  },

  onPurchaseFailed(productId, error) {
    // `error.message` is usually a JSON blob `{"message":"...","type":"..."}` from
    // native or a plain string. Parse / stringify when displaying.
    console.log(`Purchase failed: ${productId} -- ${error.message}`);
    // Show error message to user
  },

  onEntitlementsChanged(entitlements) {
    console.log(`Entitlement product IDs: ${entitlements.join(', ')}`);
    // For full entitlement objects (status, expiry, trial), call:
    //   const ents = await AppDNA.billing.getEntitlements();
  },

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

AppDNA.billing.setDelegate(billingHandler);

Full Example

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, Pressable, Button, Alert } from 'react-native';
import { AppDNA, ProductInfo } from '@appdna/react-native-sdk';

export function SubscriptionPage() {
  const [products, setProducts] = useState<ProductInfo[]>([]);
  const [isSubscribed, setIsSubscribed] = useState(false);

  useEffect(() => {
    loadProducts();
    checkSubscription();

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

    return unsubscribe;
  }, []);

  async function loadProducts() {
    const result = await AppDNA.billing.getProducts(['premium_monthly', 'premium_yearly']);
    setProducts(result);
  }

  async function checkSubscription() {
    const active = await AppDNA.billing.hasActiveSubscription();
    setIsSubscribed(active);
  }

  async function purchase(product: ProductInfo) {
    const result = await AppDNA.billing.purchase(product.id, {
      offerToken: product.offerToken ?? undefined,
    });
    if (result.status === 'purchased') {
      Alert.alert('Purchase successful!');
    }
  }

  async function restore() {
    const restored = await AppDNA.billing.restorePurchases();
    Alert.alert(`Restored ${restored.length} purchases`);
  }

  return (
    <View style={{ flex: 1 }}>
      <View style={{ padding: 16, flexDirection: 'row', justifyContent: 'flex-end' }}>
        <Button title="Restore" onPress={restore} />
      </View>
      <FlatList
        data={products}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable
            onPress={() => purchase(item)}
            style={{ padding: 16, borderBottomWidth: 1, borderColor: '#eee' }}
          >
            <Text style={{ fontWeight: 'bold' }}>{item.name}</Text>
            <Text>{item.description}</Text>
            <Text>{item.displayPrice}</Text>
          </Pressable>
        )}
      />
    </View>
  );
}
Always test purchases using sandbox/test accounts. On iOS, use StoreKit Testing in Xcode or a Sandbox Apple ID. On Android, use license testing accounts configured in the Google Play Console.

Auto-Tracked Events

The SDK automatically tracks the following billing events. You don’t need to call AppDNA.track(...) for any of them.
EventTrigger
purchase_startedCustomer taps the purchase CTA. Properties include productId, paywallId (when triggered from a paywall).
purchase_completedPurchase succeeds. Properties include productId, transactionId, price, currency.
purchase_failedPurchase fails. Properties include productId, error.
purchase_canceledCustomer dismisses the platform purchase sheet without completing.
purchase_pendingA purchase is waiting for approval (e.g., Ask to Buy, deferred payment).
purchase_restoredA previously purchased product is restored.
Android-only events (fired by Play Billing’s subscription lifecycle):
EventTrigger
billing_purchase_requestedA purchase call has been queued and is about to launch the Play Billing flow.
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 flow into the AppDNA Console exactly like any custom event you track yourself, and feed retention / monetization dashboards out of the box.