【Swift】超シンプルな課金の実装

難しいと感じる課金の実装をシンプルにまとめてみました。
コピペして実装して頂くととっても簡単にできます。必要に応じて適宜カスタムしつつ、活用してもらえたら嬉しいです。

##### 実装全体
[StoreManager.swift](https://github.com/asa08/Purchase-template/blob/main/StoreManager.swift)

## 解説
```swift
class StoreManager: NSObject, SKPaymentTransactionObserver {
    static var sharedStore = StoreManager()
    var products: [SKProduct] = []

    // product idの一覧を定義する
    let productsIdentifiers: Set<String> = []

    // AppDelegateや課金処理前に呼び出してproduct一覧を取得する
    static func setup() {
        sharedStore.validateProductsIdentifiersWithRequest()
    }
    
    // product情報をStoreから取得
    private func validateProductsIdentifiersWithRequest() {
        let request = SKProductsRequest(productIdentifiers: productsIdentifiers)
        request.delegate = self
        request.start()
    }
    ...
}
```

### Product情報の取得
- App Store Connectで作成した課金アイテムの`製品ID`を定義します。

```swift
// productIdの一覧を定義する
// 製品IDがsample.idの場合
let productsIdentifiers: Set<String> = ["sample.id"]
```

- 定義した`productsIdentifiers`を元にProduct情報の一覧を取得します。

```swift
// product情報をStoreから取得
private func validateProductsIdentifiersWithRequest() {
    // 取得のリクエスト
    let request = SKProductsRequest(productIdentifiers: productsIdentifiers)
    request.delegate = self
    request.start()
}

// 取得処理の結果は`SKProductsRequestDelegate`に通知される
extension StoreManager: SKProductsRequestDelegate {
    // product情報の取得完了
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        products = response.products
    }
}
```
### 購入リクエスト
```swift
// 購入
func purchaseProduct(_ productIdentifier: String) {
    // productIdentifierに該当するproduct情報があるかチェック
    guard let product = productForIdentifiers(productIdentifier) else { return }
    // 購入リクエスト
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
}

// 該当のproduct情報はproductsに存在するか確認
private func productForIdentifiers(_ productIdentifier: String) -> SKProduct? {
    return products.filter({ (product: SKProduct) -> Bool in
        return product.productIdentifier == productIdentifier
    }).first
}
```

### 購入リクエストのTransaction
```swift
// transactionsが変わるたびに呼ばれる
// Transactionの状態により処理したい内容を記述する
// トランザクションを終了すると消耗型のレシートは消失し、そのTransactionは復元できないので注意
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing:
            // キューに追加された
        case .purchased:
            // 購入が完了
        case .restored:
            // 購入履歴から復元が完了
        case .deferred:
            // 購入処理は保留されており、承認まち
        case .failed:
            // キューに追加される前にリクエストが失敗
        default:
            break
        }
    }
}
```

## Transactionとレシートについて
課金が完了するとAppleのサーバーにレシートが生成されます。
レシートを検証する場合、Transactionを閉じてしまうと、消耗型のレシートは削除され所得できなくなるので注意が必要です。
サブスクのレシートなどは、Transactionを閉じてもずっと取得できるので、レシートを検証してデータを作成する場合は多重に作成してしまわないように注意してください。

## .deferredとは？
「Ask to Buy」が有効になっているアカウントで購入しようとしたとき、購入が保留され管理者に許可を求める通知が届きます。その通知が許可されるまでTransactionの状態は`.deferred`となっています。
これは[ファミリー共有](https://www.apple.com/jp/family-sharing/)の機能によるものです。


##### 関連記事
[【iOS】iOS14でSand Boxアカウントの場所が変わった](https://qiita.com/_asa08_/items/be21abfdb94227c58381)