Skip to main content
Supported on: iOS SDK 1.0.70+ · Android SDK 1.0.42+ · React Native SDK 1.0.7+ (New Architecture only)
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.

Configuration

Set the billing provider when configuring the SDK:

AppDNABillingProvider Options

Omitted = 'storeKit2' (AppDNA’s own billing); 'none' disables billing. With 'none', purchase and restore fail with providerNotAvailable and the message “No billing provider configured”. iOS: store purchases belong to the Apple ID, so signing out (AppDNA.reset()) does not hide them — while no user is identified, entitlements include every purchase on the Apple ID. Once a user is identified, entitlements and restore include only that user’s purchases (tagged with the user’s appAccountToken), plus untagged older purchases only for the first user ever identified on the device.

Who owns a purchase

Exactly one component owns a store transaction. AppDNA finishes (iOS) or acknowledges / consumes (Android) a transaction only when you asked it to own billing ('storeKit2', or the option omitted). With any other provider the SDK reads store state and never writes it. Choose 'revenueCat' when RevenueCat is your purchase SDK — AppDNA then never finishes or acknowledges a transaction, SDK paywall taps call onPaywallPurchaseFailed(errorType: 'providerNotAvailable'), and purchases reach AppDNA through the RevenueCat webhook (RevenueCat integration). Do not track purchase or subscription events yourself with AppDNA.track — use custom event names for UI analytics; the webhook is the purchase source. Recipe — an AppDNA paywall with RevenueCat buying. Catch the paywall failure and start the purchase with RevenueCat yourself, then dismiss the paywall:
Call Purchases.logIn(userId) and AppDNA.identify(userId) with the same id, so webhook purchases join the user.
  • Entitlements under RevenueCat / Adapty are read from the store, not from the provider, and can differ from the provider’s own mapping (e.g. promotional grants). Use the provider’s own entitlement API (RevenueCat CustomerInfo, Adapty’s profile) as the source of truth.
  • Adapty: connect the Adapty webhook to add server-side renewal and refund data once its mapping is enabled; the SDK keeps sending device renewal / cancel events.
  • If your app also calls StoreKit / Play Billing directly under a non-owning provider, AppDNA leaves those transactions unfinished / unacknowledged — finishing them is your provider’s job (Play refunds an unacknowledged purchase after 3 days).
  • Older Android SDKs: the SDK now sends billing_owner on verify / restore; an app with a connected RevenueCat or Adapty integration that still runs an older Android SDK on 'storeKit2' gets its verifications refused (409 provider_owned) until it updates.

Google Play credentials (Android, AppDNA-owned billing)

With billingProvider: 'storeKit2', Android purchases need Google Play credentials in AppDNA (otherwise verification fails and Play refunds the purchase after 3 days); apps whose billingProvider is 'revenueCat' or Adapty do not add them. The setup — service-account JSON, package name, Real-time developer notifications and “Push service account” — is on the Android billing page. Mark consumable products as Consumable in Monetization → Products: Android consumes them immediately so they can be bought again.

Billing Module

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

Get Products

Fetch product information for one or more product identifiers:
Returns ProductInfo[].

ProductInfo

Platform-specific keys are omitted, never faked: an absent key means the platform has no concept of it.
There is no numeric price field. iOS’s native price is a Decimal, which cannot cross the bridge without loss, so both platforms send integer priceMicros. Render displayPrice, or divide priceMicros by 1,000,000 when you need the number.

Purchase a Product

Initiate a purchase for a product:
The code is identical on iOS and Android: Android only: purchase() called with no foreground Activity rejects with NO_ACTIVITY, which is not one of these codes. Returns a TransactionInfo.

TransactionInfo

The entitlement the purchase granted arrives on getEntitlements() / onEntitlementsChanged, which is where entitlement state has always lived.

Transaction Object Keys

The transaction payload delivered to onPurchaseCompleted (and AppDNAPaywallDelegate.onPaywallPurchaseCompleted) is the same four keys, as a Record<string, unknown>:
The offerToken argument 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. On iOS, an introductory offer set up in App Store Connect is applied by StoreKit itself; Apple promotional offers (which need a server signature) are not supported by the SDK, and AppDNA does not sign them.
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):
Returns string[] — the restored product IDs, not entitlements.

Restore can fail

restorePurchases() rejects on failure; the rejection code is a billingErrorType value, and entitlements are left unchanged (no onRestoreCompleted([])): The codes are exact on Android; on iOS a linked provider’s errors surface as unknown, so treat any rejection as a failure.
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:
Returns boolean. It counts active one-time products (a lifetime unlock) as well as subscriptions, on both platforms.
On Android, AppDNA.reset() and identify with a different user clear the cached entitlements (the first identify after an anonymous session keeps them): hasActiveSubscription() returns false and getEntitlements() is empty until the next refresh or server snapshot fills them again.

Get Entitlements

Retrieve all current entitlements for the user:
Returns Entitlement[].

Entitlement

Real-Time Entitlement Changes

Subscription callback

Listen for entitlement changes in real time. The first subscriber starts the native entitlement observer, so nothing is missed:
onEntitlementsChanged returns an unsubscribe function. Call it on component unmount to avoid leaks.

AppDNABillingDelegate

