どこよりもわかりやすいiOS最強課金まとめ

[2020/1/9] Appleサーバー通知について新しい通知タイプが追加されたため追記と修正をしました。
[2020/11/19] App Store Connectのデザイン変更とAppleサーバー通知の仕様変更に対応しました。

# はじめに
わかりにくいiOSのアプリ内課金についてどの記事よりもわかりやすいものを目指して書きました。
実際に実装していて困ったことなどもまとめたので同じ状況で悩んでる人の助けになれば幸いです。
今現在まだ実装途中でして、まだ細かい部分など書ききれていない部分もありますが、都度更新していきたいと思います。
間違ってることなどあればご指摘くださいmm

## 課金について
### 課金の種類

|種類|例|説明|
|-|-|-|
|消耗型|ゲーム内でのスタミナやガチャを引くためのアイテムなど|一度使うとなくなり、再度購入が可能|
|非消耗型|広告の非表示や使える機能の拡張など|一度の購入だけで無制限に使用できる|　
|自動更新サブスクリプション|月間サービスのクラウドストレージや週刊雑誌のサブスクリプションなど、サービスや定期的にアップデートされるコンテンツ|ユーザーが解約するまで定期的に課金される　|
|非更新サブスクリプション|ストリーミングコンテンツのシーズンパスなど|自動的に更新されないため、ユーザー自身が毎回更新する必要がある|

# 準備
## 1. 契約 / 税金 / 口座情報の設定
App内課金を提供するには、有料App契約に署名し、税金および口座情報を設定する必要があります。
App Store Connectの「契約 / 税金 / 口座情報」のページで各種情報を入力をしてください。

<img width="1097" alt="スクリーンショット 2019-08-30 16.23.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/00ea8bc8-7f65-5cc0-c52c-4fe3b391d07a.png">

※ダウンロードが無料のアプリであっても、アプリ内課金を提供する場合、上の画像の有料Appのステータスがアクティブになっている必要があります。
 アクティブでない場合、課金アイテムの取得などでエラーになります。
 また直接ここが関係しているかは定かではないですが、課金アイテムの追加で自動更新サブスクリプションの項目が表示されませんでした。

## 2. Appの追加
App Store Connectでアプリのページを作成します。
「マイ App」で新規Appを選択して追加します。

## 3. 課金アイテムの追加
アプリのページを作成したら課金アイテムを追加します。
課金アイテムはアプリページ内の「App内課金」→ 「管理」から作成できます。

![スクリーンショット_2020-11-19_15_26_25-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/e9227dac-162e-587d-fa07-84e86374064a.png)

⊕ボタンを押すと下の画像のポップアップが出るので作成したい種類のコンテンツを選択して作成してください。
<img width="711" alt="スクリーンショット 2019-08-30 16.29.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/bec351b0-38bc-4547-7242-6335a4763956.png">

自動更新サブスクリプションは最初に参照名と製品IDの入力とを求められます。
参照名は後ほど変更できますが、製品IDは変更できません。
<img width="600" alt="スクリーンショット 2019-08-30 18.01.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/684a709e-6304-9453-5a70-841325749298.png">

**参照名**
App Store Connect上のみで表示されます。
(例) 
自動更新型: 〇〇プラン(3ヶ月)
消耗型: 〇〇石(10個)
など


**製品ID**
アプリから課金アイテムを取得する際に必要になる固有のIDです。
アプリのBundle Identifierを先頭に付けてproductIDを設定します。

決済完了後、コンテンツを付与するときに課金の種類によって処理が分かれると思うので、productIDでどのタイプの課金アイテムなのか判別できるようにします。
後からの変更ができないのでチームで開発する際は命名規則を決めると良さそうです。
(例) 
自動更新型: com.hoge.application. **autoSubscription** .plan1
消耗型: com.hoge.application. **consumable** .item1
など

また自動更新型の場合、どのサブスクリプショングループに追加するかを選択します。
サブスクリプショングループがまだ無い場合は作成してください。

