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
| Value | Description |
|---|
'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
| Property | Type | Description |
|---|
id | string | Product identifier |
name | string | Localized product name |
description | string | Localized product description |
displayPrice | string | Formatted price string (e.g., “$9.99”) |
price | number | Numeric price value |
offerToken | string | undefined | Offer 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
| Property | Type | Description |
|---|
status | 'purchased' | 'cancelled' | 'pending' | 'unknown' | Outcome of the purchase |
entitlement | Entitlement | undefined | The 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
| Property | Type | Description |
|---|
productId | string | Product that granted this entitlement |
store | string | Store that processed the purchase |
status | string | Current entitlement status |
expiresAt | string | null | ISO 8601 expiration date, if applicable |
isTrial | boolean | Whether this is a trial period |
offerType | string | null | Type 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:
| Event | Payload | Triggered 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:
| 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 |
restore_started | A restore operation begins |
restore_completed | A 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>
);
}