iOSの消耗型課金のサーバーサイドTipsまとめ

f:id:vasilyjp:20180927090637j:plain

こんにちは、バックエンドエンジニアのじょーです。 以前、月額課金型のサーバーサイドでのレシート検証の記事を書きました。(iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ)
今回は、消耗型課金のサーバーサイド実装について書きます!

注意

この情報は2017年8月25日現在のものです。

目次

  1. 消耗型課金全体の処理フロー
  2. レシート検証について
  3. 課金アイテムの扱い方について

消耗型課金全体の処理フロー

消耗型課金とは、AppStoreで登録できる使い切りのアイテムへの課金のことをいいます。 たとえば、ゲームアプリでライフを購入するときなどは使い切りのアイテムなので消耗型課金になります。一方、1か月など決まった期間サービスが受けられる課金のことを月額課金や、自動更新購読といったりします。

(実際のアイテム登録画面) f:id:vasilyjp:20170824203933p:plain

アプリで消耗型課金商品を購入してからの処理の順番は、下記の図のようになります。

f:id:vasilyjp:20170824203833p:plain

  1. アプリからAppStoreへ購入リクエスト
  2. AppStoreからレシートを受取る
  3. サーバーサイドへBase64エンコード済みのレシートを投げる
  4. サーバーサイドはBase64エンコード済みのレシートを使ってAppStoreにレシート問い合わせ
  5. AppStoreからJSON形式のレシートを受取る
  6. レシート情報を元に、購入が不正に行われたものでないかを検証
  7. 正常な購入であれば購入結果をDBに反映
  8. アプリに検証結果を返却
  9. 処理完了のリクエストをAppStoreへ送信

また、本記事ではサーバーサイドで行う処理に着目しているため、工程の①〜③に関しての詳細説明は割愛し、サーバーサイドで行う④〜⑥のレシート検証処理の工程を詳しく説明していきます。

レシート検証について

レシート検証とは?

レシート検証とは、アプリから購入リクエストが届いた際、アプリを介さずにAppStoreに直接購入情報を問い合わせることで、不正な購入や意図しない購入でないかを検証することです。この処理によって、アプリから偽装されたレシートが送られてきた場合や二重の購入、購入処理の漏れなどを防ぐことができます。

アプリで商品を購入した場合、課金の証明としてAppStoreがレシートを発行します。 レシートと言ってもAppStoreが紙のレシートを送りつけてくるわけではなく、電子的な購入情報のことをレシートと呼びます。 その際に、AppStoreのサーバーにHTTPのPOSTリクエストでJSON形式のレシートを問い合わせ、現在の課金状況を知ることができます。その結果を元に、レシートが不正なレシートでないかをサーバーサイドでチェックします。

レシート問い合わせの方法

処理フローでの④,⑤について説明します。 レシート情報の問い合わせは、AppStoreのサーバーにHTTPのPOSTリクエストを送ることで問い合わせることができます。

リクエストURL

テスト環境用のURLと、production用のURLで分かれています。

環境 URL 用途
production https://buy.itunes.apple.com/verifyReceipt 本番用
sandbox https://sandbox.itunes.apple.com/verifyReceipt 開発時のテスト環境用
  • sandboxにはsandbox用の、productionにはproduction用のレシートがあり、productionのURLにsandbox用のレシートを送るとエラーが返ってきます。
  • sandboxを利用するには、Appleのテストアカウントを取得して課金処理のテストを行います。

リクエストbody

下記の1つだけでOKです。

key サンプル
receipt-data Base64エンコードしたレシート情報 MIIjwgYJKoZIhvcNAQcCoIIjszCCI...
  • receipt-dataのサンプルは省略してありますが、実際はかなり長いです。12KB程度のデータです。
  • 処理フローの③で示した通り、クライアントからBase64エンコード済みのデータを受け取ります。購入時にクライアントからレシートを送ってもらわないとサーバーサイドとAppStoreで直接レシート検証のやり取りができません。

AppStoreから返ってくるレシート情報の項目

下記が、レシートを問い合わせた際に返ってくるレシートサンプルです。処理フローでいうと⑤でAppStoreが返す情報です。 RubyのHash型で記してありますが、実際にはJSON形式で返ってきます。

    {
      "status"=>0,
      "environment"=>"Sandbox",
      "receipt"=>
        {
          "receipt_type"=>"ProductionSandbox",
          "adam_id"=>0,
          "app_item_id"=>0,
          "bundle_id"=>"jp.hoge.hoge",
          "application_version"=>"1",
          "download_id"=>0,
          "version_external_identifier"=>0,
          "receipt_creation_date"=>"2017-07-18 04:03:48 Etc/GMT",
          "receipt_creation_date_ms"=>"1500350628000",
          "receipt_creation_date_pst"=>"2017-07-17 21:03:48 America/Los_Angeles",
          "request_date"=>"2017-07-18 05:32:59 Etc/GMT",
          "request_date_ms"=>"1500355979599",
          "request_date_pst"=>"2017-07-17 22:32:59 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"=>"productのid",
                "transaction_id"=>"1284721948247",
                "original_transaction_id"=>"1000000316178057",
                "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"=>"productのid",
                "transaction_id"=>"1284721948248",
                "original_transaction_id"=>"1000000316185518",
                "purchase_date"=>"2017-07-18 04:03:48 Etc/GMT",
                "purchase_date_ms"=>"1500350628000",
                "purchase_date_pst"=>"2017-07-17 21:03:48 America/Los_Angeles",
                "original_purchase_date"=>"2017-07-18 04:03:48 Etc/GMT",
                "original_purchase_date_ms"=>"1500350628000",
                "original_purchase_date_pst"=>"2017-07-17 21:03:48 America/Los_Angeles",
                "is_trial_period"=>"false"
              }
            ]
        }