<img width="601" alt="スクリーンショット 2019-08-30 19.02.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/fa9ef162-64a1-731c-6baa-bcb60982957d.png">

**サブスクリプショングループとは**
>提供するサブスクリプションはすべて、1つのサブスクリプショングループに割り当てる必要があります。サブスクリプショングループは、アクセスレベル、価格、期間が異なる複数のサブスクリプションで構成されているため、ユーザーが自分のニーズに最適なオプションを選択できるようになっています。ユーザーが1回に購入できるのはグループ内の1つのサブスクリプションのみであるため、ほとんどのAppでは、グループを1つだけ作成することがベストプラクティスです。これにより、ユーザーが複数のサブスクリプションを誤って購入してしまう事態を避けることができます。
ユーザーがAppで複数のサブスクリプションを購入できるようにする必要がある場合（ストリーミングAppで複数のチャンネルのサブスクリプションを提供する場合など）は、各サブスクリプションをそれぞれ異なるグループに追加することもできます。複数のグループでサブスクリプションを購入したユーザーには、サブスクリプションごとに請求が行われます。また、ユーザーがあるサブスクリプショングループから別のグループに移動した場合、サブスクリプションの更新日は変更され、有料サービスの日数もリセットされます。1つの有効なサブスクリプションのみ存在することが通常予想されるAppに、複数のサブスクリプショングループを設定することは推奨されません。

このサブスクリプショングループにもローカリゼーションの設定が必要です。

![スクリーンショット_2020-11-19_15_41_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/48c09f32-8c97-bf5c-c79c-e6a2ccb72195.png)


### 自動更新型
**サブスクリプション期間**
登録したサブスクリプションが自動更新されるまでの期間です。
設定できる期間は下記の通りです。

|期間|
|-|
|1週間|
|1ヶ月|
|2ヶ月|
|3ヶ月|
|6ヶ月|
|1年|

**サブスクリプション価格**
通貨を日本円(JPY)にして価格を選択して次へを押すと他のテリトリでの価格を自動計算してくれます。
テリトリごとに異なる価格を設定することも可能です。
<img width="751" alt="スクリーンショット 2019-08-30 19.32.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/795099d6-041f-f342-ae77-a6eef15b012d.png">
<img width="752" alt="スクリーンショット 2019-08-30 19.33.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/b4c6ce2c-4859-7874-7985-4e2a5a68b090.png">
無料トライアルやお試し価格などの設定もできます。
トライアル期間が終わると自動的に通常のサブスクリプション価格が請求されます。


### 自動更新型以外

**価格**
USDでの表記になっているので「その他の通貨」を押して出てくるポップアップを参考に適切な価格を設定しましょう。

### 共通
**App Store 情報**
App Storeに表示するApp内課金の表示名と説明を設定します。
<img width="730" alt="スクリーンショット 2019-08-30 19.43.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/dfbe6713-ab10-cad9-863b-f153b0a0cf5e.png">
この情報はAppStoreに表示されるものになるのでユーザーに分かりやすい表示名と説明を設定しましょう。

**App Store プロモーション（オプション）**
App 内課金を App Store でのプロモーションに使用する場合は1024x1024ピクセルのプロモーション用イメージを追加します。
プロモーション用の情報なので必須ではないです。

**審査に関する情報**
課金アイテムにも審査があり、その際に必要な情報です。
実際のプロダクト一覧画面や購入画面のスクリーンショットと
審査の際に必要なメモ(アプリにログインできるアカウント情報や、課金画面表示への案内など)を記載します。

※開発時はスクリーンショット、メモ共にダミーのものでも大丈夫です。申請するときに差し替えてください。

