How to Implement a Free Trial on iOS with RevenueCat
Introduction
Free trials are one of the most effective conversion tools for subscription apps. Apple calls them introductory offers and they come in three flavors:
- Free trial — User gets access for free for a set period, then is charged the regular price.
- Pay as you go — User pays a reduced price for a limited number of billing periods.
- Pay up front — User pays a one-time reduced amount for a set duration, then switches to the regular price.
This guide focuses on the free trial type, as it is the most commonly used and has
the highest conversion impact. The code patterns for detecting and displaying the other types are identical —
only the paymentMode value differs.
Step 1: Configure in App Store Connect
You must configure the free trial in App Store Connect before RevenueCat can surface it:
- Log in to App Store Connect and open your app.
- Navigate to Subscriptions in the sidebar.
- Select your subscription group, then click on the specific subscription product.
- Scroll to Introductory Offers and click the + button.
- Set the Start Date and End Date (or leave End Date blank to run indefinitely).
- Set the Price to Free for a free trial.
-
Choose a Duration:
- 3 days, 1 week, 2 weeks, 1 month, 2 months, 3 months, 6 months, or 1 year
- Set Eligibility to New Subscribers Only (the standard choice).
- Click Save.
Step 2: Check Trial Eligibility in RevenueCat
After fetching your offerings, inspect the introductoryDiscount property on each
StoreProduct. If it exists and its paymentMode is .freeTrial,
the user is eligible for a trial on that product.
// Fetch offerings and inspect trial eligibility per package
func fetchOfferingsAndCheckTrials() async {
do {
let offerings = try await Purchases.shared.offerings()
guard let current = offerings.current else { return }
for package in current.availablePackages {
let product = package.storeProduct
if let intro = product.introductoryDiscount {
switch intro.paymentMode {
case .freeTrial:
// e.g. "7-day free trial"
print("\(product.productIdentifier): Free trial for \(intro.subscriptionPeriod)")
case .payAsYouGo:
// Discounted price for N periods
print("\(product.productIdentifier): \(intro.localizedPriceString) for \(intro.numberOfPeriods) periods")
case .payUpFront:
// One upfront reduced payment
print("\(product.productIdentifier): \(intro.localizedPriceString) upfront")
default:
break
}
} else {
print("\(product.productIdentifier): No introductory offer available")
}
}
} catch {
print("Failed to fetch offerings: \(error)")
}
}
To check eligibility for a specific user (accounting for whether they've already used the trial), use RevenueCat's dedicated eligibility API:
// Check if a specific user is eligible for the intro offer
let productIds = ["com.yourapp.premium_monthly"]
let eligibility = await Purchases.shared.checkTrialOrIntroDiscountEligibility(productIdentifiers: productIds)
if eligibility["com.yourapp.premium_monthly"]?.status == .eligible {
// Show "Start your 7-day free trial" messaging
} else {
// Show regular pricing — user has already used the trial
}
Step 3: Display Trial in Your Paywall UI
Build a pricing card component that reads from the package's introductoryDiscount and
conditionally shows trial messaging. This pattern works with any RevenueCat offering configuration —
it adapts automatically to whatever trial duration you set in App Store Connect.
import SwiftUI
import RevenueCat
struct PackagePricingCard: View {
let package: Package
let onPurchase: (Package) -> Void
private var product: StoreProduct { package.storeProduct }
private var freeTrial: StoreProductDiscount? {
guard let intro = product.introductoryDiscount,
intro.paymentMode == .freeTrial else { return nil }
return intro
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Trial badge
if let trial = freeTrial {
HStack(spacing: 4) {
Text("FREE")
.font(.caption2)
.fontWeight(.black)
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.green)
.cornerRadius(4)
Text("\(trialPeriodLabel(trial)) free trial")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.green)
}
}
// Pricing
HStack(alignment: .firstTextBaseline, spacing: 2) {
Text(product.localizedPriceString)
.font(.title2)
.fontWeight(.bold)
if let period = product.subscriptionPeriod {
Text("/ \(periodLabel(period))")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
// CTA
Button(action: { onPurchase(package) }) {
Text(freeTrial != nil ? "Start Free Trial" : "Subscribe Now")
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.accentColor)
.cornerRadius(12)
}
// Legal disclaimer
if let trial = freeTrial {
Text("Free for \(trialPeriodLabel(trial)), then \(product.localizedPriceString)/\(periodLabel(product.subscriptionPeriod)). Cancel anytime.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(16)
}
private func trialPeriodLabel(_ discount: StoreProductDiscount) -> String {
let period = discount.subscriptionPeriod
switch period.unit {
case .day: return period.value == 7 ? "7-day" : "\(period.value)-day"
case .week: return "\(period.value)-week"
case .month: return "\(period.value)-month"
case .year: return "\(period.value)-year"
default: return "trial"
}
}
private func periodLabel(_ period: SubscriptionPeriod?) -> String {
guard let period = period else { return "period" }
switch period.unit {
case .day: return "day"
case .week: return "week"
case .month: return "month"
case .year: return "year"
default: return "period"
}
}
}
Step 4: Check Trial Status After Purchase
After the user taps "Start Free Trial" and the purchase succeeds, verify that the user is in an
active trial by checking the entitlement's periodType. Use this to show trial-specific
UI, such as "Trial ends in X days":
func purchasePackage(_ package: Package) async {
do {
let result = try await Purchases.shared.purchase(package: package)
let customerInfo = result.customerInfo
if let entitlement = customerInfo.entitlements["premium"] {
if entitlement.periodType == .trial {
// User is in the free trial period
if let trialEnd = entitlement.expirationDate {
let formatter = RelativeDateTimeFormatter()
let timeRemaining = formatter.localizedString(for: trialEnd, relativeTo: Date())
showSuccessMessage("Trial started! Ends \(timeRemaining).")
} else {
showSuccessMessage("Your free trial has started!")
}
} else if entitlement.isActive {
// User purchased directly (no trial)
showSuccessMessage("Welcome to Premium!")
}
}
} catch {
showErrorMessage("Purchase failed: \(error.localizedDescription)")
}
}
Trial Expiry and Conversion
When a trial ends, Apple either converts the user to a paid subscription (if they haven't cancelled)
or expires the entitlement. RevenueCat receives this update via server-to-server notifications and
fires a customerInfoUpdatedNotification, which your SubscriptionManager
will pick up automatically.
To proactively encourage conversion before the trial ends, check willRenew:
let customerInfo = try await Purchases.shared.getCustomerInfo()
if let premium = customerInfo.entitlements["premium"],
premium.periodType == .trial {
if premium.willRenew {
// Trial will convert — show positive reinforcement
print("Trial converts automatically in \(daysRemaining(until: premium.expirationDate)) days")
} else {
// User cancelled during trial — show win-back prompt
showWinBackOffer()
}
}
func daysRemaining(until date: Date?) -> Int {
guard let date = date else { return 0 }
return Calendar.current.dateComponents([.day], from: Date(), to: date).day ?? 0
}
You can also subscribe to RevenueCat webhooks on your server to receive TRIAL_STARTED,
TRIAL_CONVERTED, and TRIAL_CANCELLED events and trigger server-side
follow-up (emails, analytics, etc.).
App Store Review Guidelines for Trials
Related Guides
- Full iOS In-App Purchases Tutorial — End-to-end RevenueCat iOS integration
- Check Subscription Status on iOS — Gate features using CustomerInfo
- Restore Purchases on iOS — Implement the required restore button
- App Store Connect Setup — Configure subscriptions in App Store Connect
- Troubleshooting — Common RevenueCat issues and solutions