WWDC Quick Look 💓 By SwiftGGTeam
What's new in StoreKit testing

What's new in StoreKit testing

元の動画を見る

ハイライト

Xcode 14 では、StoreKit テストで App Store Connect 同期設定、SwiftUI プレビュー、返金、クーポン コード、値上げ、請求の再試行、サンドボックスの請求失敗シミュレーションをカバーできるようになります。


主要内容

Food Truck はすでに StoreKit に組み込まれています。年間販売履歴は 1 回限りのアプリ内購入で、Social Feed+ はサブスクリプションです。開発者にとっての本当の問題は、購入ボタン自体ではなく、テストのライフサイクルです。製品メタデータは、App Store Connect、Xcode、ローカル単体テスト、およびサンドボックス間で一貫している必要があります。サブスクリプションでは、返金、割引コード、価格の値上げ、請求の失敗などの境界状態も発生します。

Xcode 14 では、まず構成の問題が解決されます。 StoreKit 構成ファイルは、App Store Connect からの製品とサブスクリプションのメタデータを同期します。同期されたファイルは読み取り専用です。変更する必要がある場合は、App Store Connect に戻り、Xcode の同期ボタンを押します。一時的な実験が必要な場合は、同期されたファイルをローカルの編集可能なファイルに変換できますが、変換は一方向の操作です。

次はデバッグ体験です。 StoreKit 構成ファイル内の製品は SwiftUI プレビューに直接入ることができ、ストア インターフェイスは実際のタイトルや説明を確認するためにデバイスにアクセスする必要はありません。 Transaction Manager には、Transaction Inspector とフィルタリング機能が追加されています。サブスクリプションの有効期限、その後の更新、サブスクリプション グループ、オファーなどを表示でき、製品 ID または購入日によってトランザクション リストを絞り込むこともできます。

このセッションの後半はサブスクリプション境界状態です。払い戻しが行われますTransaction.updates失効情報を含む新しいトランザクションを発行します。割引コードは現在の取引と次回の更新の両方に影響します。価格の値上げはトランザクションマネージャーを使用して行うことができます。SKTestSessionトリガー;課金の再試行と猶予期間は、Xcode、単体テスト、サンドボックスでシミュレートできます。目標は明確です。公開する前にサブスクリプションの失敗パスを完了することです。


詳細

App Store Connect 製品を同期し、SwiftUI プレビューで使用する

(01:30) Xcode 14 は、App Store Connect で設定されたアプリ内購入製品を StoreKit 設定ファイルと同期できます。こうすることで、同じ製品とサブスクリプションのメタデータを Xcode ローカル テスト、単体テスト、サンドボックス、および最終的なゴーライブ構成に使用できます。

(06:58) 同期された StoreKit 構成も SwiftUI プレビューに入ります。セッションでは、例として Social Feed+ の製品説明を使用します。App Store Connect でタイトルまたは説明を変更し、Xcode と同期した後、プレビュー インターフェイスで実際の製品データがすぐに使用されます。

VStack(alignment: .leading) {
    Text(subscription.displayName)
        .font(.headline.weight(.semibold))
    Text(subscription.description)
}

キーポイント:

  • VStack(alignment: .leading)サブスクリプション名と説明を左揃えで縦方向に表示します。 -Text(subscription.displayName)StoreKit 製品のローカライズされた表示名を使用します。データは構成ファイルから取得されます。 -.font(.headline.weight(.semibold))ストアのインターフェースで製品名をより目立つようにします。 -Text(subscription.description)製品のローカライズされた説明をお読みください。デモでは、この説明は App Store Connect と同期した後の StoreKit 構成から取得されます。

このコードは非常に短く、データ ソースに焦点を当てています。 StoreKit がプレビューにアクセスするように設定されると、ストア UI のレイアウト、コピーの長さ、ローカライズ効果を Canvas で直接確認できます。

返金リクエストと取引取り消し情報を使用してアフターセールス パスをテストする

(11:18) 払い戻し入り口refundRequestSheetSwiftUI ビューを待ってください。 Xcode 環境をテストする場合、ユーザーが選択した返金理由は StoreKit API の返金理由に対応します。RevocationReason。 Xcode または Sandbox では、返金はすぐに有効になります。

