Check Free Trial & Intro Price Eligibility with RevenueCat
Overview
Apple lets each subscriber use an introductory offer (such as a free trial or a discounted intro
price) only once per subscription group. Showing a "Start free trial" button to a user who has
already used their trial is misleading: they will be charged the full price immediately. RevenueCat's
checkTrialOrIntroductoryPriceEligibility lets you ask, per product, whether the current
user is still eligible for that intro offer so you can show the right paywall copy.
The typical flow is:
- Call
checkTrialOrIntroductoryPriceEligibilitywith the product identifiers on your paywall. - Read the intro eligibility status returned for each product.
- Show "Start free trial" only when the status is
eligible; otherwise show "Subscribe".
Purchases.configure(...). See the Configure the SDK guide,
the React Native codelab, and the
iOS codelab for setup. You also need the product identifiers you want to
check, which you can get from your offerings and packages.
The Eligibility Status Values
Both the React Native and iOS SDKs expose the same four eligibility states. In
react-native-purchases they live on the INTRO_ELIGIBILITY_STATUS enum; on
iOS they are cases of IntroEligibilityStatus:
| React Native (INTRO_ELIGIBILITY_STATUS) | iOS (IntroEligibilityStatus) | Meaning |
|---|---|---|
INTRO_ELIGIBILITY_STATUS_ELIGIBLE |
.eligible |
The user can use the intro offer. Show free trial copy. |
INTRO_ELIGIBILITY_STATUS_INELIGIBLE |
.ineligible |
The user already used the intro offer. Show standard pricing. |
INTRO_ELIGIBILITY_STATUS_NO_INTRO_OFFER_EXISTS |
.noIntroOfferExists |
The product has no intro offer configured. Show standard pricing. |
INTRO_ELIGIBILITY_STATUS_UNKNOWN |
.unknown |
Eligibility could not be determined yet. Use neutral copy as a fallback. |
unknown (for example the
receipt is not yet available), do not assume the user is eligible. Fall back to neutral copy and
re-check once the SDK has the data it needs.
Check Eligibility (React Native)
Import the INTRO_ELIGIBILITY_STATUS enum, pass an array of product identifiers to
checkTrialOrIntroductoryPriceEligibility, and read the status for each
product. The result is a map keyed by product id:
import Purchases, { INTRO_ELIGIBILITY_STATUS } from 'react-native-purchases';
async function checkTrialEligibility() {
const eligibilities = await Purchases.checkTrialOrIntroductoryPriceEligibility(['my_product_id']);
const status = eligibilities['my_product_id'].status;
if (status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE) {
// Show free trial copy: the user can start a trial.
return 'eligible';
}
// INELIGIBLE, NO_INTRO_OFFER_EXISTS, or UNKNOWN: show standard pricing.
return 'standard';
}
You can pass several identifiers at once. The returned object has one entry per product id, each with
its own status, which is handy when your paywall lists monthly and annual options:
import Purchases, { INTRO_ELIGIBILITY_STATUS } from 'react-native-purchases';
const productIds = ['monthly_sub', 'annual_sub'];
const eligibilities = await Purchases.checkTrialOrIntroductoryPriceEligibility(productIds);
productIds.forEach((id) => {
const status = eligibilities[id].status;
const eligible = status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE;
console.log(id, eligible ? 'show free trial' : 'show standard price');
});
Check Eligibility (iOS)
On iOS the API mirrors React Native. Call
checkTrialOrIntroductoryPriceEligibility with an array of product identifiers and compare
the status against the IntroEligibilityStatus cases:
import RevenueCat
func checkTrialEligibility() async {
let eligibilities = await Purchases.shared.checkTrialOrIntroductoryPriceEligibility(["my_product_id"])
if eligibilities["my_product_id"]?.status == .eligible {
// Show free trial copy: the user can start a trial.
showFreeTrialCopy()
} else {
// .ineligible, .noIntroOfferExists, or .unknown: show standard pricing.
showStandardCopy()
}
}
You can also switch over every case explicitly when you want distinct copy for each state:
let eligibilities = await Purchases.shared.checkTrialOrIntroductoryPriceEligibility(["my_product_id"])
switch eligibilities["my_product_id"]?.status {
case .eligible:
ctaTitle = "Start 7-day free trial"
case .ineligible, .noIntroOfferExists:
ctaTitle = "Subscribe"
case .unknown, .none:
ctaTitle = "Continue"
@unknown default:
ctaTitle = "Continue"
}
Use It in Your Paywall
The point of the check is to tailor the call to action. When a user is eligible, lead with the trial. When they are not, show the price directly so the button does not promise something the store will not honor. Here is a React Native hook that resolves the right label for a product:
import { useEffect, useState } from 'react';
import Purchases, { INTRO_ELIGIBILITY_STATUS } from 'react-native-purchases';
export function useTrialCta(productId: string, price: string) {
const [label, setLabel] = useState('Continue');
useEffect(() => {
let cancelled = false;
Purchases.checkTrialOrIntroductoryPriceEligibility([productId]).then((eligibilities) => {
if (cancelled) return;
const status = eligibilities[productId].status;
if (status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE) {
setLabel('Start free trial');
} else {
setLabel(`Subscribe for ${price}`);
}
});
return () => {
cancelled = true;
};
}, [productId, price]);
return label;
}
Android Note
On Google Play, free trial and introductory offer eligibility is determined by Google Play at
purchase time. A user who has not yet used the offer is generally eligible, and Google enforces this
when the purchase is made. Because of that, the explicit
checkTrialOrIntroductoryPriceEligibility check is primarily relevant on iOS and the App
Store.
On Android, instead of calling the eligibility API, rely on the offer details that Google returns for the product (the subscription options and their pricing phases). Present the trial or intro phase that comes back with the offer, and let Google decide eligibility at checkout. For the exact shape of the offer and pricing data, see Get products and prices and the RevenueCat documentation.
checkTrialOrIntroductoryPriceEligibility on Android, do not treat the result as a strong
signal. Drive the Android paywall from the offer details Google provides.
FAQ
What are the intro_eligibility_status values?
In React Native the INTRO_ELIGIBILITY_STATUS enum has four values:
INTRO_ELIGIBILITY_STATUS_UNKNOWN, INTRO_ELIGIBILITY_STATUS_INELIGIBLE,
INTRO_ELIGIBILITY_STATUS_ELIGIBLE, and
INTRO_ELIGIBILITY_STATUS_NO_INTRO_OFFER_EXISTS. On iOS the
IntroEligibilityStatus enum exposes the same set: .unknown,
.ineligible, .eligible, and .noIntroOfferExists.
How do I check free trial eligibility with RevenueCat?
Call checkTrialOrIntroductoryPriceEligibility with an array of product identifiers. It
returns a result keyed by product id; read the status for each product and compare it
against the eligible value of the intro eligibility status enum to decide whether to show
free trial copy.
Does checkTrialOrIntroductoryPriceEligibility work on Android?
The explicit check is primarily for iOS and the App Store. On Google Play, eligibility is determined
by Google Play at purchase time, so on Android you should drive the paywall from the offer details
Google returns rather than from this API.
Why does the status come back as unknown?
unknown means RevenueCat could not yet determine eligibility, often because the receipt
or purchase history is not available, or because the check ran before configuration finished. Treat
it as a safe default, show neutral copy, and re-check once the data is available.
Related Guides
- Get Products & Prices: load offerings, packages, and pricing for your paywall
- Configure the RevenueCat SDK: apiKey and appUserID setup
- Get CustomerInfo & Refresh the Cache: read entitlements and refresh state
- iOS In-App Purchases Tutorial: full end-to-end RevenueCat iOS integration
- React Native In-App Purchases Tutorial: full end-to-end RevenueCat integration
- RevenueCat Docs: official reference