StoreKit2の変更点まとめ
StoreKit2についてまとめました。
従来のStoreKitからの変更点を主としており、アプリ内課金の実装方法についての説明は割愛しています。
Product
商品情報&購読処理を持つStruct
1メソッドで商品情報の取得が可能
let products: [Product] = try await Product.products(for: <# ProductIdの配列 #>)
Productには、下記のような欲しかった情報が網羅されている
-
type: Product.ProductType
- consumable, autoRenewableなど課金タイプのenum。複数の課金タイプを持つアプリで活用できる
-
displayPrice: String
- ローカライズ済みの価格表記(ex. ¥360, $4.99)
-
displayName: String
- 商品名
Purchase
- 購読処理をasync/awaitで実行できる
try await product.purchase()
- 旧StoreKit時代のようにPaymentQueueのdelegate内でfinishTransactionしなくてもよく、1メソッドで全ての処理を完結できる
- appAccountTokenをoptionとして含めることができる(後述)
// appAccountTokenをオプションに含めて購読リクエスト
let appAccountToken = <# Generate an app account token. #>
let purchaseResult = try await product.purchase(options: [
.appAccountToken(appAccountToken)
])
StoreKit2がレシート検証までやってくれる
- StoreKit2ではトランザクションにJWSペイロードが含まれ、レシート検証プロセスを通過してAppStoreによって署名済みの端末かどうか確認してくれる
- 結果は
product.purchase()
の返り値として返却される
https://developer.apple.com/videos/play/wwdc2021/10114/?time=685
購読時のサンプル
-
product.purchase()
の返り値はenumで返ってくる- .success(VerificationResult<Transaction>), .pending, .userCancelled
-
.success(let verificationResult)
のverificationResultからレシート検証の結果を取得できる- 購読結果が、
.verified
な場合、Transactionから購読結果の情報を取得でき、transaction.finish()
で購読を完了できる - 購読結果が、
.unverified
な場合は購読処理は成功したが、レシート検証が失敗したケースで、その検証エラーが返ってくる - 検証エラー一覧 https://developer.apple.com/documentation/storekit/verificationresult/verificationerror#3875816
- 購読結果が、
- throwされるエラーは
Product.PurchaseError
orStoreKitError
let result = try await product.purchase()
switch result {
case .success(let verificationResult):
switch verificationResult {
case .verified(let transaction):
// Give the user access to purchased content.
...
// Complete the transaction after providing
// the user access to the content.
// 購読処理の戻り値でトランザクションをfinish()できるの便利!
await transaction.finish()
case .unverified(let transaction, let verificationError):
// Handle unverified transactions based
// on your business model.
...
}
case .pending:
// The purchase requires action from the customer.
// If the transaction completes,
// it's available through Transaction.updates.
break
case .userCancelled:
// The user canceled the purchase.
break
@unknown default:
break
}
Transaction (旧SKPaymentTransaction)
前提として、
ユーザーの購読ステータスの変更, アプリ外での購読(AppStoreの設定画面など), pending状態の購読処理を監視するため、アプリ起動時にTransactionをlistenする必要がある。(旧SKPaymentTransaction同様)
Transaction.updates
にAsyncSequenceが流れるので、それをアプリ起動時にlistenする
Task(priority: .background)やTask.detachedでメインスレッド外で処理する
アプリ内で購読した時はTransactionが流れない
これが素晴らしい。以前はユーザーが購読した際も問答無用でSKPaymentQueueにTransactionが流れていたので考慮が必要だった
※購読時のTransactionはtry await product.purchase()
メソッドの戻り値で取得できる
// アプリ起動時に、AppDelegateやSwiftUIならRootなViewなどでこのclassを保持し、
// Transactionをlistenし続ける
final class TransactionObserver {
var updates: Task<Void, Never>? = nil
init() {
updates = newTransactionListenerTask()
}
deinit {
// Cancel the update handling task when you deinitialize the class.
updates?.cancel()
}
private func newTransactionListenerTask() -> Task<Void, Never> {
Task(priority: .background) {
for await verificationResult in Transaction.updates {
self.handle(updatedTransaction: verificationResult)
}
}
}
private func handle(updatedTransaction verificationResult: VerificationResult<Transaction>) {
guard case .verified(let transaction) = verificationResult else {
// Ignore unverified transactions.
return
}
if let revocationDate = transaction.revocationDate {
// Remove access to the product identified by transaction.productID.
// Transaction.revocationReason provides details about
// the revoked transaction.
<#...#>
} else if let expirationDate = transaction.expirationDate,
expirationDate < Date() {
// Do nothing, this subscription is expired.
return
} else if transaction.isUpgraded {
// Do nothing, there is an active transaction
// for a higher level of service.
return
} else {
// Provide access to the product identified by
// transaction.productID.
<#...#>
}
}
}
iOS16~
transaction.environment
からAppStoreの環境を判定できるようになった
Xcodeから実行した場合、サーバーのレシート検証をskip, コンバージョンを計測するコードをskipなどハンドリングが可能になっている
if transaction.environment == .xcode {
// storekit.configを使ってシミュレータでデバッグしている場合の処理
}
Subscription status
取引履歴の他にサブスクリプションのステータスを取得できるプロパティもProductに追加されている
Transaction同様、JWSで暗号化されておりセキュア
下記の情報が取得できる
- Latest transaction: 最新の購読情報
- Renewal state: 契約中、期限切れ、猶予期間かなど購読ステータス
- Renewal info: ステータス情報の他に自動更新しているproduct ID, 期限切れの理由など全ての情報
Product.SubscriptionInfo.RenewalState
を用いてStateに応じたコミュニケーションが可能
expired
なユーザーに向けて、割引オファーを出すなど
// Product.SubscriptionInfo.RenewalStateで判定できるステータス
public static let subscribed: Product.SubscriptionInfo.RenewalState
public static let expired: Product.SubscriptionInfo.RenewalState
public static let inBillingRetryPeriod: Product.SubscriptionInfo.RenewalState
public static let inGracePeriod: Product.SubscriptionInfo.RenewalState
public static let revoked: Product.SubscriptionInfo.RenewalState
リストア処理
restoreCompletedTransactionsのデリゲートメソッドに切り替わるリストア時の処理としてAppStore.sync()が用意されている
再認証(パスワード入力)が必要になった
"Transactionを用いれば通常呼び出す必要がない処理"としてドキュメントに記載されている
あくまでTransaction.updatesを購読してユーザーのステータスを更新することをリストアの主とし、強制的に復元させるサブとしてユーザー手動の復元ボタンを設置しておくのがベストプラクティス。
引用)
In regular operations, there’s no need to call sync(). StoreKit automatically keeps up to date transaction information and subscription status available to your app. When users reinstall your app or download it on a new device, the app automatically has all transactions available to it upon initial launch. There’s no need for users to ask your app to restore transactions — your app can immediately get the current entitlements using currentEntitlements and transaction history using all. For more information about transactions, see Transaction.
"(Transactionの自動同期で大半のケースがカバーされるので)あまり使うことはないが、ユーザーが別のAppleIDを使用した場合などに強制的に復元させるため、ベストプラクティスとして復元ボタンを置いた方が良い"
Implement proactive in-app purchase restore
App account token
Product.purchaseのオプションとして付与した場合、Transactionで購読履歴を取得したときに購読結果に同じappAccountTokenをStoreKit側から付与してくれる
AppのアカウントごとにappAccountTokenを保存しておくとアカウントごとの購読情報を管理するのに役立つ
iOS16~変更点
AppTransaction
- Transactionに関連するストアの情報
-
originalAppVersion: 初回購読した際のアプリのバージョン
などが取得できる
WWDC22の動画では、課金アプリ→フリーミアムモデルに変更したアプリのユーザーの課金ステータスをコントロールする例としてAppTransactionが使われていた
StoreKitへの新しいプロパティの追加
- Price locale
- Server environment
- Recent subscription start date
なんとこのプロパティはバックポートのサポートがあり、Xcode14でビルドすればiOS15, iPadOS15でも使える
ただ、iOS15のStoreKit Testingではダミーの値が返ってくるので注意(16以降のシミュレータを使えば良い)
ストア関連
SwiftUIにオファーコードの入力, ストアレビューAPIが追加になった
StoreKitメッセージAPI: 値上げの通知などアプリがフォアグラウンドになった際に表示できる
AppStore Sever APIのアップデート(WWDC22)
Get Transaction Historyのアップデート
- 下記のソート, フィルタをかけて取得できるようになった
- 更新日時
- product type, productID, Subscription groupID, Purchase date range, Family sharing status, Exclude revoked transactions
AppStore Server Notification V2
- AppStore Server -> アプリサーバーの疎通確認用のテストエンドポイントが生えた
- https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification
- 通知が来ない場合、通知ステータスを確認するエンドポイントも新設された
- https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status
2つのフィールドの追加
- environment: (sandbox or production)
- recentSubscriptionStartDate: 最新の購読日時
返金(Refund)
document
SwiftUI sampleTips
Transactionに処理が流れてくる状態をシミュレートしたい場合、storekit.configの"Enable Ask to Buy"機能を使うのが便利
(親の許可が出るまで課金できない状態 .pending
状態をシミュレートできます)
XcodeのTransaction ManagerでAsk to Buyの許可ができます
参考にした記事・動画
document
sample revenuecatの詳しい記事 WWDC memoWWDC21: Meet StoreKit 2
WWDC22: What's new with in-app purchase WWDC22: Implement proactive in-app purchase restore
Discussion