struct RefundView: View {
    @State private var selectedTransactionID: UInt64?
    @State private var refundSheetIsPresented = false
    @Environment(\.dismiss) private var dismiss
    var body: some View {
        Button {
            refundSheetIsPresented = true
        } label: {
            Text("Request a refund")
                .bold()
                .padding(.vertical, 5)
                .frame(maxWidth: .infinity)
        }
        .buttonStyle(.borderedProminent)
        .padding([.horizontal, .bottom])
        .disabled(selectedTransactionID == nil)
        .refundRequestSheet(
            for: selectedTransactionID ?? 0,
            isPresented: $refundSheetIsPresented
        ) { result in
            if case .success(.success) = result {
                dismiss()
            }
        }
    }
}

キーポイント:

  • selectedTransactionID返金するトランザクションの ID を保存します。トランザクションが選択されていない場合、ボタンは使用できません。 -refundSheetIsPresentedシステム返金フォームを表示するかどうかを制御します。 -Buttonクリックされた後refundSheetIsPresentedに設定true、返金フォームをトリガーします。 -.disabled(selectedTransactionID == nil)無効なトランザクションの受信を防止します。 -.refundRequestSheet(for:isPresented:)トランザクション ID とインプレッション ステータスを StoreKit に渡します。
  • コールバックでの一致.success(.success)後で電話するdismiss()、返金リクエストが正常に送信されたことを示します。

(12:33) 返金完了後、Transaction.updates更新情報が送信されます。アプリは読む必要がありますrevocationDateそしてrevocationReason、その後、対応する権利と利益を取り消します。

for await update in Transaction.updates {
    let transaction = try update.payloadValue
  
    if let revocationDate = transaction.revocationDate,
       let revocationReason = transaction.revocationReason {
        print("\(transaction.productID) revoked on \(revocationDate)")
       
        switch revocationReason {
        case .developerIssue: <#Handle developer issue#>
        case .other: <#Handle other issue#>
        default: <#Handle unknown reason#>
        }
        
        <#Revoke access to the product#>
    }
    <#...#>
}

キーポイント:

  • for await update in Transaction.updatesトランザクションの更新を継続的に監視します。 -try update.payloadValue検証されたトランザクション ペイロードを取得します。 -revocationDate存在する場合、トランザクションが取り消されたことを示します。 -revocationReason開発者の問題、その他の原因、不明な原因を区別します。
  • 理由を処理した後、アプリは返金を避けるために製品のアクセス権を取り消し、権利と利益を保持する必要があります。

割引コードの現在のステータスとその後の更新ステータスをテストします。

(14:21) 割引コードをテストするには、まずオファー コードをローカル StoreKit 構成ファイルに追加し、それからアプリで使用します。offerCodeRedeemSheet引き換えフォームを表示します。 Xcode 環境では、プロファイル内のすべてのオファーがリストされるため、App Store Connect によって生成された実際のコードを入力する必要はありません。

struct SubscriptionPurchaseView: View {
    @State private var redeemSheetIsPresented = false
        
    var body: some View {
        Button("Redeem an offer") {
            redeemSheetIsPresented = true
        }
        .buttonStyle(.borderless)
        .frame(maxWidth: .infinity)
        .padding(.vertical)
        .offerCodeRedeemSheet(isPresented: $redeemSheetIsPresented)
    }

}

キーポイント:

  • redeemSheetIsPresented引き換えフォームの表示状態を保存します。 -Button("Redeem an offer")割引コード引き換えポータルを積極的に開くのはユーザーです。
  • ボタンをクリックした後、バインド値を に設定します。true
  • .offerCodeRedeemSheet(isPresented:)StoreKit にシステム引き換えフォームを表示させます。

(16:23) 割引コードの引き換えが完了すると、Transaction.updatesそしてProduct.SubscriptionInfo.Status.updates新しい価値観が生まれます。現在のトランザクションを表示するtransaction.offerType、次回サブスクリプションを更新するときにお会いしましょうrenewalInfo.offerTypeそしてofferID

