【Swift/StoreKit2】アプリ内課金の実装方法!プロダクトの取得と購入、復元
この記事からわかること
- Swift/iOSでアプリ内課金を実装する方法
- 課金アイテムの種類
- StoreKitの使い方
- アイテムの取得や購入方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- macOS:Sonoma 14.6.1
アプリ内の課金要素として「ゲームのスタミナ」や「機能の解放」、「広告の削除」などインストール自体は無料だが使用する中で料金が発生する仕組みのアプリは数多くあります。iOSアプリにおける「アプリ内課金 (In-App Purchase)」の設定と実装方法をまとめていきます。
アプリ内課金 (In-App Purchase)
iOSアプリでアプリ内課金を実装する大まかな流れは以下の通りになります。
大まかな実装の流れ
- 有料App契約(App Store Connect)
- 課金アイテムの設定(App Store Connect)
- Capabilitiesの追加(アプリ)
- 課金処理の実装(アプリ)
有料App契約とは?
iOSアプリで有料アプリとしてリリースしたり、アプリ内課金を実装するためには「有料App契約」を結んでおく必要があります。振込先の銀行口座や納税フォームを登録して収益を正しく受け取れるように準備をする必要があるということです。詳細は以下の記事を参考にしてください。
課金アイテムの種類
iOSアプリの「アプリ内課金 (In-App Purchase)」は4つの種類に分類されます。実装する際は適切なタイプを選択します。
種類 | 概要 |
---|---|
消耗型 | 一度購入すると使い切り(例: ゲーム内アイテム) |
非消耗型 | 一度購入すると永久に利用可能(例: 広告の削除、特定の機能のアンロック) |
自動更新サブスクリプション | 月額や年額で継続的に課金(例: サブスクリプション型サービス) |
非更新サブスクリプション | 定期購読だが自動更新なし(例: 一定期間限定のサービス) |
公式リファレンス:In-app purchase types
App Store Connectで課金アイテムを作成
公式リファレンス:In-app purchase information
有料App契約が済んでいる状態として一番最初に行うのは課金アイテムを登録です。「App Store Connect」にログインして対象アプリの「Monetization」>「In-App Purchases」を選択し「Create」をクリックしてアイテムの作成を始めます。
Type
「Consumable(消耗型)」または「Non-Consumable(非消耗型)」
Reference Name
売上やトレンドのレポートで使用される名称。App Storeには表示されない
Product ID
売上やトレンドのレポートに使用される一意の英数字ID。(com.example.app.item1
とかのが良いかも)
課金アイテムの詳細設定
作成が完了すると課金アイテムの詳細を設定できるようになります。続けて設定していきます。
- 「Monetization」>「In-App Purchases」>「Drafts」に作成した課金アイテムがあるのでクリック
- 「Availability」には「任意の適切な公開範囲」を選択
- 「Price Schedule」には「任意の通貨と料金」を選択
- 「App Store Localization」には「広告表示名と説明文」を記述して作成(この部分はApp Store に表示されます)
- 「Review Information」には「審査用のスクショと説明」を記述
- 全て完了したら「Save」をクリックして完成
これでApp Store Connect側の作業は一旦終了です。
Capabilitiesの追加
ここからはアプリ側の作業になります。まずは「TARGET」>「Signing & Capabilities」>「+ Capability」をクリックし「In-App Purchase」を追加します。
プロジェクトによっては以下のようなポップアップが表示されるので「Change All」をクリックします。これはおそらくStoreKitフレームワークを追加するためのポップアップかと思います。
StoreKit2の主要なクラス
StoreKit2
で実装するために主要となるクラスなどを先に軽くまとめておきます。
Product
struct Product
アプリ内の課金アイテム自体はProduct
構造体にマッピングされて管理されます。この構造体を起点にしてアイテムの詳細情報の取得や実際の購入処理などを行います。
PurchaseResult
public enum PurchaseResult {
case success(VerificationResult<Transaction>)
case userCancelled
case pending
}
列挙型PurchaseResult
は課金アイテムの購入結果を識別するためのオブジェクトです。購入自体の成功、失敗、ユーザーキャンセルだけでなく正常なトランザクション(取引)であったかどうかの検証結果も取得することができます。
Transaction
アプリ内購入の取引(トランザクション)を管理するオブジェクト。
アプリ内課金の実装:StoreKit2
アプリ内課金の実装方法はiOS15以降からStoreKit2
を使用して実装します。以前の旧StoreKit
と実装方法が大きく変わってしまったので古い記事だと動作しなかったりしたので注意してください。この記事では特に旧StoreKit
との差分などは解説しませんがざっくりいうとDelegate
パターンからSwift Concurrencyを使用した実装に変わり個人的にはですがコード量の削減と可読性がグッと向上しています。
また今回の実装コードはApple公式サンプルプロジェクトのコードを参考にしています。正直これをみた方が正しいと思うのでうまく動作しない場合はこちらを参考にしてください。
公式リファレンス:Implementing a store in your app using the StoreKit API
アプリ内課金を実装して動作確認をするためにはローカルテスト環境を作成しておくと便利です。構築方法は以下の記事を参考にしてください。
レシート検証
アプリ内購入の仕組みとして不正購入を防ぐための「レシート検証」というプロセスがあります。ここでいう「レシート」は店舗などでもらえるレシートと同様に購入の記録を示す証明書のことです。Appleはこのレシートに署名を施し取引が正規のものであることを保証します。
以前(旧StoreKit)はデバイスから購入レシート(Base64エンコードされた文字列)を取得し、Appleの検証サーバー(https://buy.itunes.apple.com/verifyReceipt
)にリクエストを送信し、レスポンスを解析して購入の正当性を確認するステップを踏んでいました。
しかしStoreKit2からはレシート検証にJWS(JSON Web Signature)
方式を採用し、購入データがトランザクション単位で署名されるようになりました。この変更によりレシート検証ステップを開発者はあまり意識せずとも実装できるようになっています。
課金アイテムの取得
アプリ内からApp Store Connectに登録した課金アイテムを取得するにはProduct.products(for:)
メソッドを使用します。引数には取得したい課金アイテムに設定したProduct ID
を配列で渡します。
// 課金アイテム取得対象のProduct IDを指定
let productIdentifiers = ["com.example.app.item1, com.example.app.item2"]
// 課金アイテム取得
let products: [Product] = try await Product.products(for: productIdentifiers)
Product.products(for:)
はアプリ内に直接定義するよりサーバー側からレスポンスで取得できるようにして課金アイテムを増やした際にアプリ側の改修工数を削減できるようにしておくことが多いです。
アイテムの保持するそれぞれの情報は各プロパティから取得することが可能です。displayName
やdisplayPrice
などはローカライズされた情報を取得できます。
product.displayName // アイテム名
product.description // アイテム説明
product.price // 通貨価格(Decimal型)
product.displayPrice // 現地通貨価格(String型)※
subscription?.subscriptionPeriod.value // サブスクリプションの期間(年数/月数/日数)
subscription?.subscriptionPeriod.unit // サブスクリプションの単位(年月日)
※displayPrice
はテスト環境でない場合にシミュレーターではデバイスの地域などをいじっても「$」のままで日本円になりませんでした。(実機では日本円になることを確認できました。)よく分かってませんがサンドボックスあたりの設定で変更できるのかもです。
課金アイテムの購入
課金アイテムを購入するには購入対象のProduct
構造体のpurchase
メソッドを呼び出します。結果(返り値)としてPurchaseResult
型が返却されます。PurchaseResult.success(VerificationResult<Transaction>)
はさらに引数からレシート検証結果を識別するVerificationResult
型を取得することができます。
// 課金アイテムを購入
let result = try await product.purchase()
// 結果によって処理を分岐する
switch result {
case .success(let verificationResult):
// 購入成功。取引開始
switch verificationResult {
case .verified(let transaction):
// レシート検証成功
// ここで購入された後の処理を実行する(機能の解放やスタミナの付与など)
// 上記処理が完了したタイミングで必ずトランザクションを終了させる
await transaction.finish()
case .unverified(let transaction, let verificationError):
// レシート検証失敗
break
}
case .pending:
// 購入保留中。ユーザーのアクション待ち
break
case .userCancelled:
// ユーザーキャンセル。
break
@unknown default:
break
}
unverified
になった場合はApp StoreサーバーでのレシートJWS検証に失敗した場合です。VerificationError
型でエラーを取得することが可能です。
またpurchase
メソッド自体はPurchaseError
またはStoreKitError
をスローする可能性があります。
let result = try await product.purchase()
purchase
メソッドを実装すると以下のような警告が表示されます。これはトランザクションを観測(リッスン)していないために表示されるので後述しているTransaction.updates
の実装をすることで解消します。
Making a purchase without listening for transaction updates risks missing successful purchases. Create a Task to iterate Transaction.updates at launch.
購入済みアイテムを取得する
公式リファレンス:Transaction.currentEntitlements
購入済みのアイテムなど購入に関する情報を取得したい場合はTransaction.currentEntitlements
を使用します。currentEntitlements(直訳:現在の権利)
で取得できるのは以下の情報です。
- nonConsumable(非消耗型)のトランザクション情報
- nonRenewable(非更新サブスクリプション)のトランザクション情報
- autoRenewable(自動更新サブスクリプション)のトランザクション情報(アクティブなもののみ)
「Consumable(消耗型)」の購入情報は取得できないので注意してください。購入件数などを取得したい場合はローカルなどに貯めておくしかないのかも知れません。
@MainActor
func updateCustomerProductStatus() async {
// 最新のトランザクション情報を取得する
for await result in Transaction.currentEntitlements {
do {
// レシート検証済みかどうか
let transaction = try checkVerified(result)
// 各タイプ別のトランザクション履歴情報を取得する
switch transaction.productType {
case .nonConsumable:
case .nonRenewable:
case .autoRenewable:
default:
break
}
} catch {
print()
}
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw StoreKitError.unknown
case let .verified(safe):
return safe
}
}
購入情報を更新する
購入情報は途中のトランザクションがあった場合や他デバイスでの購入が発生した場合などアプリが未起動の状態の際に更新されている可能性があります。そのためアプリ起動時などにその更新情報を取得しアプリにも反映させる必要があります。
更新されている情報はTransaction.updates
から取得することができます。このタスクをアプリ起動時などに実行しておけばOKです。StoreKit管理クラスなどにまとめている場合はinit
などに仕込んでおきます。
private var updateListenerTask: Task<Void, Error>? = nil
init() {
updateListenerTask = listenForTransactions()
Task {
await requestProducts()
await updateCustomerProductStatus()
}
}
deinit {
updateListenerTask.cancel()
}
private 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.")
}
}
}
}
この処理はTask(priority: .background)
やTask.detached
などメインスレッド以外から実行します。
購入を復元する
購入の復元処理は非常に簡単で AppStore.sync()
を呼び出すだけです。これでApp Storeと同期されcurrentEntitlements
から購入情報を取得できるようになるのでその購入情報に基づいてアプリ内の復元処理を実行してあげればOKです。
try await AppStore.sync()
審査に提出
アプリ内課金を実装してリリースするためには「App Store Connectでの課金アイテムの追加」と「アプリ内に課金機能の実装」の手順を踏んできました。あとは審査に提出するだけですが少しだけ注意点があります。
1.課金アイテムも審査が必要
App Store Connectに追加した課金アイテム自体にも審査が必要になります。アプリをアップデートする際のApp Store Connectの「Distribution」タブの項目の中に課金アイテムを審査対象に含められる箇所があるので選択しておいてください。
課金アイテムを審査に出しているのにアプリ内に課金処理が実装されていない場合はリジェクトされてしまうので注意してください。
2.復元処理が必要
アプリ内課金処理が実装されていても「購入を復元する」で解説した復元処理が実装されていないとリジェクトされてしまうことがあるようです。こちらも忘れずに実装しておいてください。
アプリ内課金の詳細なコードはGitHub:MinnanoTanjyoubi内のコードを参考にしてください。実際にリリースしているアプリのコードなので動作確認をしながらコードと照らし合わせるとわかりやすいかもです。
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。