上記のApp Store プロモーション以外の設定が完了すると下の画像の用に課金アイテムのステータスが「送信準備完了」となります。
(情報が不足していると「メタデータが不足」になります。自動更新型の場合サブスクリプショングループの設定も必要です。）
<img width="1003" alt="スクリーンショット 2019-08-30 20.22.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/99f2c443-27ba-ca29-6674-f1f7d29366d9.png">
このステータスが送信準備完了の状態にならないとアプリから課金アイテムの取得をする際にエラーになってしまうので注意です。

また、自動更新型の課金を提供する場合は決済後のレシートの検証で **App 用共有シークレット** というものが必要になるので上の画像右上にあるApp 用共有シークレットから生成しておきましょう。(生成後は同じところから確認、再生成することができます)

<img width="801" alt="スクリーンショット 2019-11-19 15.56.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/58f9ce18-ae54-e0f7-953a-ea57e991e6de.png">


## 4. Xcode側での設定
Xcodeではプロジェクトの設定から「TARGETS」 → 「Signing & Capabilities」で **In-App Purchase** を追加します。
<img width="684" alt="スクリーンショット 2019-11-18 18.34.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/7eb89b0f-d43a-9d26-6211-e582fd57ae5c.png">


# 課金アイテムの取得
AppStoreから課金アイテムを取得する
取得と言いつつも、予めアプリ側で課金アイテムのproductIDを知っている必要があります。
AppStoreにproductIDで問い合わせることでその課金アイテムの価格などの情報と販売可能な状態かがわかります。

このproductIDをアプリ側で管理する方法は2通りあります。

**・Appバンドル内へのプロダクトIDを埋め込む**
**・自前のサーバーから取得**


広告の削除や機能の有効化など固定されている場合は、Appバンドルにリストを埋め込みでも良いですが、
課金アイテムを増やすことが多い場合は、アプリの更新をせずすぐに反映できるようにサーバーから取得するようにしておくのが良いです。

||Appバンドルに埋め込み|サーバーからフェッチ|
|-|-|-|
|購入目的| 機能のアンロック| コンテンツ配信|
|プロダクトのリストの変更|Appの更新時に可能|いつでも可能|
|サーバの必要性|不可|可|

公式ドキュメント: https://developer.apple.com/jp/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/ShowUI.html


# レシート検証について
レシートの検証とは課金アイテムを購入した際に発行されるレシートをAppleに問い合わせることで、
不正な購入や意図しない購入でないかを検証するものです。
また、自動更新型の状態(継続、停止など)を確認する際にも使います。

### 検証の方法
レシート検証の方法は2つあります。
・ ローカルでの検証
・ AppStoreを使用した検証

基本的にはAppStoreを使用した検証をおすすめします。

ローカルでの検証についてはこちらの記事をご覧ください
[iOSで課金のレシートをローカルで判定する方法](https://qiita.com/codelynx/items/a88805b47b7e40ef3782)

### AppStoreを使用した検証

### リクエストURL

|環境|URL|
|-|-|
|production|https://buy.itunes.apple.com/verifyReceipt|
|sandbox|https://sandbox.itunes.apple.com/verifyReceipt| 

### リクエストBody
|キー|値|
|-|-|
|receipt-data|base64 でエンコードされたレシートデータ|
|password|自動更新購読が含まれているレシートにのみ使用。アプリケーションの共有シークレット（16 進数文字列）|
|exclude-old-transactions|自動更新または非更新購読が含まれる iOS7 スタイルの App レシートの場合にのみ使用します。値がtrueの場合、応答には購読の最新の更新トランザクションのみが含まれます。|


### ステータスコード
|状態コード|説明|
|-|-|
|21000|App Storeは、指定されたJSONオブジェクトを読み取ることができませんでした。|
|21002|receipt-dataプロパティのデータの形式が正しくないか、欠落しています。|
|21003|領収書を認証できませんでした。|
|21004|指定した共有秘密は、アカウントのファイルにある共有秘密と一致しません。|
|21005|レシートサーバーは現在利用できません。|
|21006|この領収書は有効ですが、サブスクリプションの有効期限が切れています。このステータスコードがサーバーに返されると、レシートデータもデコードされ、応答の一部として返されます。 <br>自動更新可能なサブスクリプションのiOS 6スタイルのトランザクションレシートに対してのみ返されます。|
|21007|このレシートはテスト環境からのものですが、検証のために実稼働環境に送信されました。代わりにテスト環境に送信してください。|
|21008|この領収書は実稼働環境からのものですが、検証のためにテスト環境に送信されました。代わりに本番環境に送信してください。|
|21010|この領収書は承認されませんでした。これは、購入したことがない場合と同じように扱います。|
|21100-21199|内部データアクセスエラー。|



まず、上の表にあるproductionのURLに問い合わせ、statusが `21007` で返ってきたらsandboxのURLに再度問い合わせるようにします。

### レスポンス
このようなレスポンスが返ってきます。

```typescript

{
	"status": 0,
	"environment": "Sandbox",
	"receipt": {
		"receipt_type": "ProductionSandbox",
		"adam_id": 0,
		"app_item_id": 0,
		"bundle_id": " ",
		"application_version": "1",
		"download_id": 0,
		"version_external_identifier": 0,
		"receipt_creation_date": "2019-11-12 08:55:19 Etc/GMT",
		"receipt_creation_date_ms": "1573548919000",
		"receipt_creation_date_pst": "2019-11-12 00:55:19 America/Los_Angeles",
		"request_date": "2019-11-18 12:58:25 Etc/GMT",
		"request_date_ms": "1574081905053",
		"request_date_pst": "2019-11-18 04:58:25 America/Los_Angeles",
		"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
		"original_purchase_date_ms": "1375340400000",
		"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
		"original_application_version": "1.0",
		"in_app": [
			{
				"quantity": "1",
				"product_id": " ",
				"transaction_id": "1000000590999315",
				"original_transaction_id": "1000000590999315",
				"purchase_date": "2019-11-12 08:55:18 Etc/GMT",
				"purchase_date_ms": "1573548918000",
				"purchase_date_pst": "2019-11-12 00:55:18 America/Los_Angeles",
				"original_purchase_date": "2019-11-12 08:55:19 Etc/GMT",
				"original_purchase_date_ms": "1573548919000",
				"original_purchase_date_pst": "2019-11-12 00:55:19 America/Los_Angeles",
				"expires_date": "2019-11-12 08:58:18 Etc/GMT",
				"expires_date_ms": "1573549098000",
				"expires_date_pst": "2019-11-12 00:58:18 America/Los_Angeles",
				"web_order_line_item_id": "1000000048178827",
				"is_trial_period": "false",
				"is_in_intro_offer_period": "false"
			}
		]
	},
	"latest_receipt_info": [
		{
			"quantity": "1",
			"product_id": " ",
			"transaction_id": "1000000590999315",
			"original_transaction_id": "1000000590999315",
			"purchase_date": "2019-11-12 08:55:18 Etc/GMT",
			"purchase_date_ms": "1573548918000",
			"purchase_date_pst": "2019-11-12 00:55:18 America/Los_Angeles",
			"original_purchase_date": "2019-11-12 08:55:19 Etc/GMT",
			"original_purchase_date_ms": "1573548919000",
			"original_purchase_date_pst": "2019-11-12 00:55:19 America/Los_Angeles",
			"expires_date": "2019-11-12 08:58:18 Etc/GMT",
			"expires_date_ms": "1573549098000",
			"expires_date_pst": "2019-11-12 00:58:18 America/Los_Angeles",
			"web_order_line_item_id": "1000000048178827",
			"is_trial_period": "false",
			"is_in_intro_offer_period": "false",
			"subscription_group_identifier": "20567235"
		}
	],
	"latest_receipt": " ",
	"pending_renewal_info": [
		{
			"expiration_intent": "1",
			"auto_renew_product_id": " ",
			"original_transaction_id": "1000000590999315",
			"is_in_billing_retry_period": "0",
			"product_id": " ",
			"auto_renew_status": "0"
		}
	]
}
```
各プロパティの詳細については下記の公式ページでご確認ください。

[receipt](https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt)

[in_app](https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt/in_app)



AppStoreを使用した検証はアプリ内から直接レシート検証のAPIを叩くこともできるのですが、推奨されていません。
↓公式ドキュメントより
> 信頼できるサーバーを使用して、App Storeと通信します。独自のサーバーを使用すると、サーバーのみを認識および信頼するようにアプリを設計し、サーバーがApp Storeサーバーに接続することを確認できます。ユーザーのデバイスとApp Storeの間に信頼できる接続を直接構築することはできません。その接続のどちらの端も制御しないため、中間者攻撃の影響を受けやすいためです。


公式ドキュメント: https://developer.apple.com/jp/documentation/Receipt-Validation-Programming-Guide-JP.pdf


## 自動更新型の状態チェックについて
自動更新型は1度登録するとユーザーが自身でキャンセルをしない限り自動で課金されるため、
その更新のタイミングでサービスの提供を次の更新日まで伸ばさないといけません。


その状態を確認し、ユーザーのステータスを更新するには、Appleが提供しているレシート検証APIをポーリングする必要があります。
ただ、このポーリングだけでは即座に対応できない場合があります。
そのため下に記載しているAppleサーバー通知と併用することをおすすめします。


## Appleサーバー通知

自動更新サブスクリプションのステータスが変更したときにAppleからの通知を受け取ることができます。
この通知を受け取ることでユーザーがAppleに問い合わせてキャンセルされた場合(返金など)や、アプリ以外からサブスクリプションを再度登録した場合など、
こちら側でハンドリングできないケースに対応することができます。

### 通知のタイプとタイミング
通知の種類とタイミングはこちらです。

|通知タイプ|通知タイミング|
|-|-|
|INITIAL_BUY|サブスクリプションの初回購入時|
|CANCEL|Appleカスタマーサポートによって返金などの理由で解約されたとき<br>または別のサブスクリプションへアップグレードしたとき<br> ※ユーザーが手動で購読の自動更新を停止した場合は通知されない|
|RENEWAL|過去に更新に失敗した期限切れのサブスクリプションの自動更新が成功したとき ※deprecate|
|INTERACTIVE_RENEWAL|ユーザーがアプリ内、またはApp Storeからサブスクリプションを更新したとき|
|DID_CHANGE_RENEWAL_STATUS|サブスクリプションの更新ステータスが変更されたとき<br> ※解除したときにも通知される|
|DID_CHANGE_RENEWAL_PREF|ユーザーがサブスクリプションプランを変更したとき|
|DID_FAIL_TO_RENEW|購読中のサブスクリプションの更新時に決済の問題で更新に失敗したとき|
|DID_RECOVER|更新に失敗した期限切れのサブスクリプションの自動更新が成功した|
|PRICE_INCREASE_CONSENT|サブスクリプションの価格が値上げされるとき|
|DID_RENEW|サブスクリプションの自動更新が成功したとき|
|REFUND|App Storeからユーザーへの払い戻しがあったとき|

※通知タイプ追加により、***RENEWAL***が*deprecate*になっています。代わりに***DID_RECOVER***を使用しましょう。

※Sandbox環境で確認したところ、購読中のサブスクリプションが正常に更新される場合、***DID_CHANGE_RENEWAL_STATUS***は通知されなかったので、
~~更新後の有効期限を得るにはこちらからレシート検証APIを叩くしかなさそうです。~~

2020/11/19更新
**DID_RENEW**の追加により自動更新の成功を受け取れるようになりました。
今までユーザーのサブスクリプションの更新情報を知るにはレシート検証APIを叩く必要がありましたが
これを使うことでAppleのサーバーが停止してない限りはサブスクリプションの更新情報を得ることができます。
※Appleのサーバーが停止することも考慮してこれまで通りこちらから更新のチェックをする処理もあると良さそうです

### サブスクリプションイベントに対して受信が予想される通知タイプ

1つのサブスクリプションイベントに対して通知が複数来る場合があります。

|サブスクリプションイベント|通知タイプ|
|-|-|
|サブスクリプションの初回購入|INITIAL_BUY|
|サブスクリプション購読時に別のサブスクリプションにアップグレード|CANCEL<br>DID_CHANGE_RENEWAL_STATUS<br>INTERACTIVE_RENEWAL|
|サブスクリプション購読時に別のサブスクリプションにダウングレード|DID_CHANGE_RENEWAL_PREF|
|購読中のサブスクリプションの期限が切れた後に同じサブスクリプションを再購読|DID_CHANGE_RENEWAL_STATUS|
|購読中のサブスクリプションの期限が切れた後に別のサブスクリプションを購読|INTERACTIVE_RENEWAL<br>DID_CHANGE_RENEWAL_STATUS|
|AppStoreのサブスクリプション設定から購読をキャンセル|DID_CHANGE_RENEWAL_STATUS|
|Appleによる返金|CANCEL<br>DID_CHANGE_RENEWAL_STATUS|
|決済の問題でサブスクリプションの更新に失敗|DID_FAIL_TO_RENEW|
|登録中のサブスクリプションが更新に失敗し再試行期間中に更新が成功した|DID_RECOVER|
|再試行期間中も更新に失敗しサブスクリプションが解約された|DID_CHANGE_RENEWAL_STATUS|
|App Storeからユーザーのへ払い戻しがあった|REFUND|
|サブスクリプションの値上げに同意した|PRICE_INCREASE_CONSENT|
|自動更新が成功したとき|DID_RENEW|

### 通知タイプ別主要フィールド
通知タイプによって同じフィールド名でも意味合いが変わるものがあるので注意しましょう。

#### INITIAL_BUY
|フィールド名|場所|意味|
|-|-|-|
|purchase_date_ms|latest_receipt_info|最初にサブスクリプションを購入した日時|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|web_order_line_item_id|latest_receipt_info||
|product_id|latest_receipt_info|解約したサブスクリプションの製品ID|

#### INTERACTIVE_RENEWAL
|フィールド名|場所|意味|
|-|-|-|
|purchase_date_ms|latest_receipt_info|最初にサブスクリプションを購入した日時|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|web_order_line_item_id|latest_receipt_info||
|product_id|latest_receipt_info|解約したサブスクリプションの製品ID|

#### DID_CHANGE_RENEWAL_PREF
|フィールド名|場所|意味|
|-|-|-|
|auto_renew_product_id|Top level JSON field|自動更新されるサブスクリプションの製品ID|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|

#### CANCEL
|フィールド名|場所|意味|
|-|-|-|
|cancellation_date_ms|Top level JSON field|キャンセルした日時|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|product_id|latest_receipt_info|解約したサブスクリプションの製品ID|

#### DID_CHANGE_RENEWAL_STATUS
|フィールド名|場所|意味|
|-|-|-|
|auto_renew_status_change_date_ms|Top level JSON field|自動更新ステータスの更新日時|
|auto_renew_status|Top level JSON field|自動更新ステータス|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|product_id|latest_receipt_info|変更されたサブスクリプションの製品ID|

#### DID_FAIL_TO_RENEW
|フィールド名|場所|意味|
|-|-|-|
|is_in_billing_retry_period|pending_renewal_info|再試行フラグ|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|expires_date_ms|latest_receipt_info|更新に失敗した日時|

#### DID_RECOVER
|フィールド名|場所|意味|
|-|-|-|
|purchase_date_ms|latest_receipt_info|支払いに成功した日時|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|expires_date_ms|latest_receipt_info|新しいサブスクリプションの有効期限|

#### PRICE_INCREASE_CONSENT
|フィールド名|場所|意味|
|-|-|-|
|price_consent_status|pending_renewal_info|値上げに同意したか<br>※値上げ確認後に通知が来るためほとんどの場合0で通知される|
|original_transaction_id|latest_receipt_info|サブスクリプションの一意の識別子|
|expires_date_ms|latest_receipt_info|サブスクリプションの有効期限<br>※この期限までに同意してもらう必要がある|


### サーバー通知を受け取るための設定
アプリページ内の**App Storeサーバー通知のURL**にURLを登録すると上記のタイミングでAppleの通知を受け取ることができます。
![スクリーンショット_2020-11-19_15_49_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/4ae5c997-2f8d-0abd-b4a3-9a87ea963dea.png)


### サーバー通知で受け取れるレスポンス

2020/11/19 更新
※レスポンスに含まれていた下記のものが非推奨になりました。(https://developer.apple.com/jp/news/?id=an960mux)

**latest_receipt、 latest_receipt_info、 latest_expired_receipt、 latest_expired_receipt_info**

代わりに**unified_receipt**というキーで返ってくるのでこちらを使いましょう。

最新のレシート情報はunified_receipt内の**latest_receipt_info**に最新100件のレシートが含まれるようです。
またトランザクションが完了している消耗型のレシート情報は除外されるようです。

unified_receiptについての詳細はこちら
https://developer.apple.com/documentation/appstoreservernotifications/unified_receipt



実際に通知で受け取ることができるレスポンスの例です。

```typescript

{
	"auto_renew_product_id": " ",
	"auto_renew_status": "false",
	"auto_renew_status_change_date": "2019-11-08 08:13:55 Etc/GMT",
	"auto_renew_status_change_date_ms": "1573200835000",
	"auto_renew_status_change_date_pst": "2019-11-08 00:13:55 America/Los_Angeles",
	"environment": "Sandbox",
	"latest_receipt": " ", // Base64エンコードされたレシート (長いので省略
	"latest_receipt_info": {
		"bid": " ",
		"bvrs": "1",
		"expires_date": "1573200886000",
		"expires_date_formatted": "2019-11-08 08:14:46 Etc/GMT",
		"expires_date_formatted_pst": "2019-11-08 00:14:46 America/Los_Angeles",
		"is_in_intro_offer_period": "false",
		"is_trial_period": "false",
		"item_id": "1486750613",
		"original_purchase_date": "2019-11-06 10:17:19 Etc/GMT",
		"original_purchase_date_ms": "1573035439000",
		"original_purchase_date_pst": "2019-11-06 02:17:19 America/Los_Angeles",
		"original_transaction_id": "1000000588791509",
		"product_id": " ",
		"purchase_date": "2019-11-08 08:11:46 Etc/GMT",
		"purchase_date_ms": "1573200706000",
		"purchase_date_pst": "2019-11-08 00:11:46 America/Los_Angeles",
		"quantity": "1",
		"subscription_group_identifier": "20567235",
		"transaction_id": "1000000589784549",
		"unique_identifier": "3bf0388bbca73e22cbfbce8e7b5b4c8181047dbc",
		"unique_vendor_identifier": "D89BF8D6-3B46-4F1E-895A-1061ECD31178",
		"version_external_identifier": "0",
		"web_order_line_item_id": "1000000048103370"
	},
	"notification_type": "DID_CHANGE_RENEWAL_STATUS",
	"password": " "
}
```

各プロパティの詳細については下記の公式ページでご確認ください。
https://developer.apple.com/documentation/appstoreservernotifications/responsebody

公式ドキュメント: https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications


## Sandbox環境でのテスト
Sandbox環境での課金テストはApp Store Connectでテスターアカウントを作って行います。

App Store Connect → ユーザとアクセス → Sandbox → テスター から作成できます。

<img width="602" alt="スクリーンショット 2019-11-19 15.33.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/61c68eb0-05d8-b887-dbae-f3989672cf10.png">

Sandbox用のアカウントなので、入力する情報は適当で大丈夫です。
ただパスワードは忘れた場合にあとから確認できないのと、Sandboxアカウントは作成後に編集ができないことに注意してください。

※メールアドレスも架空のものでOKです。(既に使われているものは使えない)
※iTunesなどのプロダクション環境に誤ってサインインした場合は、Sandboxアカウントは無効になり、以降使用できなくなります。
※iPhoneの設定でこのSandboxアカウントでログインする必要はありません。(Sandboxアカウントのログインは別にあります。詳しくは下をご覧ください)

公式ドキュメント: https://help.apple.com/app-store-connect/?lang=ja#/dev8b997bee1

### Sandboxアカウントの切り替え

Sandbox環境で決済処理を呼び出すとStoreKitが自動で下のアラートを表示してくれます。

![IMG_0012.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/5762676f-3641-6301-4cd3-a385a49c54bb.jpeg)
このアラートでSandboxのアカウントを入力して購入するを押すと
iPhoneの設定の**iTunes StoreとApp Store**の画面の下にSANDBOXアカウントの項目が追加されます。

<img width="375" alt="IMG_6971.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/8c03f9dc-e581-c75c-b313-7f98482a71fe.png">

以降ここでSandboxアカウントの切り替えができます。


## Sandbox環境におけるサブスクリプションの更新間隔
Sandbox環境ではサブスクリプションの更新間隔が短く設定されています。
更新の間隔は以下のとおりです。

|期間|Sandboxの期間|
|-|-|
|1週間|３分|
|1ヶ月|5分|
|2ヶ月|10分|
|3ヶ月|15分|
|6ヶ月|30分|
|1年|1時間|



## Sandbox環境での自動更新について
Sandboxでは自動更新型のサブスクリプションは最大6回更新され、その後自動的に期限切れになります。
実装中に何度かテストしていたところ、自動更新されなくなりました。

公式ドキュメントには
> 更新と期限切れの頻度が増しているために、サブスクリプションの期間に短い間隔を残したまま、システムがサブスクリプションの更新を実行しようとする前に、サブスクリプションが期限切れになる場合があります。

と書かれており、おそらく期限切れになったサブスクリプションへの再登録を高頻度で行ったためかと思います。
時間を置いて再度登録しても更新がされなかったので、レシート検証APIを叩いてみたところ
 `pending_renewal_info` 内の `auto_renew_status` が0で返ってきていたので、
自動更新されない状態になってしまったようです。

解決策としては、新しいSandboxユーザーを作るのが良さそうです。

auto_renew_status
「1」 - 現在の購読期間の終了時に購読が更新される。
「0」 - お客様が購読の自動更新をオフにした。

2020/11/19更新
Sandboxで中断された購入をテストすることができるようになりました。
App Store Connectで「ユーザーとアクセス」→ 「Sandbox」→「テスター」で作成済みのSandboxアカウントを選択します。
表示されたポップアップ内に**このテスターの購入を中断する**というチェックボックスがあるので有効にすることでテストができるようになります。

![スクリーンショット_2020-11-19_16_55_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106260/daefe2fc-9e5f-8ab9-338d-fdddc8c9b780.png)


サンドボックスでのテストの詳細はこちら
https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox


## 課金に関するおすすめのライブラリ

iOS: [SwiftyStoreKit](https://github.com/bizz84/SwiftyStoreKit)
javascript: [in-app-purchase](https://github.com/voltrue2/in-app-purchase)
ruby: [itunes_receipt_validator](https://github.com/mbaasy/itunes_receipt_validator)
ruby: [venice](https://github.com/nomad/venice)


## 参考資料
[iOSの消耗型課金のサーバーサイドTipsまとめ](https://qiita.com/joooee0000/items/2e66b695370c5f14be7c)
[iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ](https://qiita.com/joooee0000/items/6cf8ba1d7ba240bef14c)
[iOS課金まとめ](https://qiita.com/kayo311/items/4adeed1048064d9f41f2)
[(公式)レシート検証 プログラミングガイド](https://developer.apple.com/jp/documentation/Receipt-Validation-Programming-Guide-JP.pdf)