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.

One trial per subscription group per Apple ID. Apple allows each user to redeem an introductory offer only once per subscription group. If they subscribed and cancelled previously, they are ineligible for a new trial on any product in the same group.

Step 1: Configure in App Store Connect

You must configure the free trial in App Store Connect before RevenueCat can surface it:

  1. Log in to App Store Connect and open your app.
  2. Navigate to Subscriptions in the sidebar.
  3. Select your subscription group, then click on the specific subscription product.
  4. Scroll to Introductory Offers and click the + button.
  5. Set the Start Date and End Date (or leave End Date blank to run indefinitely).
  6. Set the Price to Free for a free trial.
  7. Choose a Duration:
    • 3 days, 1 week, 2 weeks, 1 month, 2 months, 3 months, 6 months, or 1 year
  8. Set Eligibility to New Subscribers Only (the standard choice).
  9. Click Save.
Propagation delay: Changes to introductory offers in App Store Connect can take up to one hour to propagate to the Sandbox environment. If your trial isn't appearing in testing, wait a few minutes and re-fetch offerings.

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.

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

swift
// 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.

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

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

swift
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

Clearly disclose the trial terms. Apple requires that you prominently display the trial duration, the price charged after the trial, and how to cancel. Bury this information and your app may be rejected. Include it in both your paywall UI and your app's metadata.
Show the post-trial price prominently. The subscription price after the trial must be visible before the user confirms the purchase. Do not display only the trial duration without mentioning what the user will be charged afterward.
Provide clear cancellation instructions. Direct users to Settings > [Your Name] > Subscriptions to manage or cancel. Including this path in your app (e.g., in Settings or the paywall) is both a guideline requirement and a user trust signal.

Related Guides