概要
0:01:30無料トライアルは、サブスクリプションアプリでもっとも効果的なコンバージョン施策のひとつです。Apple はこれを 導入オファー(introductory offer)と呼び、次の 3 種類があります。
- 無料トライアル: 一定期間は無料で利用でき、その後は通常価格が課金されます。
- Pay as you go: 一定回数の請求期間のあいだ、割引価格で支払います。
- Pay up front: 一定期間分の割引額を一括で支払い、その後は通常価格に切り替わります。
このガイドでは、もっともよく使われ、コンバージョンへの影響が大きい無料トライアルを中心に扱います。
他の種類を検出・表示するコードのパターンも同じで、異なるのは paymentMode の値だけです。
ステップ 1: App Store Connect で設定する
0:02:30RevenueCat がトライアルを表示できるようにするには、まず App Store Connect で無料トライアルを設定します。
- App Store Connect にログインし、対象のアプリを開きます。
- サイドバーの Subscriptions へ移動します。
- サブスクリプショングループを選び、対象のサブスクリプション商品をクリックします。
- Introductory Offers までスクロールし、+ ボタンをクリックします。
- Start Date と End Date を設定します(無期限で運用するなら End Date は空欄のままにします)。
- 無料トライアルにするため、Price を Free に設定します。
-
Duration を選びます。
- 3 日、1 週間、2 週間、1 か月、2 か月、3 か月、6 か月、1 年
- Eligibility を New Subscribers Only に設定します(標準的な選択肢です)。
- Save をクリックします。
ステップ 2: トライアルの対象かを確認する
0:02:30
Offering を取得したら、各 StoreProduct の introductoryDiscount プロパティを調べます。
これが存在し、その paymentMode が .freeTrial であれば、そのユーザーはその商品の
トライアルの対象です。
// 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)")
}
}
特定のユーザーが対象かどうかを(すでにトライアルを利用済みかどうかも踏まえて)確認するには、RevenueCat 専用の 対象確認 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
}
ステップ 3: ペイウォール UI にトライアルを表示する
0:03:00
パッケージの introductoryDiscount を読み取り、トライアルの案内を条件付きで表示する価格カードの
コンポーネントを作ります。このパターンはどの RevenueCat の Offering 構成でも動作し、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"
}
}
}
ステップ 4: 購入後にトライアルを検出する
0:02:00
ユーザーが「Start Free Trial」をタップして購入が成功したら、エンタイトルメントの periodType を
確認して、そのユーザーが有効なトライアル中かを検証します。これを使って、「トライアルはあと X 日で終了」の
ような、トライアル専用の UI を表示できます。
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)")
}
}
トライアルの期限切れと移行
0:02:00
トライアルが終わると、Apple はユーザーを有料サブスクリプションに移行させる(解約していない場合)か、
エンタイトルメントを失効させます。RevenueCat はこの更新をサーバー間通知(server-to-server notification)で
受け取り、customerInfoUpdatedNotification を発火します。これは SubscriptionManager
が自動的に受け取ります。
トライアル終了前に移行を積極的に促すには、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
}
サーバー側で RevenueCat の Webhook を購読すれば、TRIAL_STARTED、
TRIAL_CONVERTED、TRIAL_CANCELLED のイベントを受け取り、サーバー側で
フォローアップ(メール送信、分析など)を実行することもできます。
App Store ガイドライン
0:01:30関連ガイド
- iOS アプリ内課金 完全チュートリアル: RevenueCat の iOS 連携をエンドツーエンドで
- iOS でサブスクリプション状態を確認する: CustomerInfo を使った機能の制御
- iOS で購入を復元する: 必須の復元ボタンを実装する
- App Store Connect のセットアップ: App Store Connect でサブスクリプションを設定する
- トラブルシューティング: RevenueCat のよくある問題と解決策