消耗型課金をStoreKit2で実装してみる

## はじめに

非消耗型App内課金やサブスクリプションの記事が多く、消耗型課金の情報がなかったのでまとめてみました。間違いがあればご教示いただけますと幸いです。


## StoreKit2とは

iOS15から利用できる新たな課金ライブラリです。
レシート検証不要で、async/awaitの追加により簡単、簡潔なコードになります。


## 準備
下記を参考にAppStoreにて課金の準備を行ってください。
テスト課金を行おうとしても全くできないことがありましたが口座登録をしていないというミスでした。
準備は大切です。
[AppStoreConnectの課金準備](https://developer.apple.com/jp/help/app-store-connect/configure-in-app-purchase-settings/overview-for-configuring-in-app-purchases/)
[消耗型 App 内課金や非消耗型 App 内課金の作成](https://developer.apple.com/jp/help/app-store-connect/manage-in-app-purchases/create-consumable-or-non-consumable-in-app-purchases)

## 実際のコードをみてみる

### 課金アイテムを取得する
この1行だけで取得できます。簡単です。
```Swift:
    private func loadProducts() async {
        do {
            self.products = try await Product.products(for: StoreProduct.allIdentifiers)
        } catch {
            print("¥product request失敗: \(error)")
        }
    }
```

### 課金アイテムを購入する

購入処理は1行で終わります。簡単ですね〜
```Swift:
let result: Product.PurchaseResult = try await product.purchase()
```

実際のハンドリングです。購入後に検証を行い問題がなければ独自のアプリそれぞれの消耗アイテムを付与します。付与に成功したらtransactionをfinishして完了です。
```Swift:
    func purchase(_ product: Product) async throws {
        let result: Product.PurchaseResult = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            // 購入したコンテンツを提供後にトランザクションを完了させる
            try await addCoin(userId: "")
            await transaction.finish()
        case .pending:
            // SCA (Strong Customer Authentication)等 ユーザーのアクション待ち
            // Transaction.updatesで購入コンテンツを提供する
            break
        case .userCancelled:
            break
        @unknown default:
            break
        }
    }
```

### transactionをfinishできなかった際の復元
消耗型アイテムの付与に失敗しtransactionをfinishできなかった場合はtransaction監視し、再度検証やaddCoinを行います。

下記でfinishできなかったtransactionを取得できます。

```Swift:
for await result in Transaction.unfinished {
```

```Swift:
    func listenForTransactions() -> Task<Void, Error> {
        return Task(priority: .background) {
            for await result in Transaction.unfinished {
                do {
                    let transaction = try self.checkVerified(result)
                    try await addCoin(userId: "")
                    await transaction.finish()
                } catch {
                    print("Transaction検証失敗: \(error)")
                }
            }
        }
```
サブスクリプションや非消耗型アイテムの場合はunfinished以外の変数を用います。

https://developer.apple.com/documentation/storekit/transaction


## すべてのコード
```Swift:
@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 初期化
        PurchaseManager.shared.initialize()
        return true
    }
}
```


```Swift:
import StoreKit

enum StoreError: Error {
    case failedVerification
}

final class PurchaseManager {

    static let shared = PurchaseManager()

    private(set) var products: [Product] = []
    private var updateListenerTask: Task<Void, Error>?

    deinit {
        updateListenerTask?.cancel()
    }

    func initialize() {
        updateListenerTask = listenForTransactions()
        Task {
            // 初期化時にプロダクト(AppStoreに登録した全課金アイテム)を取得
            await loadProducts()
        }
    }
}

extension PurchaseManager {

    /// 課金アイテム情報を取得
    private func loadProducts() async {
        do {
            self.products = try await Product.products(for: StoreProduct.allIdentifiers)
        } catch {
            print("¥product request失敗: \(error)")
        }
    }

    /// 購入処理
    func purchase(_ product: Product) async throws {
        let result: Product.PurchaseResult = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            // 購入したコンテンツを提供後にトランザクションを完了させる
            try await addCoin(userId: "")
            await transaction.finish()
        case .pending:
            // SCA (Strong Customer Authentication)等 ユーザーのアクション待ち
            // Transaction.updatesで購入コンテンツを提供する
            break
        case .userCancelled:
            break
        @unknown default:
            break
        }
    }
}

private extension PurchaseManager {

    func addCoin(userId: String) async throws {
        // ユーザーにコインを付与する
    }

    // 検証が成功しているかチェック
    func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .unverified:
            throw StoreError.failedVerification
        case .verified(let safe):
            return safe
        }
    }

    // バックグラウンドでfinishを行えていない(コインの付与に失敗した)transactionを監視して付与に成功したらtransactionをfinishする
    func listenForTransactions() -> Task<Void, Error> {
        return Task(priority: .background) {
            for await result in Transaction.unfinished {
                do {
                    let transaction = try self.checkVerified(result)
                    try await addCoin(userId: "")
                    await transaction.finish()
                } catch {
                    print("Transaction検証失敗: \(error)")
                }
            }
        }
    }
}
```

```Swift:
    // 実際に購入する
    Task {
         let item1 = PurchaseManager.shared.products[0]
         try await PurchaseManager.shared.purchase(item1)
     }
```

## 終わりに
Storekitと比べてかなり分かりやすく簡潔になったと思います。
iOS15以上のプロジェクトであれば是非導入してみてください。