StoreKit 2によるiOSのアプリ内課金のチュートリアル
実際に動作するサンプルコードが付属したアプリ内課金のステップ・バイ・ステップ形式の解説です。
この StoreKit 2 チュートリアルにはサンプルコードとサンプルアプリが提供されており、以下のURLからダウンロードできます。https://github.com/RevenueCat/storekit2-demo-app.
はじめに
アプリ内課金とサブスクリプションはApp Storeで収益を上げるための最適な方法の一つです。Appleが新たにアップデートしたStoreKit 2は、アプリ内課金のためのフレームワークで、開発者はこれを利用してiOS、macOS、watchOS、tvOSのアプリにIAP(アプリ内課金)を 追加できます。AppleのドキュメントにはStoreKitの使い方に関して基本的な説明がありますが、複雑な部分の詳細や完全な使用例は提供されていません。
このチュートリアルでは、基本的なコンセプト、App Store Connectの設定、StoreKit 2の導入方法、購入の表示、完了、確認の方法、そしてアプリ外で発生する変更(更新、キャンセル、課金の問題など)の対応方法について解説します。さらに、サーバーを持つかどうかの利点とトレードオフについても説明します。Swiftによるサンプルコードと実行可能なサンプルアプリも提供します。
用語について
アプリ内課金(IAP)とは、アプリ内で使用するために購入するデジタル製品のことです。これらの購入は通常アプリ内で直接行われますが、App Store経由で購入することも可能です。
Appleプラットフォームでは4種類のIAPが提供されています。
- 消耗型
ゲームをさらに進めるためのライフや宝石、デートアプリで自分のプロフィールの表示頻度を向上するためのブースト、ソーシャルメディアアプリでクリエイターが活用できるデジタルヒントなど、異なるタイプの消耗型アイテムを提供できます。消耗型のアプリ内課金は一度使うとなくなり、再度購入することが可能です。フリーミアムビジネスモデルを使うアプリやゲームでよく提供されています。
消耗品は、一度か複数回購入でき、大量に利用される商品です。例えば、ゲーム内の「ライフ」や「ジェム」、出会い系アプリの「ブースト」、開発者やクリエイター向けの「ヒント」などがあります。 - 非消耗型
一度購入すれば無期限に使用できる、非消耗型のプレミアム機能を提供することができます。たとえば、写真アプリの追加フィルタ、イラスト作成アプリの追加ブラシ、ゲームのコスメティックアイテムなどがあります。非消耗型のアプリ内課金では、ファミリー共有を提供することができます。
非消費型商品は、一度しか購入できない永続的な商品です。これには、アプリのプレミアム機能のアンロック、ペイントアプリの追加ブラシ、ゲームのコスメティックアイテムなどが含まれます。 - 自動更新サブスクリプション
アプリのコンテンツ、サービス、プレミアム機能への継続的なアクセスを提供できます。自動更新サブスクリプションでは、ユーザーがキャンセルするか何らかの問題が発生するまで定期的に課金が行われます。一般的なユースケースには、メディアやコンテンツのライブラリ(ビデオ、音楽、記事など )、サービスとしてのソフトウェア(クラウドストレージ、仕事効率化、グラフィックス、デザインなど)、教育コンテンツなどへのアクセスがあります。自動更新サブスクリプションでは、ファミリー共有を提供することができます。
自動更新可能なサブスクリプションは、アプリ内のコンテンツやサービス、プレミアム機能への継続的なアクセスを提供します。顧客は、キャンセルするか課金問題が発生するまで定期的に課金されます。例としては、ソフトウェアサービスへのアクセスや教育アプリのレッスンなどがあります。 - 非自動更新サブスクリプション
ゲーム内コンテンツのシーズンパスなど、期間限定のサービスやコンテンツを提供することができます。このタイプのサブスクリプションは自動的に更新されないため、アクセスの継続を希望する場合は、ユーザー自身が都度購入する必要があります。
更新不要のサブスクリプションは、自動更新なしで、アプリ内のコンテンツやサービス、プレミアム機能への期間限定アクセスを提供します。このタイプのサブスクリプションでは、サービスやコンテンツへのアクセスを継続するためにユーザーが手動で再購読する必要があります。例としては、ゲーム内コンテンツのシーズンパスが挙げられます。
App Store Connectの設定
アプリにアプリ内課金を追加する最初の手順は、App Store Connectで商品を作成することです。
アプリ内課金の作成と管理には、「アプリ内課金」と「サブスクリ プション」という2つのセクションがあります。「アプリ内課金」セクションでは、消耗型と非消耗型を扱います。一方、「サブスクリプション」セクションでは、自動更新サブスクリプションと非自動更新サブスクリプションを管理します。サブスクリプションは、消耗型や非消耗型と比べて設定が複雑なため別のセクションに分離されました。
App Store Connectの管理画面で「アプリ内課金」と「サブスクリプション」の場所を示しています。
App Store Connectの要件
App Store Connectでアプリがアプリ内課金を販売するためには、以下の管理手続きを完了する必要があります。
自動更新および非自動更新サブスクリプションの作成
- 「サブスクリプション」を選択
- 「サブスクリプショングループ」を作成
- 「サブスクリプショングループ」のローカリゼーションを追加する
- 新しいサブスクリプションを作成する(参照名と製品ID)
- すべてのメタデータを記入する(期間、価格、ローカリゼーション、審査に関する情報)
自動更新と非自動更新のサブスクリプションを作成するApp Store Connectの画面
消耗型および非消耗型の作成
- 「アプリ内課金」を選択
- 「アプリ内課金」を作成する
- タイプ(消耗型または非消耗型)を選択し、参照名と製品IDを設定する
- すべてのメタデータを記入する(期間、価格、ローカリゼーション、審査に関する情報)
消耗型と非消耗型のアプリ内課金を作成するApp Store Connectの画面
StoreKit Configuration Fileをセットアップする
App Store Connectで商品を設定する作業は大変ですが、アプリ内課金(IAP)付きアプリをリリースするためには不可欠です。しかし、ローカル開発にはApp Storeは必要ありません。Xcode 13からはアプリ内課金の全ワークフローをStoreKit Configuration Fileを使って行うことができます。
StoreKit Configuration Fileの利用には、App Store Connectへのログインを遅らせるだけでなく、以下のような用途もあります:
- シミュレーターでの購入フローのテスト
- 単体テストやUIテストでの購入フローのテスト
- ネットワーク接続がない場合のローカルテスト
- サンドボックス環境での設定や、エッジケースのデバッグ
- トラブル、更新、課金問題、プロモーションオファー、紹介オファーを含むエンドツーエンドのトランザクションテスト
実際、このチュートリアルに添付されているサンプルコードとサンプルアプリは、StoreKit設定ファイルを使用しています。これらをダウンロードして実行すると、App Store Connectでの設定は不要です。
StoreKit Configuration Fileの全機能については、以下のWWDCセッションが詳しく説明しています:
また、iOS 14のStoreKitテストの改善に関する記事でこれらのWWDCセッションの内容を簡潔にまとめています。
次に、StoreKit Configuration Fileの作成、設定、有効化の手順の概要を説明します。
Create the file
- Xcodeを起動して、メニューバーから「File」>「New」>「File…」と選択します。
- 検索フィールドに「storekit」と入力します。
- 「StoreKit Configuration File」を選択します。
- 適当なファイル名を入力して「Sync this file with an app in App Store Connect 」をチェックして保存します。
製品の追加(任意)
Xcode 14ではこのファイルをApp Store Connectのアプリと同期する機能が追加されました。製品を手作業でStoreKit設定ファイルに追加しなくてすみます。
Xcode 13を使用している場合や異なる製品タイプや期間でテストしたい場合は下記の手順で製品を追加できます。
- XcodeでStoreKit Configuration Fileを選択し左下の「+」ボタンをクリックします。
- アプリ内課金の種類を選択します。
- 下記の必要事項を記入します。
- 参照名
- 製品ID
- 価格
- 1つ以上のローカリゼーション
StoreKit Configuration Fileを有効にする
StoreKit Configuration Fileを作成しただけではまだ使用できません。XcodeのSchemeでStoreKit Configuration Fileを選択する必要があります。
- Scheme名をクリックして「Edit Scheme…」を選択します。
- 「Run」>「Options」と選択します。
- 「StoreKit Configuration」でファイルを選択します。
StoreKit Configuration Fileを使用することはアプリ内課金をテストする最も簡単な方法ですが、デバイス上でサンドボックス環境をテストする必要がある場合もあります。もともとのSchemeを複製して「YourApp (SK Config)」のような名前でStoreKit Configuration File専用のSchemeにすることを推奨します。そうしておくと、StoreKit Configuration Fileを使用するかどうかを簡単に切り替えられます。
StoreKit 2を利用してデバイス上でアプリ内課金を実装する
StoreKit Configuration Fileを使うか実際のApp Store Connectのデータを使用するかに関係なく、アプリ内課金における製品はStoreKit APIを使う際の最初のステップです。
このセクションではStoreKit 2を使用するコードをステップ・バイ・ステップで解説します。
- List products
- Purchase products
- Unlock features for active subscriptions and lifetime purchases
- Handle renewals, cancellations, and billing errors
- Validate receipts
上記のそれぞれの手順はバックエンドサーバーを使わない方法で実装します。バックエンドサーバーを使わずにアプリ内課金の処理を実行することはStoreKit 1では難しく、安全ではありませんでした。AppleはStoreKit 2でそれを可能にし、安全にするために大きな改良を加えました。バックエンドサーバーで購入処理を行うには、デバイスだけで実行するより多くの作業が必要になりますが、利点もたくさんあります。
では、はじめましょう。
手順1:商品の一覧を表示する
まずアプリ内課金に登録した製品をボタンとして表示します。ボタンをタップすると製品を購入できます。下記の例では「月額課金」「年額課金」「終身サブスクリプション課金」の各製品が表示されます。この手順を実行すると表示される画面は下記になります。
サンプルアプリの製品一覧画面
StoreKit 2では製品データの取得に必要なコードはほんの数行だけです。
1import StoreKit
2
3let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]
4let products = try await Product.products(for: productIds)
上記のコードの意味は、まずStoreKitをインポートします。次に画面に表示する製品IDを文字列の配列として定義します。最後にAPIから製品データを取得します。製品IDはStoreKit Configuration Fileまたは App Store Connect で定義された商品と一致する必要があります。結果としてProductオブジェクトの配列が得られます。Productオブジェクトには画面のボタンに表示するために必要な情報がすべて含まれています。またProductオブジェクトはボタンがタップされて購入の処理が発生するときにも使用されます。
この例では製品IDをハードコーディングしていますが、実際のアプリではハードコーディングすべきではありません。製品IDをハードコーディングしてしまうと、製品の追加や変更のたびにアプリのアップデートが必要になります。すべてのユーザーが自動アップデートを有効にしているわけではないので、すでに削除した製品などが購入されるおそれがあります。リモートサーバーから購入可能な製品の一覧を提供することがもっとも良い方法です。
次に、取得した製品をビューに表示します。下記は@Stateを付与したproductsという変数に取得した製品を保持するSwiftUIのビューです。
1import SwiftUI
2import StoreKit
3
4struct ContentView: View {
5 let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]
6
7 @State
8 private var products: [Product] = []
9
10 var body: some View {
11 VStack(spacing: 20) {
12 Text("Products")
13 ForEach(self.products) { product in
14 Button {
15 // Don't do anything yet
16 } label: {
17 Text("\(product.displayPrice) - \(product.displayName)")
18 }
19 }
20 }.task {
21 do {
22 try await self.loadProducts()
23 } catch {
24 print(error)
25 }
26 }
27 }
28
29 private func loadProducts() async throws {
30 self.products = try await Product.products(for: productIds)
31 }
32}
こちらのコードでは製品データを取得する処理を新しくloadProducts()関数に移動しています。この関数は.task()モディファイアを使用してビューが表示されたときに呼び出されます。取得した製品はForEachループを使って取り出されて各製品のボタンが表示されます。このボタンはまだタップしても何もしません。
ここまでのコードの完全な実装はこちらをご覧ください。
https://github.com/RevenueCat/storekit2-demo-app/tree/main/StepByStepExamples/Step1
手順2:商品の購入
ここまでで、各製品 が画面に一覧として表示されるようになりました。次の手順はボタンをタップすると製品を購入できるようにすることです。
製品に対して購入の処理を開始するには、製品オブジェクトのpurchase()関数を呼び出します。
1private func purchase(_ product: Product) async throws {
2 let result = try await product.purchase()
3}
この関数がProduct.PurchaseErrorまたはStoreKitErrorをthrowした場合、購入が成功しなかったことを示します。ただし、エラーが発生しなくても購入が成功したとはまだ限りません。購入処理が成功したかどうかを判断するには関数の戻り値を検証する必要があります。
purchase()の戻り値はenum Product.PurchaseResult型です。PurchaseResultの定義は下記になります。
1public enum PurchaseResult {
2 case success(VerificationResult<Transaction>)
3 case userCancelled
4 case pending
5}
6
7public enum VerificationResult<SignedType> {
8 case unverified(SignedType, VerificationResult<SignedType>.VerificationError)
9 case verified(SignedType)
10}
PurchaseResultは製品の購入処理で起こりうるすべての結果を表しています。PurchaseResultを検証するように更新したコードを下記に示します。
1private func purchase(_ product: Product) async throws {
2 let result = try await product.purchase()
3
4 switch result {
5 case let .success(.verified(transaction)):
6 // Successful purhcase
7 await transaction.finish()
8 case let .success(.unverified(_, error)):
9 // Successful purchase but transaction/receipt can't be verified
10 // Could be a jailbroken phone
11 break
12 case .pending:
13 // Transaction waiting on SCA (Strong Customer Authentication) or
14 // approval from Ask to Buy
15 break
16 case .userCancelled:
17 // ^^^
18 break
19 @unknown default:
20 break
21 }
22}
PurchaseResultの値の種類と意味は下記の表をご覧ください。
値 | 説明 |
Success – verified | 製品の購入処理が成功しました。 |
Success – unverified | 製品の購入処理は成功しましたが、StoreKitの検証が失敗しました。この状態は脱獄したデバイス上で実行されたことが原因の可能性があります。しかしStoreKitのドキュメン トにこの状態についての記載はなく、詳細は不明です。 |
Pending | 「強力な顧客認証(SCA:Strong Customer Authentication)」または「承認と購入のリクエスト」のいずれかによって発生します。「強力な顧客認証」は購入処理が完了する前に金融機関が求める追加の確認や承認のプロセスです。このプロセスはアプリやSMSのテキストメッセージを通じて行われます。承認された後に購入処理のトランザクションが更新されます。「承認と購入のリクエスト」は、子どもがアプリ内課金で製品を購入しようとした際に、親や保護者の承認を必要とする機能です。保護者が購入を承認または却下するまで、購入処理は保留状態になります。 |
User Canceled | ユーザーが購入処理をキャンセルしました。通常はこの値をエラーとして扱う必要はありません。キャンセルが発生したことを記録できるようにしておくとアプリの改善に役立ちます。 |
Error | Errorがthrowされた場合の値はProduct.PurchaseErrorまたはStoreKitErrorです。インターネットに接続できない、App Storeに障害が発生している、クレジットカードの支払いに問題が発生した、などの原因が考えられます。 |
ボタンをタップすると購入処理が実行されるようにボタンのActionクロージャ内でpurchase(_ product: Product)関数を呼び出します。
1ForEach(self.products) { (product) in
2 Button {
3 Task {
4 do {
5 try await self.purchase(product)
6 } catch {
7 print(error)
8 }
9 }
10 } label: {
11 Text("\(product.displayPrice) - \(product.displayName)")
12 }
13}
ここまでのコードの完全な実装はこちらをご覧ください。
https://github.com/RevenueCat/storekit2-demo-app/tree/main/StepByStepExamples/Step2
手順3:有料の機能を解放する 処理の準備
ここまでで、購入可能なすべての製品(今回の例では、2種類の定期サブスクリプションと終身サブスクリプション)を一覧に表示し、ボタンをタップするとその製品を購入できるようになりました。しかし購入処理が実行されるだけでは完成とはいえません。製品の購入に成功するとなんらかの機能が解放されたり、サブスクリプションが有効な間に定期的にコンテンツをダウンロードする必要があるかもしれません。
購入後のコンテンツのダウンロードや機能の解放を実装しようとすると、コードが複雑になってきます。購入が成功した後に機能を解放する処理を追加するだけでいいのであれば簡単ですが、たいていはそれだけでは十分ではありません。アプリから購入する以外にも複数の購入フローがあり、前の手順ではそのうちの2つである「強力な顧客認証」と「承認と購入のリクエスト」について説明しました。アプリ内課金は、直接App Storeアプリから購入することもできるので、アプリの外部でも発生します。購入はいつでも起こりうるので、アプリはあらゆる状況に対応できるように準備しなければなりません。
これらのすべてのケースを扱い始める前に既存の実装をクリーンアップする必要があります。現在すべてのアプリ内課金のロジックはSwiftUIビュー内に存在します。これはエンドツーエンドの購入フローを動作させようとするときには問題ありませんでしたが、アプリの規模が大きくなるにつれてうまくスケールしません。すべてのアプリ内課金のロジックはビューではなく再利用可能なコンポーネントに移動するべきです。これにはさまざまな方法があるのでアプリによってやり方は異なりますが、このステップではアプリ内課金のロジックを新しくPurchaseManagerに移動します。PurchaseManagerは、最初は製品データの読み込みと製品の購入処理を担当します。他の必要な機能は後ほど追加していきます。
1import Foundation
2import StoreKit
3
4@MainActor
5class PurchaseManager: ObservableObject {
6
7 private let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]
8
9 @Published
10 private(set) var products: [Product] = []
11 private var productsLoaded = false
12
13 func loadProducts() async throws {
14 guard !self.productsLoaded else { return }
15 self.products = try await Product.products(for: productIds)
16 self.productsLoaded = true
17 }
18
19 func purchase(_ product: Product) async throws {
20 let result = try await product.purchase()
21
22 switch result {
23 case let .success(.verified(transaction)):
24 // Successful purhcase
25 await transaction.finish()
26 case let .success(.unverified(_, error)):
27 // Successful purchase but transaction/receipt can't be verified
28 // Could be a jailbroken phone
29 break
30 case .pending:
31 // Transaction waiting on SCA (Strong Customer Authentication) or
32 // approval from Ask to Buy
33 break
34 case .userCancelled:
35 // ^^^
36 break
37 @unknown default:
38 break
39 }
40 }
41}
loadProducts()とpurchase()関数をPurchaseManagerに移動します。ContentViewではPurchaseManagerを使用します。 PurchaseManagerはAppで作成されて、EnvironmentObjectとしてContentViewに渡されます。この方法では他にSwiftUIのビューが増えても、同じPurchaseManagerオブジェクトに簡単にアクセスできます。
1struct YourApp: App {
2 @StateObject
3 private var purchaseManager = PurchaseManager()
4
5 var body: some Scene {
6 WindowGroup {
7 ContentView()
8 .environmentObject(purchaseManager)
9 }
10 }
11}
PurchaseManagerはObservableObjectなので、プロパティが変更されるとSwiftUIのビューは自動的に再描画されます。
1struct ContentView: View {
2 @EnvironmentObject
3 private var purchaseManager: PurchaseManager
4
5 var body: some View {
6 VStack(spacing: 20) {
7 Text("Products")
8 ForEach(purchaseManager.products) { product in
9 Button {
10 Task {
11 do {
12 try await purchaseManager.purchase(product)
13 } catch {
14 print(error)
15 }
16 }
17 } label: {
18 Text("\(product.displayPrice) - \(product.displayName)")
19 .foregroundColor(.white)
20 .padding()
21 .background(.blue)
22 .clipShape(Capsule())
23 }
24 }
25 }.task {
26 Task {
27 do {
28 try await purchaseManager.loadProducts()
29 } catch {
30 print(error)
31 }
32 }
33 }
34 }
35}
アプリの実行結果はステップ2とまったく変わりません。ですがアプリ内課金のユースケースを増やす際に、ステップ2と比べてコードが管理しやすくなりました。
ここまでのコードの完全な実装はこちらをご覧ください。
https://github.com/RevenueCat/storekit2-demo-app/tree/main/StepByStepExamples/Step3
手順4:有料の機能を解放する
これか らPurchaseManagerに有料の機能を解放するためのロジックを記述していきます。有料の機能の購入の判定についてはStoreKit 2のTransaction.currentEntitlementsプロパティを主に利用します。
1for await result in Transaction.currentEntitlements {
2 // Do something with transaction
3}
4
currentEntitlementsという名前は、StoreKit 2のそれ以外のAPIの命名からすると少しわかりにくいです。実際にはTransaction.currentEntitlementsは有効なトランザクションの配列を返します。currentEntitlementsのドキュメント