Free Trials

Fork your paywall on trial eligibility the store reports, pull trial terms from product variables, and remind users before a trial ends.

The store decides who gets a trial — not you, and not the user's claim. Someone who used their trial two years ago and reinstalled is ineligible, and only the store knows. useTrialEligibility() is that signal, and it splits your paywall into two versions that must both read as intentional.

Read eligibility

import { useTrialEligibility } from "superwall/hooks";

const { eligible } = useTrialEligibility();   // boolean | undefined

eligible is undefined until the SDK reports, so gate trial-only UI on eligible === true — never on "not false."

Two paywalls in one

Fork every user-facing string, including the CTA. A returning customer sees the ineligible copy, and it cannot read like a mistake:

const { eligible } = useTrialEligibility();
const days = annual?.variables.trialPeriodDays;

<h1>{eligible ? "Start free" : "Go Pro"}</h1>
<p>
  {eligible
    ? days
      ? `${days} days free, then ${annual?.variables.price ?? "the annual price"}.`
      : "Your trial is on the house."
    : "You have used your trial. Subscribe to keep going."}
</p>
<button>{eligible ? "Start free trial" : (annual?.variables.price ?? "Subscribe")}</button>

Two details in that snippet are deliberate:

  • The fallbacks nest. Eligible-but-days-unknown gets its own sentence — the data may not have arrived yet, and "undefined days free" is never acceptable copy.
  • The ineligible side is written, not defaulted. "You have used your trial" tells a returning customer the paywall knows who they are.

Trial terms come from the product

Trial length, price, and end date are variables on the product — trialPeriodDays, trialPeriodPrice, trialPeriodEndDate, trialPeriodText — never values in your files. They follow the same rules as every product read: guard them, and Number() before arithmetic. See Products.

Test both sides

  • The studio has a trial-eligibility toggle — flip it and check every string on both sides. See The studio.
  • Config can force either side while you're building:
introductoryOfferEligibility: "alwaysEligible" | "alwaysIneligible"   // default "automatic"

Leave it on "automatic" for production — that lets the store decide.

The trial-eligibility example is the reference: every string forks, and both states read as designed.

Trial reminder notifications

Declare a local notification in config.ts and the SDK schedules it when a trial actually starts — the paywall doesn't need to be open when it fires:

notifications: {
  trialReminder: {
    title: "Your trial ends tomorrow",
    body: "Keep Pro, or cancel in Settings — no charge either way.",
    beforeTrialEndDays: 1,        // default 1
  },
},

title, subtitle, and body accept message keys (resolved through t() — see Localization) or literal copy.

For full control, pass a function instead. It receives { trialEndDate, product, t, locale } and returns { title, body, delayMs } — or null to skip the notification entirely:

notifications: {
  trialReminder: ({ trialEndDate, t }) =>
    trialEndDate
      ? { title: t("reminder.title"), body: t("reminder.body"), delayMs: 0 }
      : null,
},

The trial-reminders example shows both forms.

Users warned before the charge cancel calmly instead of charging back — and the ones who stay chose to stay.

How is this guide?

On this page