Implement the AppDNABillingDelegate interface for fine-grained billing lifecycle callbacks:
The 5 methods’ semantics:
  • onPurchaseCompleted(productId, transaction) — fires after the store completes a purchase, including purchases reported later (app start, identify, restore rescue, an Android late ITEM_ALREADY_OWNED for a purchase call that has already ended, iOS interrupted / Ask-to-Buy); delivery starts when a JS onPurchaseCompleted is registered; grant idempotently by transaction.transactionId. The transaction map shape is documented above.
  • onPurchaseFailed(productId, error) — fires on store-side failure (user cancel, payment declined, network). error is the platform’s error message as a string, delivered as unknown — it is not a JS Error, and it has no .message property. Narrow it (String(error)) before displaying. For a machine-readable reason code, use AppDNAPaywallDelegate.onPaywallPurchaseFailed, which also carries errorType.
  • onEntitlementsChanged(entitlements) — fires whenever the active entitlement set changes: on Android on a purchase, restore, renewal, expiry or refund, and on AppDNA.reset() (an empty list, when it was not already empty); on iOS whenever a product appears or disappears or its isActive / expiresAt changes — a purchase, restore, identify, sign-out (AppDNA.reset(): the device’s StoreKit entitlements, which belong to the Apple ID, without the signed-out user’s server-only ones — not an empty list. This holds under every provider of the published package — StoreKit 2, and RevenueCat or Adapty, whose SDKs the published package does not link, so the SDK reads StoreKit itself. Only a source build of the iOS SDK with RevenueCat or Adapty linked in skips this check, see iOS billing), renewal, expiry, refund or revocation (checked on Transaction.updates, on each return to the foreground and at the earliest known expiry; an unchanged state, including the first check after a relaunch, does not fire). On iOS each map’s isActive and expiresAt are real values (StoreKit’s expiry, or the server’s for an entitlement bought on another platform). Delivers an array of entitlement maps in the Entitlement shape (identifier, productId, isActive, expiresAt on both platforms, plus store, status, isTrial, offerType on Android) — not product-ID strings. For typed Entitlement objects, subscribe with AppDNA.billing.onEntitlementsChanged(...) instead, which parses them for you.
  • onRestoreCompleted(restoredProductIds) — fires when a direct AppDNA.billing.restorePurchases() call resolves. Empty array means no prior purchases on file.
  • onBillingUnavailable() — Android only. Play Services is missing or broken and billing will never work on this device. Never fires on iOS. Hide your purchase UI.
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. Later-reported purchases reach only the billing delegate.

Late purchases and the delivery queue

A purchase can complete outside a live purchase() call: on Android, a purchase whose verification failed earlier or that was made outside the app (a Play Store resubscribe, a promo code, a pending purchase completing while the app was closed) is reported at app start, at identify or during restore; on iOS, interrupted and Ask-to-Buy purchases complete later, a purchase made on another device of the same Apple ID may also be reported, and a late purchase made while another user was signed in is delivered when that user signs in. Such a report has no paywall (paywall_id is "").
  • Every late purchase_completed reaches onPurchaseCompleted — possibly later, at least once: the SDK queues it and delivers it only while a JS onPurchaseCompleted is registered with AppDNA.billing.setDelegate — at the moment you register it, and then whenever the SDK reports a purchase, after identify and at the end of configure; on Android also on each return to the foreground (app start included) and after a restore. A billing delegate without onPurchaseCompleted does not drain the queue (the purchase waits for one that has it); a queued purchase counts as delivered once the bridge hands it to your JS onPurchaseCompleted — a handler that throws, rejects or is slow is not retried, so persist the grant yourself.
  • Grant idempotently, keyed on transactionId. JS onPurchaseCompleted can fire at rescue time, and the same order can reach JS again later (e.g. purchase() resolving it on a re-tap of an owned item); a crash can re-deliver once.
  • Consumables are consumed immediately. A consumable whose verification failed during the purchase and that completes later is delivered through onPurchaseCompleted — at once when a JS onPurchaseCompleted is registered, otherwise queued until one is (the later triggers above deliver it only while one is registered). The queue keeps 100 entries for 30 days; past either, grant manually from the dashboard / support.
  • Known: TransactionInfo carries no quantity (a quantity > 1 under-grants); after a reinstall an earlier acknowledged purchase is not reported again.

Example Implementation

Full Example

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. Android-only events (fired by Play Billing’s subscription lifecycle): Under billingProvider: 'revenueCat' the SDK does not emit subscription_renewed / subscription_canceled / subscription_renewal_failed (the RevenueCat webhook is the single source); under 'none' there is no billing and none of these occur. A late purchase_completed (reported at app start, identify or restore) carries an empty paywall_id (""); re-tapping an owned item emits purchase_restored with reason: "item_already_owned" instead of a second conversion. These events flow into the AppDNA Console exactly like any custom event you track yourself, and feed retention / monetization dashboards out of the box.

Revenue the SDK cannot see

Supported on: React Native SDK 1.0.20+
If some of your revenue never passes through the SDK — billing run by your own server, a web checkout, a seat sold by a salesperson — those users are invisible to the paying-user meter unless you say otherwise:
Safe to call on every launch; a user counted by both a purchase and a report is billed once. See Who counts as a paying user for what counts, what it costs and the rules we hold ourselves to.