WWDC Quick Look 💓 By SwiftGGTeam
Implement proactive in-app purchase restore

Implement proactive in-app purchase restore

元の動画を見る

ハイライト

Apple は、StoreKit 2 およびオリジナルの StoreKit を通じてプロアクティブなアプリ内購入回復メカニズムを提供しています。これにより、ユーザーが「購入を復元」をクリックしたり、パーソナライズされたインターフェイス コンテンツを表示するためにパスワードを入力したりすることなく、アプリケーションの起動時にユーザーの購入ステータス (新規顧客、アクティブな購読者、非アクティブな購読者) が自動的に検出されます。

主要内容

従来のエクスペリエンスの問題点

ユーザーは App Store からアプリをダウンロードし、それを開いた後、購入、ログイン、購入の復元という 3 つのオプションに直面します。有料購読者にとって、特に新しいデバイスの場合、どれをクリックすればよいのかわかりません。

このプロセスにより摩擦が加わります。ユーザーは間違えて再度購入してしまう可能性や、購入したコンテンツが見つからずに迷ってしまう可能性があります。

アクティブリカバリーの核となる考え方

Apple のソリューションはシンプルです。アプリが起動すると、デバイス上にすでに存在する StoreKit データを使用して、ユーザーの取引履歴を積極的にチェックします。結果に基づいて、ユーザーは 3 つのカテゴリに分類され、それぞれ異なるインターフェイスが表示されます。

新規顧客: 購入記録はなく、デフォルトの製品プロモーション インターフェイスが表示されます。

アクティブな購読者: 有効なトランザクションでは、すべての機能が直接ロック解除され、購入ボタンが非表示になります。

非アクティブな定期購入者: 過去に購入したが期限切れまたはキャンセルされたため、リコール オファーが表示されます。

このプロセスは完全に自動であり、ユーザーによるアクションは必要ありません。

3 つの主要な顧客ステータス

03:28

アクティブ リカバリには、非消耗型、非更新型サブスクリプション、および自動更新型サブスクリプションの 3 つの購入タイプがあります。これらはすべてユーザーのトランザクション履歴に保存されます。

新規顧客: Apple ID を使用してサインインしましたが、現在または過去にアプリ内購入を行っていません。毎月/年間のサブスクリプショントライアルを表示する Ocean Journal アプリなど、デフォルトのマーチャンダイジング インターフェイスを表示します。

購入済みでアクティブ: 顧客は有効なトランザクションを持っており、アプリはアクセスを許可する必要があります。このインターフェイスには、購入ボタンがなく、ライブビーチカメラなどのロック解除されたコンテンツが直接表示されます。

非アクティブ: 過去に購入しましたが、有効期限が切れたか、キャンセルされました (払い戻し、ファミリー共有の取り消し)。そのようなユーザーにプロモーション オファーや割引コードを表示して思い出してもらいます。請求再試行期間中のユーザーは、支払い情報を更新するように求められます。

請求の再試行と猶予期間

05:27

サブスクリプションが自動更新に失敗した場合、Apple は最大 60 日間の請求再試行期間を開始します。 App Store Connect で請求猶予期間が有効になっている場合、ユーザーは再試行中にサービスを引き続き使用できます。アプリは、サービスを中断するのではなく、この間、ユーザーに支払い情報を更新するよう優しく促す必要があります。

詳細

StoreKit 2 は 3 つのステップを実装します

10:07

アプリの起動後、購入ボタンが表示されるまでに、次の 3 つの手順を完了する必要があります。

ステップ 1: トランザクションの更新を監視する

11:16

func listenForTransactions() -> Task<Void, Error> {
    return Task.detached {
        for await result in Transaction.updates {
            do {
                let transaction = try self.checkVerified(result)
                await self.updateCustomerProductStatus()
                await transaction.finish()
            } catch {
                print("Transaction failed verification")
            }
        }
    }
}

キーポイント:

  • Transaction.updates未処理のトランザクションまたはトランザクションの更新を返す
  • トランザクションの信頼性を最初に検証する必要があります (checkVerified)、コンテンツを配信します
  • 配達後に電話する必要がありますtransaction.finish()それ以外の場合、トランザクションは戻り続けます。
  • ファミリー共有、コード引き換え、自動更新などからトランザクションが発生する可能性があるため、アプリが起動したらすぐにリスニングを開始する必要があります。

ステップ 2: お客様の製品のステータスを確認する

12:27

func updateCustomerProductStatus() async {
    var purchasedCars: [Product] = []
    var purchasedSubscriptions: [Product] = []
    var purchasedNonRenewableSubscriptions: [Product] = []

    for await result in Transaction.currentEntitlements {
        do {
            let transaction = try checkVerified(result)

            switch transaction.productType {
            case .nonConsumable:
                if let car = cars.first(where: { $0.id == transaction.productID }) {
                    purchasedCars.append(car)
                }
            case .nonRenewable:
                if let nonRenewable = nonRenewables.first(where: { $0.id == transaction.productID }),
                   transaction.productID == "nonRenewing.standard" {
                    let currentDate = Date()
                    let expirationDate = Calendar(identifier: .gregorian).date(
                        byAdding: DateComponents(year: 1),
                        to: transaction.purchaseDate
                    )!
                    if currentDate < expirationDate {
                        purchasedNonRenewableSubscriptions.append(nonRenewable)
                    }
                }
            case .autoRenewable:
                if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) {
                    purchasedSubscriptions.append(subscription)
                }
            default:
                break
            }
        } catch {
            print("Verification failed")
        }
    }

    self.purchasedCars = purchasedCars
    self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions
    self.purchasedSubscriptions = purchasedSubscriptions

    subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state
}

