概要

0:01:00

購入の復元は、サブスクリプションやアプリ内課金を販売するすべての iOS アプリにとって基本的な機能です。 Apple の App Store Review Guidelines では、アプリ内課金を提供するすべてのアプリに、ユーザーが購入を復元できる手段を用意することが義務づけられています。 これがないと、審査でアプリがリジェクトされることがあります。

ユーザーが復元を必要とする代表的な場面は次のとおりです。

  • アプリを削除したあとに再インストールしたとき
  • 新しい iPhone や iPad をセットアップするとき
  • 同じ Apple ID でサインインした 2 台目の端末にアプリをダウンロードしたとき
  • ローカルの購入状態が失われたアプリのアップデート後
App Store の要件: Apple のガイドライン(セクション 3.1.1)では、アプリ内課金を含むアプリには、ユーザーが見つけやすい「購入を復元」の仕組みを備えることが義務づけられています。これがないアプリはリジェクトされることがあります。

前提条件

  • RevenueCat iOS SDK がインストールされている(Swift Package Manager または CocoaPods 経由)
  • App のエントリーポイントで API キーを使って SDK を構成済み
  • RevenueCat のダッシュボードにエンタイトルメントが少なくとも 1 つ構成されている

restorePurchases() API

0:01:30

RevenueCat の restorePurchases() メソッドは、次の処理を順に実行します。

  1. 端末から現在の App Store レシートを取得する
  2. Apple での検証のため、レシートを RevenueCat のサーバーに送信する
  3. 有効なサブスクリプションや購入を現在の顧客レコードに同期する
  4. すべての有効なエンタイトルメントを含む、更新された CustomerInfo オブジェクトを返す

Swift の最新の async/await 構文を使った基本的な使い方は次のとおりです。

swift
// Basic restore using async/await
do {
    let customerInfo = try await Purchases.shared.restorePurchases()
    if customerInfo.entitlements["premium"]?.isActive == true {
        // User has premium access
        print("Purchases restored successfully")
    } else {
        print("No active purchases found to restore")
    }
} catch {
    print("Error restoring purchases: \(error.localizedDescription)")
}

返される CustomerInfo オブジェクトは、復元後のユーザーの現在のサブスクリプション状態を反映します。 エンタイトルメントは、プロダクト ID ではなく、必ず識別子(たとえば "premium")で確認してください。 こうすると、機能のゲート処理を特定のプロダクトから切り離しておけます。


SwiftUI での実装

0:02:30

次に示すのは、単体で動作する本番向けの RestorePurchasesButton という SwiftUI ビューです。 ローディング状態を扱い、結果をアラートで表示し、二重タップを防ぐために非同期処理の間はボタン自身を無効化します。

swift
import SwiftUI
import RevenueCat

struct RestorePurchasesButton: View {
    @State private var isRestoring = false
    @State private var showAlert = false
    @State private var alertMessage = ""

    var body: some View {
        Button(action: restorePurchases) {
            if isRestoring {
                ProgressView()
                    .progressViewStyle(.circular)
            } else {
                Text("Restore Purchases")
            }
        }
        .disabled(isRestoring)
        .alert("Restore Purchases", isPresented: $showAlert) {
            Button("OK") { }
        } message: {
            Text(alertMessage)
        }
    }

    private func restorePurchases() {
        isRestoring = true
        Task {
            do {
                let customerInfo = try await Purchases.shared.restorePurchases()
                if customerInfo.entitlements.active.isEmpty {
                    alertMessage = "No active purchases found. If you believe this is an error, please contact support."
                } else {
                    alertMessage = "Your purchases have been successfully restored!"
                }
            } catch {
                alertMessage = "Failed to restore purchases: \(error.localizedDescription)"
            }
            isRestoring = false
            showAlert = true
        }
    }
}

このボタンをペイウォールや設定画面に置きます。

swift
struct PaywallView: View {
    var body: some View {
        VStack(spacing: 16) {
            // ... your pricing options ...

            RestorePurchasesButton()
                .font(.footnote)
                .foregroundColor(.secondary)
        }
    }
}

UIKit での実装

0:02:00

まだ SwiftUI に移行していないアプリ向けに、クロージャベースのコールバック API を使った UIKit の実装は次のとおりです。

swift
@IBAction func restorePurchasesTapped(_ sender: UIButton) {
    sender.isEnabled = false

    Purchases.shared.restorePurchases { customerInfo, error in
        sender.isEnabled = true

        if let error = error {
            self.showAlert(title: "Error", message: error.localizedDescription)
            return
        }

        if customerInfo?.entitlements["premium"]?.isActive == true {
            self.showAlert(title: "Success", message: "Purchases restored successfully!")
            self.updateUIForPremium()
        } else {
            self.showAlert(
                title: "Not Found",
                message: "No active purchases found. Contact support if you believe this is an error."
            )
        }
    }
}

private func showAlert(title: String, message: String) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    present(alert, animated: true)
}

組み込みの復元ボタン

0:01:30

RevenueCat の ペイウォール (RevenueCatUIPaywallView または PaywallViewController) を使っている場合、「購入を復元」ボタンは自動的に含まれます。 自分で追加する必要はありません。

この組み込みボタンは、RevenueCat のすべてのペイウォールテンプレートの下部に表示され、上記の手動実装とまったく同じように動作します。 完全にカスタムなペイウォール UI を構築している場合は、上記の RestorePurchasesButton コンポーネントを使ってください。

swift
import RevenueCatUI

// RevenueCat's PaywallView includes a restore button automatically
struct AppPaywall: View {
    var body: some View {
        // The restore purchases button is built in, no extra code needed
        PaywallView()
    }
}

重要な注意点

0:01:30
App Store の要件: Apple は、復元ボタンをユーザーが簡単にアクセスできる場所に置くことを求めています。ペイウォール画面や、アプリの設定 / アカウントのセクションに配置してください。何度もタップしないと届かない場所に隠さないでください。
匿名ユーザー: アプリが RevenueCat の匿名ユーザー ID を使っていて、有効な購入がない状態でユーザーが restorePurchases() を呼び出すと、RevenueCat は新しい匿名 ID を作成することがあります。 アプリ独自の認証システムがある場合は、復元を呼び出す前に Purchases.shared.logIn(appUserID:) でユーザーをログインさせ、購入が正しいアカウントに ひも付くようにしてください。
起動時に自動復元しない: restorePurchases() は、ユーザーが明示的に復元ボタンをタップしたときにのみ呼び出してください。 アプリ起動のたびに自動で呼び出すのは不要で、App Store へのネットワークリクエストが発生し、 アプリの起動を遅くする可能性があります。

関連ガイド