大事故防止！iOSの自動更新購読型と消耗型の課金を共存させるときのサーバーサイドTipsまとめ

今回は、ひとつのアプリに自動更新購読型と消耗型を共存させたときのサーバーサイドで行うレシート検証のTipsを紹介します。

### 自動更新購読型課金のサーバーサイド実装について

自動更新購読型課金単体で実装する場合はこちらの記事が参考になります。
(昔書いた記事で古い情報がある場合があります)
下記の記事では月額課金と呼んでいますが、自動更新購読と同義です。

https://qiita.com/joooee0000/items/6cf8ba1d7ba240bef14c


### 消耗型課金のサーバーサイド実装について

消耗型課金単体で実装する場合はこちらの記事が参考になります。

https://qiita.com/joooee0000/items/2e66b695370c5f14be7c


# 自動更新購読型課金と消耗型課金を共存させるときのレシート検証

「レシート検証って何？」という疑問については、上記にリンクを載せた記事にすでに書いてあるので、この記事では触れないことにします。

自動更新購読型課金と消耗型課金を同じアプリに共存させようとした場合、気になるのがレシート検証の仕方です。
AppStoreのサーバーから返ってくるレシートの形式によって、レシート検証の仕方がそれぞれ単独で存在していたときとは異なってきます。
例えば、レシートはそれぞれ発行されるのか？ひとつのレシートにどちらの情報も返ってくるのか？その場合新しいJSONキーが増えるのか？等が気になる事項です。

さっそく二種類の購入型を共存させたときに実際に返ってくるレシートを見てみましょう。

## 共存させたときに返るレシートの内容

```json
 {
 "status"=>0,
 "environment"=>"Sandbox",
 "receipt"=>
  {"receipt_type"=>"ProductionSandbox",
   "adam_id"=>0,
   "app_item_id"=>0,
   "bundle_id"=>"your bundle id",
   "application_version"=>"36",
   "download_id"=>0,
   "version_external_identifier"=>0,
   "receipt_creation_date"=>"2017-10-23 12:15:47 Etc/GMT",
   "receipt_creation_date_ms"=>"1508760947000",
   "receipt_creation_date_pst"=>"2017-10-23 05:15:47 America/Los_Angeles",
   "request_date"=>"2017-10-25 07:03:22 Etc/GMT",
   "request_date_ms"=>"1508915002492",
   "request_date_pst"=>"2017-10-25 00:03:22 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"=>"your product id",
      "transaction_id"=>"10000003161787",
      "original_transaction_id"=>"10000003161787",
      "purchase_date"=>"2017-07-18 03:20:05 Etc/GMT",
      "purchase_date_ms"=>"1500348005000",
      "purchase_date_pst"=>"2017-07-17 20:20:05 America/Los_Angeles",
      "original_purchase_date"=>"2017-07-18 03:20:05 Etc/GMT",
      "original_purchase_date_ms"=>"1500348005000",
      "original_purchase_date_pst"=>"2017-07-17 20:20:05 America/Los_Angeles",
      "is_trial_period"=>"false"},
     {"quantity"=>"1",
      "product_id"=>"your product id",
      "transaction_id"=>"10000003457411",
      "original_transaction_id"=>"10000003457411",
      "purchase_date"=>"2017-10-23 12:15:43 Etc/GMT",
      "purchase_date_ms"=>"1508760943000",
      "purchase_date_pst"=>"2017-10-23 05:15:43 America/Los_Angeles",
      "original_purchase_date"=>"2017-10-23 12:15:46 Etc/GMT",
      "original_purchase_date_ms"=>"1508760946000",
      "original_purchase_date_pst"=>"2017-10-23 05:15:46 America/Los_Angeles",
      "expires_date"=>"2017-10-23 12:20:43 Etc/GMT",
      "expires_date_ms"=>"1508761243000",
      "expires_date_pst"=>"2017-10-23 05:20:43 America/Los_Angeles",
      "web_order_line_item_id"=>"1000000036650295",
      "is_trial_period"=>"false"}],
   "original_json_response"=>{...}},
 "latest_receipt_info"=>  [{"quantity"=>"1",
    "product_id"=>"your product id",
    "transaction_id"=>"10000003161780",
    "original_transaction_id"=>"10000003161780",
    "purchase_date"=>"2017-07-18 03:20:05 Etc/GMT",
    "purchase_date_ms"=>"1500348005000",
    "purchase_date_pst"=>"2017-07-17 20:20:05 America/Los_Angeles",
    "original_purchase_date"=>"2017-07-18 03:20:05 Etc/GMT",
    "original_purchase_date_ms"=>"1500348005000",
    "original_purchase_date_pst"=>"2017-07-17 20:20:05 America/Los_Angeles",
    "is_trial_period"=>"false"},
   {"quantity"=>"1",
    "product_id"=>"your product id",
    "transaction_id"=>"10000003457411",
    "original_transaction_id"=>"10000003457411",
    "purchase_date"=>"2017-10-23 12:15:43 Etc/GMT",
    "purchase_date_ms"=>"1508760943000",
    "purchase_date_pst"=>"2017-10-23 05:15:43 America/Los_Angeles",
    "original_purchase_date"=>"2017-10-23 12:15:46 Etc/GMT",
    "original_purchase_date_ms"=>"1508760946000",
    "original_purchase_date_pst"=>"2017-10-23 05:15:46 America/Los_Angeles",
    "expires_date"=>"2017-10-23 12:20:43 Etc/GMT",
    "expires_date_ms"=>"1508761243000",
    "expires_date_pst"=>"2017-10-23 05:20:43 America/Los_Angeles",
    "web_order_line_item_id"=>"1000000036650295",
    "is_trial_period"=>"false"},
       .
       .
       .
   {"quantity"=>"1",
    "product_id"=>"your product id",
    "transaction_id"=>"10000003464111",
    "original_transaction_id"=>"10000003457411",
    "purchase_date"=>"2017-10-25 05:54:36 Etc/GMT",
    "purchase_date_ms"=>"1508910876000",
    "purchase_date_pst"=>"2017-10-24 22:54:36 America/Los_Angeles",
    "original_purchase_date"=>"2017-10-23 12:15:46 Etc/GMT",
    "original_purchase_date_ms"=>"1508760946000",
    "original_purchase_date_pst"=>"2017-10-23 05:15:46 America/Los_Angeles",
    "expires_date"=>"2017-10-25 06:54:36 Etc/GMT",
    "expires_date_ms"=>"1508914476000",
    "expires_date_pst"=>"2017-10-24 23:54:36 America/Los_Angeles",
    "web_order_line_item_id"=>"1000000036672623",
    "is_trial_period"=>"false"},
   {"quantity"=>"1",
    "product_id"=>"your product id",
    "transaction_id"=>"10000003464307",
    "original_transaction_id"=>"10000003457411",
    "purchase_date"=>"2017-10-25 06:54:36 Etc/GMT",
    "purchase_date_ms"=>"1508914476000",
    "purchase_date_pst"=>"2017-10-24 23:54:36 America/Los_Angeles",
    "original_purchase_date"=>"2017-10-23 12:15:46 Etc/GMT",
    "original_purchase_date_ms"=>"1508760946000",
    "original_purchase_date_pst"=>"2017-10-23 05:15:46 America/Los_Angeles",
    "expires_date"=>"2017-10-25 07:09:36 Etc/GMT",
    "expires_date_ms"=>"1508915376000",
    "expires_date_pst"=>"2017-10-25 00:09:36 America/Los_Angeles",
    "web_order_line_item_id"=>"1000000036672630",
    "is_trial_period"=>"false"}],
 "pending_renewal_info"=>
  [{"auto_renew_product_id"=>"your product id", "original_transaction_id"=>"1000000345741172", "product_id"=>"your product id", "auto_renew_status"=>"1"}]}
```

