iOS17以降のアプリ内課金の最小実装方法

# はじめに
広告非表示の実装をしたく非消耗型のアプリ内課金実装をしました。

iOS17以降で使える`ProductView`を使いながらミニマムでアプリ内課金を実装する方法としてまとめます。

# 環境
Xcode 15.1
iOS17 ~ サポート


# 内容

ざっくりと以下の手順でアプリ内課金の実装をしていきます。
- AppStoreConnectのアプリ内課金設定
- アプリ内課金の実装
- 審査提出


### 1. AppStoreConnectから有料アプリの契約を確認する

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/872d15fc-687e-a1ff-c92c-0ff9e73e61fa.png">

ステータスがアクティブになっていなかったので、口座情報と納税フォームを設定していきます。

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/2a3f32f6-033f-d343-24d9-9dce2a9fef30.png">

問題なく設定できたことが確認されるとステータスがアクティブに変わります。

### 2. AppStoreConnectでアプリ内課金の設定をする

AppStoreConnectの対象アプリのアプリ内課金画面から作成します。
特に難しい項目はないので必須項目を順に埋めていきます。

- 種類・参照名・製品IDを入力しアプリ内課金を作成

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/7b22b223-b901-91ed-93a9-f4a06d8428f2.png">

種類は広告の非表示機能のため、非消耗型を選択しました。

![スクリーンショット 2024-02-18 16.21.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/5be95652-edea-679b-c04f-185b6e419979.png)


>参照名はApp Store Connectおよび「売上とトレンド」のレポートで使用されます。App Storeには表示されません。名前は最大 64 文字です。

>レポートに使用される固有な英数字のIDです。ある製品に対して使用した製品IDは、その製品が削除されても再度使用することはできません。

- アプリ内課金の価格を設定

好きな価格を設定します。

<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/063d7507-ca57-2f29-7f4a-8a1f86ac9eb3.png">

- App Storeのローカリゼーションを設定

ローカリゼーション、表示名と説明を入力します。
ここの表示名と説明はそのまま課金画面に表示されるため注意した方が良いかもです。

<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/4bd6a817-430f-4bca-ca25-eba0710d08c1.png">
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/eeef5276-6a98-c128-23dc-eaa45c2f2f1a.png">

- 審査に関する情報を設定

シミュレーターで表示した課金画面のスクリーンショットを貼り、審査メモには広告非表示のためのアプリ内課金ですと記載しました。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/c0fd576b-7fcd-4a19-c652-6954fe9c97dd.png">

>スクリーンショットは審査の目的においてのみ使用します。App Storeに表示されることはありません。

https://developer.apple.com/help/app-store-connect/reference/in-app-purchase-information


### 3. Xcodeプロジェクトの設定をする

Xcodeの`Signing&Capabilities`から`In-App Purchase`を追加します。

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/5b0fecb3-1a70-b03d-5eb9-660540da4a9a.png">

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/394b717c-34f9-64c2-0c5d-a5aafe2118a7.png">

### 4. アプリ内課金画面を実装する

iOS17以上ではSwifUIの課金画面をサクッと実装できるようになりました。

広告非表示の一つの課金画面を表示するだけであれば、`ProductView`に製品IDを渡すだけでUIを作ることができます。



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

```swift
import SwiftUI
import StoreKit

struct IAPView: View {
    var body: some View {
        ProductView(id: "AppStoreConnectで設定したIDを入力")
            .productViewStyle(.large)
            .onInAppPurchaseCompletion { product, result in
                if case .success(.success(_)) = result {
                    // 課金が成功した場合の処理
                } else {
                    // 課金が失敗した場合の処理
                }
            }
    }
}
```

#### 課金処理

`onInAppPurchaseCompletion`を使うことで課金処理の結果に合わせた処理を実装できます。