for await status in Product.SubscriptionInfo.Status.updates {
    let transaction = try status.transaction.payloadValue
    let renewalInfo = try status.renewalInfo.payloadValue
    
    if let currentOfferType = transaction.offerType {
        switch currentType {
        case .introductory: <#Handle introductory offer#>
        case .promotional:  <#Handle promotional offer#>
        case .code:         <#Handle offer for codes#>
        default:            <#Handle unknown offer type#>
        }
        self.hasCurrentOffer = true
    }

    if let nextOfferType = renewalInfo.offerType {
        switch currentType {
        case .introductory: <#Handle introductory offer#>
        case .promotional: <#Handle promotional offer#>
        case .code:
            print("Customer has \(renewalInfo.offerID) queued")
            <#Handle offer for codes#>
        default: <#Handle unknown offer type#>
        }
        self.hasQueuedOffer = true
    }
}

キーポイント:

  • Product.SubscriptionInfo.Status.updatesサブスクリプションステータスの変化を監視するために使用されます。 -status.transaction.payloadValue現在のサブスクリプション トランザクションを示します。 -status.renewalInfo.payloadValueリニューアル情報を示します。 -transaction.offerType現在のトランザクションに使用されるオファーのタイプを反映します。 -renewalInfo.offerType次回の更新で使用されるオファーのタイプを反映します。 -renewalInfo.offerID設定されたオファー参照名は、コード タイプ オファーで取得できます。 -hasCurrentOfferそしてhasQueuedOffer現在のオファーと後続のオファーをキューに個別に記録します。

価格引き上げのプロンプトを制御し、単体テストを作成する

(18:45) 価格引き上げのプロンプトは自動的に表示されますが、アプリは StoreKit Messages API を通じて表示タイミングを制御できます。フード トラックの例では、機密ビューが表示されたときにメッセージを一時的に保存し、ユーザーが編集プロセスを終了した後にメッセージを表示します。

private var pendingMessages: [Message] = []

private func updatesLoop() {
    for await message in Message.messages {
      if <#Check if sensitive view is presented#>,
         let display: DisplayMessageAction = <#Get display message action#> {
           try? display(message)
      }
      else {
        pendingMessages.append(message)
      }
    }
}

キーポイント:

  • pendingMessages一時的に表示に適さない StoreKit メッセージを保存します。 -Message.messages価格上昇アラートが到着する非同期メッセージ シーケンスです。
  • 条件分岐は、現在の状況がシステム メッセージをポップアップするのに適しているかどうかを確認するために使用されます。 -DisplayMessageAction実際にメッセージを提示する責任を負います。 ・展示に適さない場合は、messageキューに入れて後で処理します。

(20:53) ユーザーが値上げに同意または拒否すると、サブスクリプション ステータスは終了します。Product.SubscriptionInfo.Status.updates更新します。アプリでも視聴できるpriceIncreaseStatusそしてexpirationReason

for await status in Product.SubscriptionInfo.Status.updates {
    let renewalInfo = try status.renewalInfo.payloadValue

    if renewalInfo.priceIncreaseStatus == .agreed {
        print("Customer consented to price increase")
        <#Handle consented to price increase#>
    }
    if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
        print("Customer did not consent to price increase")
        <#Handle expired due to not consenting to price increase#>
    }

    <#...#>

}

キーポイント:

  • renewalInfo.priceIncreaseStatus == .agreedユーザーが値上げに同意したことを示します。 -renewalInfo.expirationReason == .didNotConsentToPriceIncrease価格値上げに同意できなかったため、サブスクリプションが期限切れになったことを示します。
  • これら 2 つの判断は、サブスクリプションを継続するか、サブスクリプションを失うかという、値上げプロセスの主要な結果をカバーします。

(21:19) 単体テストが使用可能SKTestSession価格上昇プロセスを直接トリガーし、システム ポップアップを無効にします。

let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>")
session.disableDialogs = true

<#Purchase a subscription#>

var transaction: SKTestTransaction! = session.allTransactions().first
session.requestPriceIncreaseConsentForTransaction(identifier: transaction.identifier)

transaction = session.allTransactions().first
XCTAssertTrue(transaction.isPendingPriceIncreaseConsent)

<#Assert app updates for pending price increase#>

session.consentToPriceIncreaseForTransaction(identifier: transaction.identifier)

// OR

session.declinePriceIncreaseForTransaction(identifier: transaction.identifier)
session.expireSubscription(productIdentifier: "<#Product ID#>")

<#Assert app updates for finished price increase#>

