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

f:id:vasilyjp:20180927090637j:plain

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

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

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

tech.vasily.jp

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

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

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

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

自動更新購読型課金と消耗型課金を同じアプリに共存させようとした場合、気になるのがレシート検証の仕方です。 AppStoreのサーバーから返ってくるレシートの形式によって、レシート検証の仕方がそれぞれ単独で存在していたときとは異なってきます。 例えば、レシートはそれぞれ発行されるのか?ひとつのレシートにどちらの情報も返ってくるのか?その場合新しい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_applatest_receipt_infoの購入履歴を保持する配列に各購入情報が混ざって入ってきますので、注意が必要です。

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

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

消耗型課金で返る値

{
      "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"
}

自動更新購読型で返る値

{
      "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_applatest_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で発火する処理

f:id:vasilyjp:20171124121103p:plain

まとめ

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

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

終わりに

VASILYエンジニアはお金周りに興味があるエンジニアを募集しています。 興味ある方は以下のリンクからご応募ください。

カテゴリー