[`
func onInAppPurchaseCompletion(perform action: ((Product, Result<Product.PurchaseResult, any Error>) async -> ())?) -> some View
`](https://developer.apple.com/documentation/storekit/storeview/4203485-oninapppurchasecompletion)

結果 [`Product.PurchaseResult`](https://developer.apple.com/documentation/storekit/product/purchaseresult
)は以下の通り

- `case success(VerificationResult<Transaction>)` → 成功
- `case userCancelled` →　ユーザーによるキャンセル
- `case pending` →　ユーザーによるアクションが必要

<img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/0868472d-2a25-b965-9cb9-f7457f2f95d9.png"><img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/f2f584ef-6abf-913b-04fd-30365dffaffc.png"><img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/1ce22b2f-cfb8-6c59-1a5c-4cdd80abe06d.png">

購入完了と購入済みのアラートはデフォルトで表示されました。

<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/ff714a01-7224-bc1a-a179-4bb0a841314e.png"><img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/47b36b44-26ba-8564-649b-5cd8f75b8626.png">

### 課金UI

表示するUIは3パターンから選ぶことができます。

`func productViewStyle(_ style: some ProductViewStyle) -> some View`

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

`.large`, `.regular`, `.compact` or `.automatic`

<img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/d3c2b535-4463-d16f-873a-41e377f8ef0f.png"><img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/a62f80c5-ed50-5340-5873-cca90dcd566b.png"><img width="150" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/804df77a-676f-8126-d24e-d1d698a72791.png">


尚、設定画面側で`ProductView`の表示を`.presentationDetents([.height(200)])`としています

```swift
        HStack {
            Text("広告非表示")
            Spacer()
            if hideAd {
                Text("購入済み")
            } else {
                Button {
                    showIAPView = true
                } label: {
                    Text("購入する")
                }
            }
        }
        .sheet(isPresented: $showIAPView) {
            IAPView(hideAd: $hideAd)
                .presentationDetents([.height(200)])
        }
```

課金画面のUI実装は他にもできることがたくさんありますが、今回はミニマム実装でやりたいことが満たせそうだったのでここまでに。

と思い審査に一度提出してみたところ、復元ができないのでダメよと怒られてしまいました。当然ですね。うっかりしてました。

### 復元処理

復元の処理は`AppStore.sync()`でAppStoreと同期

https://developer.apple.com/documentation/storekit/appstore/3791906-sync

`Transaction.currentEntitlement(for: "AppStoreConnectで設定したIDを入力")`で課金情報を取得します。

https://developer.apple.com/documentation/storekit/transaction/3851204-currententitlements/

```diff_swift
import SwiftUI
import StoreKit

struct IAPView: View {
    var body: some View {
+       VStack(spacing: 20) {
            ProductView(id: "AppStoreConnectで設定したIDを入力")
                .productViewStyle(.large)
                .onInAppPurchaseCompletion { product, result in
                    if case .success(.success(_)) = result {
                        // 課金が成功した場合の処理
                    } else {
                        // 課金が失敗した場合の処理
                    }
                }
+           Button("復元はこちら") {
+               Task {
+                   do {
+                       try await AppStore.sync()
+                       let verificationResult = await Transaction.currentEntitlement(for: "AppStoreConnectで設定したIDを入力")
+                       if case .verified = verificationResult {
+                           // 復元が成功した場合の処理 
+                       } else {
+                           // 復元が失敗した場合の処理
+                       }
+                   } catch {
+                       // 復元が失敗した場合の処理
+                   }
+               }
+           }
        }
    }
}
```

ここまでがミニマム実装です！

### 5. 審査に提出

>最初のアプリ内課金は、新しいアプリバージョンと共に提出する必要があります。アプリ内課金を作成したら、アプリバージョンのページの「アプリ内課金とサブスクリプション」セクションからそのアプリ内課金を選択してください。その後、そのアプリバージョンをApp Reviewに提出してください。

アプリの申請画面にアプリ内課金を追加するフィールドが表示されます。
作成したアプリ内課金を追加して通常通り審査に提出します。

![スクリーンショット 2024-02-17 23.55.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/c39673bb-4575-4285-a03a-64df310739d3.png)

一度は復元の実装漏れでリジェクトされたものの、再提出して無事審査に通りました🎉

# おわりに

今回は新規の個人アプリだったのでiOS17~サポートとしました。`ProductView`が使えたことで、かなり実装が楽だった印象です。
サブスク課金なども実装してみようかなと思いました。

# 参考
https://qiita.com/Masataka-n/items/6f98a5a9fee7b28ccd1f

https://ulog.sugiy.com/wwdc-2023-storekit2-swiftui/

https://zenn.dev/naoya_maeda/articles/55a467c912e9c2