今回は、レシート検証(処理フローで言う④,⑤の部分)にveniceというgemを使いました。こちらのgemを使うと、開発環境ではsandboxを、本番環境ではproductionのエンドポイントを叩いてくれたり、JSONをパースしてRubyのHash型でレシートの値を返してくれます。

検証項目

⑤で返ってきたJSON形式のレシートを使って、サーバーサイドで検証すべき内容を検証します。処理フローで言うと、⑥の部分です。 レシートから返ってきた項目のうち検証すべき項目は以下です。

項目 項目の内容 検証内容
status 0であれば正常なレシート、その他は不正なレシート(エラーコード表参照) AppStoreから正常なレシートが返ってきているか
in_app 購入情報の配列 課金処理すべき購入情報のチェック
in_app: transaction_id in_appの1購入ごとに存在する固有のid すでに処理されたレシートでないか
bundle_id iTunesConnectで設定したCFBundleIdentifierの値 自分のアプリのものか
product_id iTunesConnectで設定したproductIdentifierの値 意図した商品への課金か、存在している商品への課金か

消耗型課金の場合、上記の表以外の項目はデバッグに用いる値と考えて良いと思います。他に検証すべき項目があればコメントをください。よろしくお願いします。

これらの項目の中で注意すべきは、in_appキーの中身です。 in_appの中身は、購入1回分の購入情報の配列です。注意しなければならないポイントは、in_appの配列の中身が複数返ってくることがあるというところです。

AppStoreでは、①〜⑨までの処理にトランザクションをはっており、どこかで通信エラーやサーバーエラー等のエラーが起きて⑨の完了処理を実現できなかった場合、トランザクションが完了せずに未処理の購入となります。

未処理の購入は下記の2パターンの方法で再度処理を行う必要があります。

  1. ⑨が実現できていないという情報がStoreKitからアプリに通知されたタイミングで再度③から課金処理を行う
  2. 別のアイテムの購入処理をした際に、AppStoreから未処理の購入情報も一緒に返ってくるのでそのタイミングで課金処理を行う(トランザクションが完了していない購入がある場合同じアイテムは購入できない)

パターン2の場合、別の購入処理で問い合わせたレシートのin_appの中に未処理の購入が含まれて返ってきます。 この場合、in_appの中に返ってくる購入情報は新しく購入されたか、一度処理を失敗して残っている情報なのでin_appの中身すべてを処理する必要があります。

また、下記の図のように、サーバーサイドのアイテム付与の処理はすべて終了しているにも関わらず、⑧で通信が失敗してしまい、⑨の処理が完了していないというパターンが存在します。その際、サーバーサイド側に過去に処理したtransaction_idを保持しておき、過去に処理したtransaction_idであればサーバーサイドでは二度同じ処理をせずに⑨の購入完了処理のみをアプリ側に行ってもらうようにしておく必要があります。

f:id:vasilyjp:20170824203817p:plain

課金アイテムの扱い方について

消耗型課金は、課金アイテムが一つでない限り、課金アイテムのリストをアプリに表示する必要があります。たとえば、ルビー1200個購入で10000円、12個で120円など、アイテムが複数ある場合です。

その際に、AppStoreにないアイテムはそもそも購入できないため、アイテムがAppStoreに存在しているかをチェックしてからアイテムリストをアプリに表示します。 また、逐一AppStoreにアイテムリストを問い合わせる理由として、価格の変化があります。AppStoreで登録した商品は、為替レートの変動により価格が変わることがあるので、毎回現状の価格をAppStoreに問い合わせなければアプリで表示している価格と実際にAppStoreで決済される価格が食い違ってしまいます。

AppStoreへの課金アイテム問い合わせフロー

f:id:vasilyjp:20170824203915p:plain

このような手順で課金アイテムリストを表示します。 AppStoreにアイテムを登録する際、アイテムを識別するユニークなkey名をproduct_idと呼びます。 また、課金アイテムリストは、サーバーサイドでも管理します。

AppStoreへアクセスすればサーバーサイドでアイテムリストを管理する必要がないのでは?と思うかもしれませんが、サーバーサイドでもアイテムリストを持ったほうがいい理由は3つあります。

  1. AppStoreでは最小限の商品情報しか持てない

    • たとえば、ルビー1200個購入で10000円という商品の場合、ルビー1200個という情報はAppStoreでは登録できず、参照名、product_id(製品ID)と価格くらいしか保持していません。(下図参照) よって、サーバーサイドでproduct_idと商品の内容を紐付けたテーブルをDBに持つ必要があります。
  2. 課金履歴などのデータを保持しやすくなる

  3. 期間限定のアイテムなどを配信したい際にサーバーサイドでコントロールできる

(課金アイテムの登録画面) 参照名、製品ID(product_id)、価格が登録できます。 f:id:vasilyjp:20170824203930p:plain

まとめ

本記事では、消耗型課金におけるサーバーサイドに必要な実装について説明しました。

参考資料

In-App Purchaseプログラミングガイド
Validating Receipts With the App Store

※ バージョンによって仕様が変わるので、もともとの仕様書をよく読むことをおすすめします。

最後に

VASILYでは、積極的に挑戦していけるエンジニアを募集しています。興味のある方は以下のリンクからぜひご応募ください。 https://www.wantedly.com/projects/61389www.wantedly.com

カテゴリー