自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ

はじめに

AWAサーバサイドエンジニアの辻(jun06t)です。

今回はiOSとAndroidの月額課金のための実装について書かせていただきます。
形式として読み物と言うよりドキュメントっぽくなっています。
理由は私が実装しようとした際に実装方法についてまとめて書かれた記事が少なく、「検証時に使えるフィールドはどれだろう?」「昔はこうだったけど、今は違う?」「Androidではできるけど、iOSではできない(逆も然り)」など、色々と分からない部分が多くとても困ったためです。

やや長い記事となったため、iOSの実装を前編、Androidの実装を後編として説明させていただきます。

注意事項

※1:開発中にプラットフォーム側の仕様変更があったなど、記載している内容は情報が古い可能性があります。
※2:記載している動作は十分に調査できていないものも含んでいるため、内容が不正確である可能性があることをご了承ください。

対象環境

目次

  1. Apple公式ドキュメント
  2. iOSでの購読登録処理の流れ
  3. データフォーマット
  4. レシートの検証フロー
  5. 購読期間
  6. 自動更新手順
  7. レシートの復元
  8. 二重購読
  9. 実装していて困ったこと
  10. まとめ
  11. 謝辞

Apple公式ドキュメント

iOSでの購読登録処理の流れ

おおまかに以下の流れになります。
左半分はクライアントサイドで閉じており、右半分はサーバサイドで閉じてます。

ios_flow

以降はサーバ - AppStoreAPI間の検証処理について述べていきます。

iOSのデータフォーマット

RequestURL

AppStoreAPIのエンドポイントにはSandbox用と本番用があります。

それぞれ使うレシートが違うため、間違った方に送信すると後述するエラーコード2100721008が返ります。
Appleの審査中はSandboxレシートを使用するため、本番サーバでは両方共扱える実装になっている必要があります。

例)必ず最初本番にレシートを投げ、21007が返ってきたらSandboxのエンドポイントへ投げ直す。

RequestBody

上記エンドポイントに対して以下のbodyでリクエストします。

{
  "receipt-data": "MIIu0QYJKoZIhvcNAQcCoIIuwjCCLr4CAQExCzAJBgUrDgMCGgUAMIIeggYJKoZIhvcNA...",
  "password": "hogehoge3ddad5a2f2c06fe47fcdf22e"
}


ResponseBody

正しいレシートだと以下の結果が返ってきます。
※iOS7以降のフォーマットです。transaction_id等はダミーです。

{
  "status": 0,
  "environment": "Sandbox",
  "receipt": {
    "receipt_type": "ProductionSandbox",
    "adam_id": 0,
    "app_item_id": 0,
    "bundle_id": "your_bundle_id",
    "application_version": "1",
    "download_id": 0,
    "original_application_version": "1.0",
    "in_app": [
      {
        "quantity": "1",
        "product_id": "your_product_id",
        "transaction_id": "900200162803342",
        "original_transaction_id": "4002002162812828",
        "is_trial_period": "false",
        "app_item_id": "",
        "version_external_identifier": "",
        "web_order_line_item_id": "3300000022116144",
        "purchase_date": "2015-07-09 08:19:17 Etc/GMT",
        "purchase_date_ms": "1436429957000",
        "purchase_date_pst": "2015-07-09 01:19:17 America/Los_Angeles",
        "original_purchase_date": "2015-07-09 08:17:27 Etc/GMT",
        "original_purchase_date_ms": "1436429847000",
        "original_purchase_date_pst": "2015-07-09 01:17:27 America/Los_Angeles",
        "expires_date": "2015-07-09 08:24:17 Etc/GMT",
        "expires_date_ms": "1436430257000",
        "expires_date_pst": "2015-07-09 01:24:17 America/Los_Angeles",
        "cancellation_date": "",
        "cancellation_date_ms": "",
        "cancellation_date_pst": ""
      },
      {
        "quantity": "1",
        "product_id": "your_product_id",
        "transaction_id": "4500000152802828",
        "original_transaction_id": "4002002162812828",
        "is_trial_period": "false",
        "app_item_id": "",
        "version_external_identifier": "",
        "web_order_line_item_id": "3300000022116145",
        "purchase_date": "2015-07-09 08:14:17 Etc/GMT",
        "purchase_date_ms": "1436429657000",
        "purchase_date_pst": "2015-07-09 01:14:17 America/Los_Angeles",
        "original_purchase_date": "2015-07-09 08:14:18 Etc/GMT",
        "original_purchase_date_ms": "1436429658000",
        "original_purchase_date_pst": "2015-07-09 01:14:18 America/Los_Angeles",
        "expires_date": "2015-07-09 08:19:17 Etc/GMT",
        "expires_date_ms": "1436429957000",
        "expires_date_pst": "2015-07-09 01:19:17 America/Los_Angeles",
        "cancellation_date": "",
        "cancellation_date_ms": "",
        "cancellation_date_pst": ""
      },
    ],
    "latest_receipt": "MIIu0QYJKoZIhvcNAQcCoIIuwjCCLr4CAQExCzAJBgUrDgMCGgUAMIIeggYJKoZIhvcNA" // 最新レシートのBase64文字列
  }
}

重要そうなものだけ説明します。

エラーコード


21006が取り消し線なのは、現在は期限切れでもstatus: 0が返るためです。
なので毎回期限をチェックする必要があります。

レシートの検証フロー

クライアントでの検証はセキュアではないため、基本的にはサーバを経由した検証を推奨します。

receipt_verification

