AppStoreサブスクリプションのバックエンドを構築する ①レシート検証編

とあるwebサービスでApp Storeの自動更新サブスクリプションを実装したのですが、
公式ドキュメントがわりと雑で、backend側を作るのがすごく大変だったのでまとめます

（Node.jsとfirebase Functionsを使用）

# 前提

この記事はiOSアプリ側の話ではなく、バックエンド側の話なので
レシートデータを取得できてからのお話になります

Sandboxのアカウントの作成、アプリ側の購入処理などについては

[@Masataka-n](https://qiita.com/Masataka-n)様の[どこよりもわかりやすいiOS最強課金まとめ](https://qiita.com/Masataka-n/items/6f98a5a9fee7b28ccd1f)で分かりやすくまとめられているので参考にしてみてください

# App Storeのサブスクリプションについて

サブスクリプションを実装する上で一番大事なものってなんでしょうか

それは、**『ユーザーが現在サブスクリプション購読中か否か』**を知ることです

そのためには、[verifyReceipt](https://developer.apple.com/documentation/appstorereceipts/verifyreceipt)と[App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications)の2つを理解する必要があります

また、重要なパラメータとしてApp Storeのレシート情報に含まれる`original transaction id`があります

これはApple IDと1対1で紐づいているIDで、
これを使用することでサービス内のユーザーを特定することができます

今回は`verifyReceipt`を、次回で`App Store Server Notifications`を説明していきます


# レシート検証API verifyReceipt

とりあえず持っているbase64エンコードされたくっっっっそ長いレシートデータをPostmanなどを使って
以下の通りjson形式で
`https://sandbox.itunes.apple.com/verifyReceipt`へPOSTしてみましょう

```json
{
  "password": ***, // iOSアプリ側で設定済みのはず
  "exclude-old-transactions": true,
  "receipt-data": ******** //base64エンコードされたくっっっっそ長い文字列
}
```

返ってくる値はこんな感じです

```json
{
    "environment": "Sandbox",
    "receipt": {
        "receipt_type": "ProductionSandbox",
        "adam_id": 0,
        "app_item_id": 0,
        "bundle_id": "***",
        "application_version": "45",
        "download_id": 0,
        "version_external_identifier": 0,
        "receipt_creation_date": "2020-10-13 01:48:13 Etc/GMT",
        "receipt_creation_date_ms": "1602553693000",
        "receipt_creation_date_pst": "2020-10-12 18:48:13 America/Los_Angeles",
        "request_date": "2020-11-10 01:59:34 Etc/GMT",
        "request_date_ms": "1604973574730",
        "request_date_pst": "2020-11-09 17:59:34 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": "1000000724743083",
                "original_transaction_id": "1000000724743083",
                "purchase_date": "2020-09-30 10:30:56 Etc/GMT",
                "purchase_date_ms": "1601461856000",
                "purchase_date_pst": "2020-09-30 03:30:56 America/Los_Angeles",
                "original_purchase_date": "2020-09-30 10:30:58 Etc/GMT",
                "original_purchase_date_ms": "1601461858000",
                "original_purchase_date_pst": "2020-09-30 03:30:58 America/Los_Angeles",
                "expires_date": "2020-09-30 10:35:56 Etc/GMT",
                "expires_date_ms": "1601462156000",
                "expires_date_pst": "2020-09-30 03:35:56 America/Los_Angeles",
                "web_order_line_item_id": "1000000056115800",
                "is_trial_period": "false",
                "is_in_intro_offer_period": "false"
            },
            {
                "quantity": "1",
                "product_id": "***",
                "transaction_id": "1000000724745717",
                "original_transaction_id": "1000000724743083",
                "purchase_date": "2020-09-30 10:35:56 Etc/GMT",
                "purchase_date_ms": "1601462156000",
                "purchase_date_pst": "2020-09-30 03:35:56 America/Los_Angeles",
                "original_purchase_date": "2020-09-30 10:30:58 Etc/GMT",
                "original_purchase_date_ms": "1601461858000",
                "original_purchase_date_pst": "2020-09-30 03:30:58 America/Los_Angeles",
                "expires_date": "2020-09-30 10:40:56 Etc/GMT",
                "expires_date_ms": "1601462456000",
                "expires_date_pst": "2020-09-30 03:40:56 America/Los_Angeles",
                "web_order_line_item_id": "1000000056115801",
                "is_trial_period": "false",
                "is_in_intro_offer_period": "false"
            },
            ..
        ]
    },
    "latest_receipt_info": [
        {
            "quantity": "1",
            "product_id": "***",
            "transaction_id": "1000000737669781",
            "original_transaction_id": "1000000724743083",
            "purchase_date": "2020-11-04 08:38:37 Etc/GMT",
            "purchase_date_ms": "1604479117000",
            "purchase_date_pst": "2020-11-04 00:38:37 America/Los_Angeles",
            "original_purchase_date": "2020-11-04 08:37:58 Etc/GMT",
            "original_purchase_date_ms": "1604479078000",
            "original_purchase_date_pst": "2020-11-04 00:37:58 America/Los_Angeles",
            "expires_date": "2020-11-04 08:43:37 Etc/GMT",
            "expires_date_ms": "1604479417000",
            "expires_date_pst": "2020-11-04 00:43:37 America/Los_Angeles",
            "web_order_line_item_id": "1000000057023410",
            "is_trial_period": "false",
            "is_in_intro_offer_period": "false",
            "subscription_group_identifier": "20664761"
        }
    ],
    "latest_receipt": "***", // めっちゃ長い
    "pending_renewal_info": [
        {
            "expiration_intent": "1",
            "auto_renew_product_id": "***",
            "original_transaction_id": "1000000724743083",
            "is_in_billing_retry_period": "0",
            "product_id": "***",
            "auto_renew_status": "0"
        }
    ],
    "status": 0
}
```


## 購読中かどうかを知りたいとき

項目多すぎてワケワカラン。

でも大丈夫。**『ユーザーが現在サブスクリプション購読中か否か』**を知りたい場合

`latest_receipt_info`内の`expired_date`を見るだけです

既に期限切れであれば、`latest_receipt_info`等についている`original_transaction_id`から該当のユーザーと照合し、
フラグを折る感じの実装になると思います


## 過去の購入履歴を知りたい

他にも、過去の購入履歴を見たいときは`in_app`内から`purchase_date`と`expires_date`でフィルターをかけると取得できます


## レシートデータの更新

リクエスト時に投げた`receipt-data`が古い場合は、
`latest_receipt`で渡される値に更新しておきましょう

## 現在自動更新が有効かどうかを知りたい

ときは、`pending_renewal_info`に含まれる`auto_renew_status`を見ればOKです

`1`ならば自動更新が有効で、`0`ならば次回自動更新されないユーザーです

## 自動更新されない理由を知りたい

ときは、`pending_renewal_info`に含まれる`expiration_intent`を見てください

月に一回cronでレシートデータをチェックして、`1`のユーザーには訴求用のプッシュ通知を送る、
などの実装例が想像できますね

![expiration_intent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174833/355c7660-7a4c-c0ff-9036-8b47c559706e.png)

[詳しくは公式リファレンスへ](https://developer.apple.com/documentation/appstorereceipts/expiration_intent)

# 次回 Server Notifications

レシート検証APIはいかがだったでしょうか？

次回は`App Store Server Notifications`について説明していきます！