### レシートが一緒になって返ってくる！

返ってきたレシートをよーーーく見ると、どちらの情報も1つのレシートに混ぜこぜになって返ってきています。自動更新購読型と消耗型それぞれ別のレシートが発行されるというわけでも、新しいJSONのキーが増えるわけでもありません。
特に、`in_app`と`latest_receipt_info`の購入履歴を保持する配列に各購入情報が混ざって入ってきますので、注意が必要です。

## 自動更新購読型と消耗型の情報の見分け方

一緒に返ってくることがわかったところで、購入型の違いによってどのような情報の違いがあるかを見ていきます。
下記のJSONが、それぞれの購読型で返ってくるトランザクションごとの情報です。(`in_app`や`latest_receipt_info`の配列の中身)

#### 消耗型課金で返る値

```json

{
      "quantity"=>"1",
      "product_id"=>"card_10",
      "transaction_id"=>"10000003161787",
      "original_transaction_id"=>"10000003161787",
      "purchase_date"=>"2017-07-18 03:20:05 Etc/GMT",
      "purchase_date_ms"=>"1500348005000",
      "purchase_date_pst"=>"2017-07-17 20:20:05 America/Los_Angeles",
      "original_purchase_date"=>"2017-07-18 03:20:05 Etc/GMT",
      "original_purchase_date_ms"=>"1500348005000",
      "original_purchase_date_pst"=>"2017-07-17 20:20:05 America/Los_Angeles",
      "is_trial_period"=>"false"
}
```

#### 自動更新購読型で返る値

```json
{
      "quantity"=>"1",
      "product_id"=>"monthly_paid_1",
      "transaction_id"=>"10000003457411",
      "original_transaction_id"=>"10000003457411",
      "purchase_date"=>"2017-10-23 12:15:43 Etc/GMT",
      "purchase_date_ms"=>"1508760943000",
      "purchase_date_pst"=>"2017-10-23 05:15:43 America/Los_Angeles",
      "original_purchase_date"=>"2017-10-23 12:15:46 Etc/GMT",
      "original_purchase_date_ms"=>"1508760946000",
      "original_purchase_date_pst"=>"2017-10-23 05:15:46 America/Los_Angeles",
      "expires_date"=>"2017-10-23 12:20:43 Etc/GMT",
      "expires_date_ms"=>"1508761243000",
      "expires_date_pst"=>"2017-10-23 05:20:43 America/Los_Angeles",
      "web_order_line_item_id"=>"1000000036650295",
      "is_trial_period"=>"false"
}
```