キーポイント:

  • SKTestSession(configurationFileNamed:)StoreKit 構成ファイルを使用してテスト セッションを作成します。 -session.disableDialogs = trueテストがシステム UI によってブロックされないようにします。 -session.requestPriceIncreaseConsentForTransactionトランザクションを値上げ保留ステータスにプッシュします。 -XCTAssertTrue(transaction.isPendingPriceIncreaseConsent)検証ステータスは保留中になりました。 -consentToPriceIncreaseForTransactionシミュレートされたユーザーは同意します。 -declinePriceIncreaseForTransaction追加expireSubscriptionシミュレートされたユーザー拒否後のサブスクリプションの有効期限。

請求の再試行、猶予期間、サンドボックス請求の失敗を上書きする

(24:57) サブスクリプションの更新が失敗した後、アプリは最初に猶​​予期間に入り、その後再試行するために課金に入ることがあります。 StoreKit は、状態が変化すると更新を発行します。 Food Truck は猶予期間中も引き続き Social Feed+ をリリースし、請求が再試行されるときにユーザーに支払い方法を更新するよう案内します。

for await status in Product.SubscriptionInfo.Status.updates {
    let renewalInfo = try status.renewalInfo.payloadValue

    if let gracePeriodExpirationDate = renewalInfo.gracePeriodExpirationDate,
       gracePeriodExpirationDate < .now {
        print("In grace period until \(gracePeriodExpirationDate)")
        <#Allow access to subscription#>
    }
    else if renewalInfo.isInBillingRetry {
        <#Handle billing retry#>
    }

    <#...#>

}

キーポイント:

  • renewalInfo.gracePeriodExpirationDate猶予期間の終了時刻を示します。
  • 猶予期間がある場合、セッションはサブスクリプション コンテンツへの継続的なアクセスを明示的に要求します。 -renewalInfo.isInBillingRetry請求のリトライステータスを識別するために使用されます。
  • 請求が再試行されると、アプリはユーザーに支払いの問題を解決するよう求めるメッセージを表示する必要があります。

(25:27) インターフェイス層は直接表示できますstatus.stateをクリックして、App Store の請求ページを開きます。

struct SubscriptionStatusView: View {
    let currentSubscription: Product
    let status: Product.SubscriptionInfo.Status
    @Environment(\.openURL) var openURL
    var body: some View {
        Section("Your Subscription") {
            <#...#>
            if status.state == .inBillingRetryPeriod || status.state == .inGracePeriod {
                VStack {
                    Text("""
                    There was a problem renewing your subscription. Open the App Store to
                    update your payment information.
                    """)
                    Button("Open the App Store") {
                        openURL(URL(string: "https://apps.apple.com/account/billing")!)
                    }
                }
            }
        }
    }
}

キーポイント:

  • status.stateサブスクリプション ステータスを UI で直接使用できる列挙値に一般化します。 -.inBillingRetryPeriodそして.inGracePeriodすべては支払いに関する問題のプロンプトを表示する必要があります。 -openURLApp Store の請求ディープリンクを開きます。 -https://apps.apple.com/account/billingセッション例での支払い情報入力です。

(25:50) 単体テストでは、SKTestSession使用する前に猶予期間と請求の再試行を有効にすることができます。resolveIssueForTransactionApp Store サブスクリプションの復元をシミュレートします。

let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>")
session.billingGracePeriodIsEnabled = true
session.shouldEnterBillingRetryOnRenewal = true

<#Purchase a subscription#>

