Subscription Validation
React Native IAP exposes modern StoreKit 2 (iOS) and Google Play Billing (Android) pipelines. This guide walks through the data that is available on the JavaScript side, how it maps to the underlying native APIs, and practical strategies to answer common lifecycle questions such as "is the user currently inside their free trial?"
iOS and Android share the same high-level API surface, but individual capabilities differ. Notes in each section call out platform-specific behaviour—for example,
subscriptionStatusIOS
only exists on Apple platforms, whereas Android relies on Purchase objects and the Play Developer API.
Summary of key surfaces
Capability | API | iOS | Android |
---|---|---|---|
Fetch latest entitlement records the store still considers active | getAvailablePurchases | Wraps StoreKit 2 Transaction.currentEntitlements ; optional flags control listener mirror & active-only filtering | Queries Play Billing twice (inapp + subs ) and merges validated purchases, exposing purchaseToken for server use |
Filter entitlements down to subscriptions only | getActiveSubscriptions | Adds expirationDateIOS , daysUntilExpirationIOS , environmentIOS convenience fields | Re-shapes merged purchase list and surfaces autoRenewingAndroid , purchaseToken , and willExpireSoon placeholders |
Inspect fine-grained subscription phase | subscriptionStatusIOS | StoreKit 2 status API (inTrialPeriod , inGracePeriod , etc.) | Not available; pair getAvailablePurchases with Play Developer API (purchases.subscriptions /purchases.products ) for phase data |
Retrieve receipts for validation | getReceiptDataIOS , validateReceipt | Provides App Store receipt / JWS for backend validation | validateReceipt forwards to OpenIAP’s Google Play validator and expects purchaseToken / packageName |
Working with getAvailablePurchases
getAvailablePurchases
returns every purchase that the native store still considers active for the signed-in user.
- iOS — The library bridges directly to StoreKit 2’s
Transaction.currentEntitlements
, so each item is a fully validatedPurchaseIOS
. Optional flags (onlyIncludeActiveItemsIOS
,alsoPublishToEventListenerIOS
) are forwarded to StoreKit and mimic the native behaviour. - Android — Google Play Billing keeps one list for in-app products and another for subscriptions. The React Native IAP wrapper automatically queries both (
type: 'inapp'
andtype: 'subs'
), merges the results, and validates them before returning control to JavaScript.
Because the data flows through the same validation pipeline as the purchase listeners, every element in the array has the same shape you receive when a new transaction comes in.
import {useEffect} from 'react';
import {useIAP} from 'react-native-iap';
export function SubscriptionGate({subscriptionId}: {subscriptionId: string}) {
const {getAvailablePurchases, availablePurchases} = useIAP();
useEffect(() => {
getAvailablePurchases([subscriptionId]);
}, [getAvailablePurchases, subscriptionId]);
const active = availablePurchases.some(
(purchase) => purchase.productId === subscriptionId,
);
// ...render locked/unlocked UI...
}
Data included
For each purchase you can inspect fields such as:
expirationDateIOS
: milliseconds since epoch when the current period expiresisAutoRenewing
: whether auto-renew is still enabledofferIOS
: original offer metadata (paymentMode
,period
, etc.)environmentIOS
:Sandbox
orProduction
Limitations
StoreKit does not bake "current phase" indicators into these records—offerIOS.paymentMode
tells you which introductory offer was used initially, but does not tell you whether the user is still inside that offer window. To answer questions like "is the user still in a free trial?" you need either the StoreKit status API or server-side receipt validation.
Using getActiveSubscriptions
getActiveSubscriptions
is a thin helper that filters getAvailablePurchases
down to subscription products. It returns an array of ActiveSubscription
objects with convenience fields:
isActive
: alwaystrue
as long as the subscription remains in the current entitlement setexpirationDateIOS
&daysUntilExpirationIOS
: surfaced directly from StoreKittransactionId
/purchaseToken
: handy for reconciling with receipts or Play BillingwillExpireSoon
: flag set by the helper when the subscription is within its grace windowautoRenewingAndroid
: reflects the Google Play auto-renew status for subscriptionsenvironmentIOS
: tells you whether the entitlement came fromSandbox
orProduction
The helper does not fetch additional metadata beyond what getAvailablePurchases
already provides. It exists to make stateful hooks easier to consume.
Platform note: On iOS the helper simply re-shapes the StoreKit 2 entitlement objects. On Android it operates on the merged
inapp
+subs
purchase list returned by Play Billing, so the output always contains both one-time products and subscriptions unless you pass specific product IDs.
import {getActiveSubscriptions} from 'react-native-iap';
const active = await getActiveSubscriptions(['your.yearly.subscription']);
if (active.length === 0) {
// user has no valid subscription
}
Deriving a lightweight phase without subscriptionStatusIOS
If you want a coarse subscription phase that works the same way on iOS and Android, you can compute it from the entitlement cache that backs getActiveSubscriptions
.
import {getActiveSubscriptions} from 'react-native-iap';
const MS_IN_DAY = 1000 * 60 * 60 * 24;
const GRACE_WINDOW_DAYS = 3;
type DerivedPhase = 'subscribed' | 'expiringSoon' | 'expired';
export async function getCurrentPhase(sku: string): Promise<DerivedPhase> {
const subscriptions = await getActiveSubscriptions([sku]);
const entry = subscriptions.find((sub) => sub.productId === sku);
if (!entry) {
return 'expired';
}
const now = Date.now();
const expiresAt = entry.expirationDateIOS ?? null;
if (
typeof entry.daysUntilExpirationIOS === 'number' &&
entry.daysUntilExpirationIOS <= 0
) {
return 'expired';
}
if (expiresAt && expiresAt <= now) {
return 'expired';
}
const graceWindowMs = GRACE_WINDOW_DAYS * MS_IN_DAY;
if (
(expiresAt && expiresAt - now <= graceWindowMs) ||
(typeof entry.daysUntilExpirationIOS === 'number' &&
entry.daysUntilExpirationIOS * MS_IN_DAY <= graceWindowMs) ||
entry.autoRenewingAndroid === false
) {
return 'expiringSoon';
}
return 'subscribed';
}
Tweak
GRACE_WINDOW_DAYS
(or add additional checks such aswillExpireSoon
) to match how your product defines "grace period". For Android plans you can also look atautoRenewingAndroid
and the Play Developer API for richer state.
## StoreKit 2 status API (`subscriptionStatusIOS`)
When you need to know the exact lifecycle phase, call [`subscriptionStatusIOS`](../api/methods/core-methods.md#subscriptionstatusios). This maps to StoreKit 2’s `Product.SubscriptionInfo.Status` API and returns an array of status entries for the subscription group. Each `status.state` comes through as a string so you can forward unknown values to your analytics or logging when Apple adds new phases.
```ts
import {subscriptionStatusIOS} from 'react-native-iap';
const statuses = await subscriptionStatusIOS('your.yearly.subscription');
const latestState = statuses[0]?.state ?? 'unknown';
Phase reference
state value | Meaning |
---|---|
subscribed | Subscription is active and billing is up to date |
expired | Subscription is no longer active |
inGracePeriod | Auto-renewal failed but StoreKit granted a grace period before suspending access |
inBillingRetryPeriod | Auto-renewal failed and StoreKit is retrying the payment method |
revoked | Apple revoked the subscription (for example, due to customer support refunds) |
inIntroOfferPeriod | User is currently inside a paid introductory offer (e.g., pay upfront or pay-as-you-go) |
inTrialPeriod | User is currently in the free-trial window |
paused | Subscription manually paused by the user (where supported) |
Relationship with other APIs
- Use
getActiveSubscriptions
(or the helper shown above) to keep your UI in sync with entitlement data across both platforms, then enhance it withsubscriptionStatusIOS
when you need the StoreKit-specific phase strings. latestTransactionIOS
returns the fullPurchase
object tied to the most recent status entry—useful when storing transaction IDs on your backend.currentEntitlementIOS
shortcuts to the single entitlement for a SKU if you do not need the full array.
Server-side validation and trials
If you already maintain a server, Apple’s /verifyReceipt
endpoint exposes flags like is_trial_period
and is_in_intro_offer_period
for each transaction in the receipt (see Apple’s Latest Receipt Info documentation). React Native IAP’s validateReceipt
API does not merge iOS and Android fields into a single structure; it returns platform-specific payloads:
- iOS (
ReceiptValidationResultIOS
) – IncludesisValid
,receiptData
,jwsRepresentation
, and an optionallatestTransaction
converted to the sharedPurchase
shape. You must parse the Base64receiptData
on your server (or call/verifyReceipt
) to inspectis_trial_period
and similar flags. - Android (
ReceiptValidationResultAndroid
) – Mirrors Google Play’s server response with fields such asautoRenewing
,expiryTimeMillis
,purchaseDate
,productId
, andraw receipt
metadata (term
,termSku
, etc.).
Because the library simply forwards the underlying store data, any additional aggregation (for example, emitting a unified object that contains both Apple transaction fields and the raw Play Billing response) must be performed in your own backend.
We recommend the following layering:
- Use
subscriptionStatusIOS
for fast, on-device checks when UI needs to react immediately. - Periodically upload receipts (via
getReceiptDataIOS
) to your backend for authoritative validation and entitlement provisioning. - Recalculate client caches (
getAvailablePurchases
) after server reconciliation to ensure consistency across devices.
Putting everything together
A typical subscription screen in React Native IAP might:
- Call
initConnection
andfetchProducts
when mounted. - Use
useIAP
to observe purchase updates and update local state. - Fetch
getAvailablePurchases
on launch to restore entitlements. - Query
subscriptionStatusIOS
to display whether the user is inside a trial or grace period. - Sync receipts to your server to unlock cross-device access.
By combining these surfaces you can offer a reliable experience that embraces StoreKit 2’s richer metadata while preserving backwards compatibility with existing server flows.