StoreKit2の実装方法を確認する
StoreKit2とは?
今年もやってきたWWDC! 今年も去年に引き続きオンライン開催になり、セッションの動画が簡単にアプリやWebから見れて、とてもありがたいです。
今回はStoreKit関連に大きくUpdateがあり、StoreKit2として発表されました。
StoreKit関連の動画はいくつかあり、以下に順番に見ることがおすすめされています。
1. Meet StoreKit 2
2. Manage in-app purchases on your server
3. Support customers and handle refunds
ざっくり内容をまとめると、以下の通りです。
- Swift concurrency(async/await)を使って、簡単に、短く記述できるようになった
- アプリ内でサブスクリプションの管理や返金が行えるようになった
- 過去のトランザクションの履歴や購入済みの課金アイテムのステータスも取得できる
今回は主に「1. Meet StoreKit 2」の内容についてまとめます。
Xcode13 betaで触れるStoreKit2のサンプルアプリの中の、主にStoreKit2の課金処理が書かれているStore.swift
ファイルの処理を確認します。
- Mac: Monterey v12.0(21A5248p)
- Xcode: 13 beta(13A5154h)
StoreKit2での記述方法を確認
ここで実際の記述内容を確認してみましょう。
ちなみにIn-App-Purchase
自体の実装方法・フローがよくわかっていないという方のために簡単に説明すると、課金の処理はだいたい以下のようなフローになります。
① 課金アイテムの情報を取得(消費型(課金用アイテムなど)、非消費型(広告非表示などの買い切りアイテム)、自動更新型(いわゆるサブスクリプション)など)
②課金処理用のトランザクション という単位で課金処理を管理
②-1 トランザクション
を開始
②-2 AppStore側での購入処理に成功した後、レシート検証(課金)が必要な場合はその処理を行う、だいたいサーバーで実施
②-3 トランザクション
を終了する
詳しくは偉大な先人たちの記事で確認することができます
これを今までのStoreKitで実装しようとすると、
SKProduct
SKPayment
, SKPaymentQueue
, SKPaymentTransaction, SKProductsRequest
など多くの登場人物たちの役割や使い方を把握しないといけません。
しかしStoreKit2ではこの辺りの登場人物も大幅に刷新されました
①Product
: 課金アイテムのstruct(旧 SKProduct
)
②Transaction
: トランザクションを管理するstruct(旧SKPaymentTransaction
)
名前もシンプルになり、登場人物も少なくなりました。
次にコードの記述方法を確認しましょう。
WWDCの動画でも使われていたサンプルアプリのコードを引用します。
なおasync/await
の詳細については、割愛します。
課金アイテムの取得方法
課金アイテムの取得方法を確認します。
let storeProducts = try await Product.request(with: Set(productIdToEmoji.keys))
今までのSKProductRequestなどを使って、たくさんの処理を書いていたところ、取得自体は、課金アイテムのIDのSetを渡すだけで、1行で取得することができます。
結果は、Product
の配列として、取得できます。
この後は、Product
のtype
プロパティを使い、消耗型、非消耗型、サブスクリプションなどを振り分けることができます。
サンプルコードでは以下のように書かれています。
for product in storeProducts {
switch product.type {
case .consumable:
newFuel.append(product)
case .nonConsumable:
newCars.append(product)
case .autoRenewable:
newSubscriptions.append(product)
default:
//Ignore this product.
print("Unknown product")
}
}
課金アイテムの購入処理
let result = try await product.purchase()
購入処理自体も1行で書くことができます。
今まので書き方だと、SKPaymentTransactionObserver
などを使って、かなりわかりにくい実装をする必要がありましたが、だいぶシンプルになりました。
このResult
は、VerificationResult
というenumになります。
AppStoreにより署名され、その後StoreKitによる検証が実行され、それで問題がなければ、.verified
が返り、なにかしらの理由で失敗した場合は、unverified
が返ります。
サンプルアプリでは、このenumをcheckVerified
という処理を通して、Transaction
を取得します。
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T
//Check if the transaction passes StoreKit verification.
switch result {
case .unverified:
//StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
throw StoreError.failedVerification
case .verified(let safe):
//If the transaction is verified, unwrap and return it.
return safe
}
}
適切に購入処理が終了したら、Transaction
を終了します。
await transaction.finish()
Transactionの監視
上記の課金処理が即座に終了しない場合などの場合、Transactionのステータスが変更された通知を受けたい場合があると思います。
WWDCのセッションでは、課金時に親の許可が必要な場合が例として紹介されていて、その場合「①課金処理を開始」→「②親の端末で親がその課金を承認」→「③実際に課金処理を実行」というフローになるため、この監視処理が必要になります。
サンプルアプリでは、以下のように書かれています。
func listenForTransactions() -> Task.Handle<Void, Error> {
return detach {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
for await result in Transaction.listener {
do {
let transaction = try self.checkVerified(result)
//Deliver content to the user.
await self.updatePurchasedIdentifiers(transaction)
//Always finish a transaction.
await transaction.finish()
} catch {
//StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
上記の例の場合、「②親の端末で親がその課金を承認」をした後に、上記のlistenerの中の処理がよばれます。
その中の処理は既述の購入時の処理と同じです。
なおここで書かれているTask
やdetach
などは、async/await関連でiOS15から使える新しい表現で、ここでの説明は省略します(自分もまだしっかり理解できていないので、これからキャッチアップします)
ちなみに「②親の端末で親がその課金を承認」もXcode上で操作することで、シミュレートできます。
サンプルアプリでProduct.storekit
ファイルを開き、上部のメニューの「Editor」→「Enable Ask to Buy」を選択します。
その後に課金アイテムを購入すると、キャプチャのようなアラートが表示されるので、「Ask」を選択します。
その後、Xcodeで以下の赤い丸の箇所をクリックすると、Transactionの履歴を確認できます。
そこから今回の課金アイテムのTransactionを選択して、赤丸の箇所をクリックすると、課金が承認されて、アプリのUIにも購入したことが反映されます。
なおこの画面便利で、ここでTransactionを削除すると、アプリ側でも反映されるので、検証する時にとても役に立つDebugツールです。
課金アイテムの購入状況を確認する
次に課金アイテムの購入状況を確認する実装を記載します。
サンプルアプリでは、そのアプリが課金済みかの確認をする関数を作成しています。
func isPurchased(_ productIdentifier: String) async throws -> Bool {
//Get the most recent transaction receipt for this `productIdentifier`.
guard let result = await Transaction.latest(for: productIdentifier) else {
//If there is no latest transaction, the product has not been purchased.
return false
}
let transaction = try checkVerified(result)
//Ignore revoked transactions, they're no longer purchased.
//For subscriptions, a user can upgrade in the middle of their subscription period. The lower service
//tier will then have the `isUpgraded` flag set and there will be a new transaction for the higher service
//tier. Ignore the lower service tier transactions which have been upgraded.
return transaction.revocationDate == nil && !transaction.isUpgraded
}
まずTransaction.latest(for: {課金アイテムのID})
でその課金アイテムの最新のTransactionを取得します。nilにならない場合はすでに購入済みということになります。
一番最後の transaction.revocationDate == nil && !transaction.isUpgraded
については、revocationDate(失効日)
がnil、つまりその課金アイテムが返金等で失効しておらず、サブスクリプションの場合は上位のアイテムにupgradeされていないか: isUpgraded
(購入しているものの中で最上位のものを購入済みとしたいため)を確認しています。
このサンプルアプリのデモアプリでは、この関数を使用して、購入済みの場合は、チェックマーク✔️を表示するようにしています。
なおTransactionの履歴は全てのデバイスでリアルタイムに同期されるそうなので、複数デバイスで利用している場合でも利用可能です。
サブスクリプションのステータスを確認する
最後にサブスクリプションのステータスの取得方法を確認します。
具体的には、サブスクリプションのグループ(サンプルアプリだとStandard, Premium, Proの3種類のアイテムが同じグループとして登録されている)の中で、どのアイテムをユーザーが購読しているかなどを取得できます。
サンプルアプリでは、SubscriptionView.swift
に記載があります。
guard let product = store.subscriptions.first,
let statuses = try await product.subscription?.status else {
return
}
上記でサブスクリプショングループのステータスを取得できます。
グループのステータスを取得するので、そのグループに所属しているどのサブスクリプションアイテムからでもステータスは取得できます。
サンプルアプリでは、storeにあるサブスクリプションのアイテムの先頭のものを取得しています。
ステータスはProduct.Subscription.Status
の配列で取得することができます。
配列の理由は自身のステータスに加え、ファミリーシェアなどで他のステータスを取得する場合もあるためだそうです(ここはしっかり理解できてないです)
for status in statuses {
switch status.state {
case .expired, .revoked:
continue
default:
let renewalInfo = try store.checkVerified(status.renewalInfo)
guard let newSubscription = store.subscriptions.first(where: { $0.id == renewalInfo.currentProductID }) else {
continue
}
guard let currentProduct = highestProduct else {
highestStatus = status
highestProduct = newSubscription
continue
}
let highestTier = store.tier(for: currentProduct.id)
let newTier = store.tier(for: renewalInfo.currentProductID)
if newTier > highestTier {
highestStatus = status
highestProduct = newSubscription
}
}
}
status = highestStatus
currentSubscription = highestProduct
ここで有効なサブスクリプションで、かつ現在購読しているサブスクリプションよりもハイグレードなアイテム(このアプリではグレードのことをTierと記述しています)の場合、表示を更新する処理を実装しています。renewalInfo
は、Product.Subscription.RenewalInfo
というstructでProductIDや失効理由、指定月に自動更新されるかなどの情報を取得することができます。
まとめ
今回はWWDC2021で新しく発表されたStoreKit2
についてまとめました。
今の会社に入ってから課金周りの処理を触るようになり、とてもわかりにくいなと感じでいましたが、このStoreKit2
の書き方は、async/awaitのキャッチアップが必要ではありますが、可読性や実装のしやすさという観点では大きく向上したと思います。
またサブスクリプションについてアプリ内でできることが増えたため。開発者としてもiOS15以上限定ですが、サブスクリプションの機能をより実装しやすくなり、またユーザーとしても、アプリ内でサブスクリプションの管理ができるようになれば、利便性が大きく上がると思います。
ここに記述したこと以外にもできることが大きく増えていそうなので、これから少しずつ調べて、試しに自分で実装してみるのも面白そうだなと思いました。
サンプルアプリで1通りの実装例が書かれているので、それを参考にすれば、サクッと実装できそうな気がします。
加えて、今回のStoreKit2
でも大きく使われているasync/await
は今後さらに色々な場面で使われそうな記法なので、今後そちらのキャッチアップもしていきたいと思います。
参考情報
- Meet StoreKit 2(https://developer.apple.com/videos/play/wwdc2021/10192/)
- iOSアプリ内でそれぞれのサブスクの管理や返金が可能に、アップルがStoreKit 2を発表(https://jp.techcrunch.com/2021/06/11/2021-06-10-apples-storekit-2-simplifies-app-store-subscriptions-and-refunds-by-making-them-accessible-inside-apps/)