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 checkTrialOrIntroductoryPriceEligibility with 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".
Prerequisites: The RevenueCat SDK must already be installed and configured with 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.
Treat unknown defensively. If the status is 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:

typescript
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:

typescript
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:

swift
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:

swift
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:

tsx
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;
}
Eligibility is per subscription group, not per product. Apple grants one introductory offer per subscription group. If a user already used the trial on your monthly plan, they are typically ineligible for the trial on the annual plan in the same group. Check each product id you display so the copy matches what the store will actually charge.

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.

Do not invent an Android eligibility method. There is no equivalent explicit per-user eligibility query on Google Play. If you call 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