wait(for: [<#XCTExpectation#>], timeout: 60)

let transaction: SKTestTransaction! = session.allTransactions().first
XCTAssertTrue(transaction.hasPurchaseIssue)

<#Assert app still allows access to subscription due to grace period#>

wait(for: [<#XCTExpectation#>], timeout: 60)

<#Assert app detects billing retry and no longer allows access to subscription#>

session.resolveIssueForTransaction(identifier: transaction.identifier)

<#Assert app allows access to subscription#>

キーポイント:

  • billingGracePeriodIsEnabledテスト セッションで猶予期間をオンにします。 -shouldEnterBillingRetryOnRenewal更新を請求失敗パスに入れます。 -transaction.hasPurchaseIssueトランザクションが購入発行ステータスであることを確認します。
  • 最初のアサーションは、サブスクリプションへのアクセスが猶予期間内に引き続き許可されているかどうかをチェックします。
  • 2 番目のアサーション チェック猶予期間が終了した後、アプリ認識の課金が再試行されます。 -resolveIssueForTransactionシミュレートされた支払いの問題が修正されました。

(27:55) サンドボックスには、新しい請求失敗シミュレーションも追加されています。サンドボックス テスター作成フォームにはフィールドが少なく、電子メール アドレスはプラス記号をサポートし、パスワードには複雑さのプロンプトが表示されます。その後、App Store Connect API は、サンドボックス Apple ID のクエリ、購入履歴のクリア、購入中断状態の設定をサポートする予定です。サンドボックス アカウント設定の請求失敗シミュレーションにより、フロントエンドの購入が失敗し、自動更新可能なサブスクリプションが運用環境と一致する請求失敗状態になることが可能になります。サーバーは使用できますverifyReceipt、App Store サーバー API、および App Store サーバー通知 V2 を使用して、これらのステータスを確認します。


重要ポイント

  • サブスクリプション境界状態テスト パネルのセットを作成: 返金、割引コード、価格値上げ、請求の再試行、猶予期間をテスト リストとしてリストします。実行する価値がある理由: これらの状態は Xcode または Sandbox でシミュレートできるため、リリース前に手動で受け入れるのに適しています。開始方法: StoreKit 構成ファイルを使用してサブスクリプションを構成し、トランザクション マネージャーのインスペクターとフィルターを使用して項目ごとに検証します。

  • 払い戻し後の権利取り消しのための回帰テストを作成します: 払い戻しが成功した後、アプリが対応する機能を直ちに削除することを確認します。実行する価値がある理由: 返金が行われるTransaction.updatesベルトを送り出すrevocationDateトランザクションの場合、コード パスは明確です。開始方法: モニタリングTransaction.updates、検出後revocationReason製品へのアクセス権が取り消されます。

  • ストア ページに現在のオファーと待機中のオファーを表示: 現在利用しているオファーと次回の更新に適用されるオファーを加入者に知らせます。実行する価値がある理由: セッションは現在のトランザクションを表示し、renewalInfo提供できるofferType。開始方法: 購読するProduct.SubscriptionInfo.Status.updates、別途お読みくださいtransaction.offerTyperenewalInfo.offerTypeそしてrenewalInfo.offerID

  • 値上げメッセージの表示を遅らせる: StoreKit メッセージをエディター、支払い確認ページ、またはその他の機密プロセスに一時的に保存します。実行する価値がある理由: 価格引き上げプロンプトは複数回表示される場合があり、Xcode はタイミングを確認するために繰り返しプロンプトをトリガーできます。開始方法: モニタリングMessage.messages、 得るDisplayMessageAction次に、現在のインターフェイスに基づいてそれを表示するか配置するかを決定します。pendingMessages

  • 請求に失敗したユーザーに修復ポータルを提供: 猶予期間中はアクセスを維持し、請求が再試行されるときにユーザーに支払い情報を更新するよう促します。実行する価値がある理由: セッション内で猶予期間を明確に示すことで、非自発的離脱を減らすことができます。開始方法: 確認してくださいProduct.SubscriptionInfo.Status.stateそうですか.inGracePeriodまたは.inBillingRetryPeriod、そして使用しますopenURLApp Store の請求ページを開きます。


関連セッション

  • アプリ内購入の新機能 - まず、StoreKit 2、アプリ トランザクション、メッセージ API、サーバー API の年次更新を確認してから、このサイトに戻ってテスト カバレッジを確認してください。
  • アプリ内購入の統合と移行を探索する — このセッションでは、サンドボックスでの V2 通知と App Store サーバー API の検証ステータスが紹介され、移行の詳細はこのセッションから始まります。
  • アプリ内購入のプロアクティブな復元を実装する — サブスクリプション ステータスのテストが完了した後、アプリの起動時にプロアクティブに復元してユーザー権限を決定する方法を引き続き学習できます。
  • SKAdNetwork の最新情報 - この記事では、広告アトリビューション テストの背景を補う SKAdNetwork 単体テストに StoreKitTest を使用できることについて説明しています。

コメント

GitHub Issues · utterances