AWAはこの通りではありませんが、一般的なレシート検証の場合、最低限このような検証が必要になります。

iOS購読期間

設定可能な期間

現状では6種類あります。

  • 1週間
  • 1ヶ月
  • 2ヶ月
  • 3ヶ月
  • 6ヶ月
  • 1年

Sandboxの場合

Sandboxでは、時間が早回しで進みます。

Sandbox上の自動更新は6回までで、1ヶ月だと、半年(6回)で自動更新しなくなるので注意してください。
また本番では設定から購読の停止ができますが、Sandboxでは設定が無く停止できないので待つしかないです。

iOSの自動更新手順

iOSはAndroidと違って毎回レシートを送信する必要があります。
以下の流れで購読期限を更新します。

  1. 期限日の24時間前にApp Storeで購読の自動更新を開始
  2. クライアントで更新されたレシートをサーバに送信
  3. サーバからAppleへ送信
  4. レスポンスをサーバで検証し、expires_dateを更新

購読したプロダクトのレシート復元

自動購読を利用する際はレシートによる復元機能が必要になります。ない場合、審査時にリジェクトされます。
ユーザの使用しているAppleIDが同じであれば、どの端末でも同じレシートを再生成することができるため、その再生成したレシートを使って課金履歴と一致するユーザに再ログインさせます。

リストアは以下のメソッドで行います。

[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

トランザクション結果は、以下のデリゲートメソッドで受け取ります。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;

レシートを再生成できたら、先ほどのレシート検証フローで「履歴のuserIdと一致する」場合、該当ユーザの情報を返却すれば良いでしょう。

二重購読

現在AWAではlite, standardの二種類の購読プロダクトがあります。lite->standardへのアップグレード(二重購読)も可能です。また二重購読状態でstandardを解約すれば、standardの期限後に自動的にliteにダウングレードします。

リリースするまでこのような二重購読を実装しているアプリはAppStore上で見当たらなかったため、そもそも審査に通るのか不明でした。
※Evernoteなど、有名な購読アプリでのプラン変更はお問い合わせベースで対応だったため。

WWDCでAppleのエンジニアにもヒアリングしてみましたが、彼らも「大丈夫だと思うが、審査チームがOKかは提出してみないとわからない」と言った回答でした。

結論としてはAWAがリリースできているので二重購読はAppleとしてもOKということになります。

二重購読の場合、アップグレードやダウングレードの処理であったり、複数のレシートをチェックする処理などがありやや複雑になります。しかし「今すぐ上位プランに変更したいのに下位プランをキャンセルして期限まで待たないといけない」「今すぐ変えるためには必ずお問い合わせをしなきゃいけない」といったユーザにとって不利益な実装にはしたくなかったため、今回この対応をしました。

iOSで実装していて困ったこと

テストがしにくい

App内課金機能の開発中は、Sandbox環境に接続して課金操作を検証する事になります。
この時に使用するAppleIDは専用に発行するSandboxアカウントになるのですが、Sandboxアカウントは機能が制限されているため本番アカウントであればアクセスが可能な管理/設定画面等にアクセスできません。
そのため、「テストしたいのに出来ない」ケースが度々ありました。

具体的にテストが出来ず困ったのは以下の機能です。

購入の承認機能(ファミリー共有機能の一部)

子供が購入しようとした時、親のAppleIDに対して請求される仕組みがあるのですが、この機能の一部がSandbox環境ではテストできませんでした。
本来は子供のアカウントで購入の承認を求める操作を行うと親のアカウントにプッシュ通知が届くのですが、これが何故か届いてくれません。
(親のアカウントはクレジットカード情報を設定する必要があるために本番アカウントを使用しなければならなかったので、それも関係しているかもしれません。)

子供側の端末に親のアカウント情報を入力して直接承認するケースのみ、テスト可能でした。

自動購読の解約操作

Sandboxアカウントでは購読管理画面にアクセスできず、解約操作が行えません。

TestFlightの仕様が開発途中で変わった

6月頃まではTestFlight環境では本番環境用のAppleIDを利用できる仕様だったのですが、7月頃からSandboxアカウントしか使えなくなりました。
また、同時に購読の自動更新の更新感覚もそれまでの「12時間」から「5分」に変わってしまいました。

仕様変更前はTestFlightで本番アカウントが利用できていたため、テスターは手持ちのiPhoneで普段使いをしながらテスト出来ていたのですが、仕様変更後はTestFlightでの購入操作時にSandboxアカウントのAppleIDにログインしなおす必要が出てきてしまい、日常的に利用しながらのテストが不可能なケースが出てきてしまいました。

レシート周りの挙動がTestFlightとAppStore版のアプリで異なる

TestFlight環境では、購入操作を行うまではレシートが存在しないため「レシートがない=課金操作を行ったことがない」とみなして処理を進めることができました。
しかし、AppStoreからダウンロードしたアプリではインストール直後の時点で既にレシートがローカルに存在するようで、この条件が成り立たなくなってしまいました。
最終的に、上記の条件を前提に設計していたアーキテクチャを見直す必要がありました。

まとめ

iOSの自動購読課金の実装をする上で「実装時に知りたかった」と思っていた情報をまとめてみました。レシートのフィールドはドキュメントにも書いてないフィールドなどがあったりなど、どれが正しいのか分からずとても苦労しました。
今後iOSアプリで自動購読を導入する方にとって少しでも参考になれば幸いです。

謝辞

AWAの課金実装および今回の記事を書くにあたってAWAのiOSエンジニアである岐部さん(@beryu)には非常にお世話になりました。本当にありがとうございます。