キーポイント:

  • Transaction.currentEntitlementsユーザーがアクセスできるすべてのトランザクションを返します。
  • 更新不可のサブスクリプションには有効期限メカニズムが組み込まれていないため、手動で計算する必要があります (購入日プラス 1 年など)。
  • 自動更新可能なサブスクリプションパスsubscription?.status.first?.state更新ステータスを取得する
  • 更新ステータスには次のものが含まれます。.subscribed.expired.revoked.inBillingRetryPeriod.inGracePeriod

ステップ 3: ステータスに基づいてインターフェイスをパーソナライズする

13:45

if store.purchasedCars.isEmpty &&
   store.purchasedNonRenewableSubscriptions.isEmpty &&
   store.purchasedSubscriptions.isEmpty {
    // 新客户:展示默认推销界面
    VStack {
        Text("SK Demo App")
            .bold()
            .font(.system(size: 50))
        Text("Head over to the shop to get started!")
        NavigationLink { StoreView() } label: { Text("Shop") }
    }
} else {
    // 有购买记录:展示已购内容
    List {
        Section("My Cars") {
            ForEach(store.purchasedCars) { product in
                NavigationLink { ProductDetailView(product: product) } label: {
                    ListCellView(product: product, purchasingEnabled: false)
                }
            }
        }
    }
}

キーポイント:

  • アクティブなトランザクションがない場合に新しい顧客インターフェイスを表示します
  • アクティブなトランザクションがある場合に購入済みコンテンツのリストを表示し、購入機能を無効にします
  • 非アクティブな加入者に対してさまざまなプロンプトを表示します (有効期限、キャンセル、請求の再試行)

オリジナルの StoreKit 実装 (iOS 7 以降)

16:06

StoreKit 2 をサポートしていない古いバージョンの場合は、オリジナルの StoreKit+ を使用してくださいverifyReceipt終点:

public func getReceipt() {
    if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
       FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
        do {
            let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
            let receiptString = receiptData.base64EncodedString(options: [])
            // 发送到服务器验证
            sendToServer(receiptString)
        } catch {
            print("Couldn't read receipt data: \(error)")
        }
    }
}

キーポイント:

  • からBundle.main.appStoreReceiptURLアプリのレシートを取得する
  • レシートはサーバーに送信され、サーバーは Apple のサービスを呼び出します。verifyReceiptエンドポイントの検証
  • Sandbox と TestFlight では、レシートは購入または復元の完了後にのみ存在します。
  • レシートがない場合は新規のお客様として扱います。

サーバーステータスの同期

05:17

各トランザクションには固有のトランザクションがありますoriginalTransactionID、ユーザーの Apple ID とストア全体で持続します。この ID を使用して、サーバー上のユーザー アカウント (匿名またはユーザー作成) を関連付けることが推奨されます。

App Store サーバー通知バージョン 2 を使用すると、サーバーはトランザクション ステータスの変更 (払い戻し、キャンセル、更新、請求の再試行、有効期限など) をリアルタイムで学習できます。バージョン 2 は、28 の異なるイベント タイプとサブタイプをサポートします。

ベストプラクティスのまとめ

19:08

  • ユーザーが Apple ID を切り替えたり、問題をトラブルシューティングしたりするためのフォールバックとして、[購入を復元] ボタンを保持します。
  • CloudKit を使用して、購入ステータス データをユーザーの複数のデバイスに安全に同期します
  • 複数の製品またはサブスクリプション グループをサポートする場合、各製品の顧客ステータスを個別に判断します。
  • クロスプラットフォームでの購入(ウェブなど)が顧客ステータスに与える影響を考慮する
  • Sandbox、TestFlight、Xcode StoreKit テストによる包括的なテスト

重要ポイント

  • ゼロ フリクション サブスクリプション エクスペリエンス: フィットネス アプリや学習アプリは、起動時にユーザーがアクティブなサブスクリプションを持っているかどうかを自動的に検出します。すべてのコースを直接ロック解除するものもあれば、トライアルオファーを表示するものもあります。入口API:Transaction.currentEntitlements + Product.SubscriptionInfo.RenewalState

  • 失われたユーザーのリコール システム: 音楽またはビデオ アプリケーションは、ユーザーのサブスクリプションの有効期限が切れたことを認識し、起動時に独占的なリコール オファー (初月の半額など) を直接表示します。エントリーAPI:チェックsubscriptionGroupStatus == .expired、そして電話しますProduct.subscription?.subscriptionOfferプロモーション特典を表示します。

  • ファミリー共有の自動適応: 子供向け教育アプリケーションは、ファミリー共有を通じてアクセス権を獲得した新しいデバイスを検出し、親が再度購入する必要なく、すべてのコンテンツのロックを自動的に解除します。入口API:Transaction.updatesホームシェアリングの承認の変更を監視し、checkVerified取引確認後に自動配信されます。

  • 請求に関する問題に関する優しいリマインダー: SaaS ツール アプリケーションは、ユーザーが請求の再試行期間中であることを認識し、インターフェイスの上部に非侵入的なバナーを表示して、サービスの利用を維持しながら支払い情報を更新するよう促します。入口API:subscriptionGroupStatus == .inBillingRetryPeriod、ユーザーを次のように誘導しますhttps://apps.apple.com/account/billing

  • クロスデバイスステータス同期: iPhone でメモまたはドキュメント アプリの Pro バージョンを購入すると、iPad は初めて電源を入れたときに自動的に認識され、高度な機能のロックが解除されます。ポータル API: StoreKit 2 トランザクションは、同じ Apple ID を持つすべてのデバイスに自動的に同期されます。Transaction.currentEntitlementsどのデバイスでも一貫した結果を返します。

関連セッション

コメント

GitHub Issues · utterances