上記で確認できる通り、自動更新購読型と消耗型の見分けがつくkeyは`expires_date`の有無と、`product_id`です。
なので、このどちらかで自動更新購読型か消耗型かを見分けてそれぞれの処理をする必要があります。

## 各購読型で見るべき項目

`in_app`と`latest_receipt_info`はそれぞれの購入型によって配列の中に購入情報が残る条件が違います。

|         | in_app | latest_receipt_info|
|--------|--------|------|
|自動更新購読型| 購入情報の一部が無期限に残る   |課金情報の更新の履歴がすべて残る |
|消耗型|トランザクションが完了していない情報のみ残る|?|

Appleの公式ドキュメントには、消耗型は`in_app`を参照し、自動更新購読型に関しては`latest_receipt_info`で自動更新された最新のレシートを取得してくださいという説明があります。
消耗型だけのときは、レシートに`latest_receipt_info`は返ってきません。なので、消耗型は`in_app`しか見ない、自動更新購読型は`latest_receipt_info`しか見ないという実装でいいと思います。
ただ、それぞれのkeyの中にはそれぞれの購読型の購入情報が混ざって入ってきてしまうので(なぜ`latest_receipt_info`の配列の中に消耗型の情報が返ってくるかはわかりません。。)、例えば`expires_date`というkeyが必ずあるというていでコードを書いてしまうと事故になります。

## AppStoreのレシート問い合わせ

AppStoreのサーバーへレシートを問い合わせる際のとても大きな注意点が1つあります。これは知らないと大事故になると思うのでぜひ事前に把握しておきたいポイントです。

AppStoreのサーバーへのレシート問い合わせは、HTTPのPOSTリクエストを送ることで問い合わせることができます。
その際に、自動購読型と消耗型それぞれが単独で存在している場合のレシートを問い合わせに必要なリクエストbodyには、下記の違いがあります。

### 消耗型

|key|値|サンプル|
|---|---|---|
|receipt-data|Base64エンコードしたレシート情報|MIIjwgYJKoZIhvcNAQcCoIIjszCCI…|

### 自動購読型

|key|値|サンプル|
|---|---|---|
|receipt-data|Base64エンコードしたレシート情報|MIIjwgYJKoZIhvcNAQcCoIIjszCCI…|
|passward|アプリケーションの共有鍵| fea2ebde5...|

表を見ると分かる通り、消耗型のみのときには共有鍵が不要なのに対し、自動購読型では共有鍵をbodyに指定する必要があります。
では、両購入型を共存させた場合、どのようにリクエストする必要があるでしょうか？
レシートがひとつしかないとなると、消耗型を購入した際のレシートでAppStoreに問い合わせる際にも共有鍵が必要になるかが気になるポイントです。

この問題を、下記の手順でテストしてみました。

1. 課金履歴のないAppleのアカウントを用意
2. 消耗型アイテムを購入
3. その後自動購読型アイテムを購入
4. 消耗型アイテムを購入

結果は下記のようになりました。

|状態|共有鍵の必要性|
|---|---|
|2| 必要なし
|3| 必要
|4| 必要

このように、自動購入型を購入する前と後で、消耗型アイテムを購入した際のレシート問い合わせ時の共有鍵の必要性が変わってきます。
なので、自動購入型を導入した時点でレシートを問い合わせる際は共有鍵を必ず送るようにしておく必要があります。


# 実際の課金構造

購入型が混ざっている場合、ネイティブからの課金リクエストは3種類あります。

1. 消耗型のアイテムを購入したとき
2. 自動更新購読型のアイテムを購入したとき
3. StoreKitで発火する、未処理のレシートが存在するとき

です。

1, 2に関しては今まで通りそれぞれの購入型のレシートを処理すれば問題ありません。
しかし、3に関しては注意が必要です。なぜなら、StoreKit経由でのリクエストは消耗型の未処理トランザクションによって発火したものか、自動更新購読型の更新によって発火しているリクエストかがわからないためです。

なので、3の場合は下記の図のように、自動更新購読型と消耗型の両方の購入情報をチェックする必要があります。

図1　StoreKitで発火する処理

<img width="250" alt="kakin_min.png" src="https://qiita-image-store.s3.amazonaws.com/0/43089/59461d3d-f562-9ed3-e1a7-82829cd1f6f0.png">

# まとめ

- 自動購読型と消耗型を共存させたとき、レシートはひとつに統合される
- 自動購読型の存在の有無によってAppStoreのサーバーにレシートを問い合せる際のbodyの内容が変わる
- StoreKitで発火する末トランザクションがある場合のリクエストは自動更新購読型と消耗型のどちらの情報もチェックする必要がある

これらを把握して、事故のない課金ライフを送りましょう！