はじめに
個人的な備忘として、iOSアプリでアプリ内課金を実装する際のHowToやノウハウを残しておく。
動いていることは確認しているが、あまり洗練されている気がしない。。。
App Store Server Notifications を利用すれば、多分、もう少し良い具合に実装できそう。
ベストプラクティスだとは思わないで頂けると。。。
処理
以下、時系列に従って実装の概要・勘所を説明する。
1. アプリ内課金を実施する
割愛(サーバーから商品情報取得したり、App Storeで決済したり)
2. アプリでレシートを取得する
アプリ側の実装として、以下のコードを参考にApp Storeからレシートを取得する。
取得したレシートは、サーバーへ送信し、検証・商品提供を行う。
// エラーハンドリングは割愛
let url = Bundle.main.appStoreReceiptURL
let data = Data(contentsOf: url, options: .alwaysMapped)
let receipt = data.base64EncodedString()
3. サーバーでレシート検証を行う
App Store verifyReceipt を実行して、処理項番2で取得したレシートの検証を行う。
参考: Validating Receipts with the App Store | Apple Developer Documentation
以下、URLにPOSTすると、検証結果、及びトランザクション情報が返却される。
環境 | URL |
---|---|
Production | https://buy.itunes.apple.com/verifyReceipt |
Sandbox | https://sandbox.itunes.apple.com/verifyReceipt |
パラメーター
キー | データ | 備考 |
---|---|---|
receipt-data |
処理項番2で取得したレシート | - |
password |
iTunes Connectのキー | サブスクのみ |
exclude-old-transactions |
true (最新のトランザクション情報のみ取得) |
サブスクのみ |
レスポンス
公式ドキュメント参照。
Appleレビュアー対策
Appleレビュアーは、Sandbox環境のレシートを送信してくる。
Productionに対して、Sandboxレシートを送信するとstatus
が21007 (環境不整合)
になり、処理できない。
課金処理が正常終了しないと、不具合ありとしてレビュー通過できないため、考慮が必要。
考えられる解決策は、以下3つ。
1. User-Agentを利用したバージョンチェック
User-Agentのアプリバージョンが公開前バージョンであれば、Appleレビュアーの検証によるものとみなし、SandboxでverifyReceiptを行う。
- メリット
- 直感的な実装になる(と思う)
- デメリット
- 開発側で公開前バージョンアプリを利用した、決済テストができない
- アプリバージョンの管理が必要
2. verifyReceiptの実行結果に応じてハンドリング
一旦、ProductionでverifyReceiptを行う。status = 21007
だった場合はSandboxで再実行する。
- メリット
- 案1のデメリットが解消できる
- 比較的、少ない手間で対応可能
- デメリット
- 実装がスッキリしない
- Appleレビュアー以外からの不正なリクエストだった場合、検出しにくい
3. テストユーザーとして管理する
Appleへアプリ審査を依頼する際に、テストユーザーの認証情報を指定することができる。
サービスの本番環境向けテストユーザー管理の仕組みに組み込む。
- メリット
- 案1, 2のデメリットを解消できる
- テスト容易性が良い
- あるべき論的にも良いかも
- デメリット
- 途中から導入すると、かなり手間がかかる
4. トランザクション検証を行う
処理項番3で取得したトランザクション情報を検証し、商品の提供可否を判定する。
サブスクリプションの場合
verifyReceiptの結果(以降verifyReceipt
とする)のlatest_receipt_info[]
にて評価する。
以下、verifyReceipt.latest_receipt_info[n]
の主な評価ポイント。
キー | 概要 | チェック方法 |
---|---|---|
product_id |
商品ID | サービス側で定義している 商品IDと一致するか、チェックする |
original_transaction_id |
購入処理トランザクションID 新たに課金する都度、付与されるID |
既に管理されている(DBに存在する)IDの場合、 新規購入ではない可能性がある |
transaction_id |
トランザクションID 新規・継続など支払い都度付与されるID |
既に管理されている(DBに存在する)IDの場合、 商品提供が済んでいる可能性がある |
expires_date |
期限日時 | 期限切れかチェックする |
cancellation_date |
キャンセル日時 | 有効な日時が設定されている場合 キャンセルされている |
latest_receipt_infoの取り扱い
新しい順・古い順が変わることがあるらしい。順不同と思って扱うべき。
また、同一商品の通常価格品・セール品が有効になってるとか、不整合アラート目的で全てのレコードをチェックすると安心。
ポイントなど消費系商品の場合
verifyReceiptのreceipt.in_app[]
にて評価する。
以下、verifyReceipt.receipt.in_app[n]
の主な評価ポイント。
キー | 概要 | チェック方法 |
---|---|---|
product_id |
商品ID | サービス側で定義している 商品IDと一致するか、チェックする |
transaction_id |
トランザクションID 支払い都度付与されるID |
既に管理されている(DBに存在する)IDの場合、 商品提供が済んでいる可能性がある |
cancellation_date |
キャンセル日時 | 有効な日時が設定されている場合 キャンセルされている |
5. レシート情報を保管
後述のサブスク継続処理用にレシート情報を保管する。
verifyReceipt.latest_receipt
をDBなどに保管しておき、後の継続処理で利用する。
6. 商品提供を行う
サービスの要件に応じた商品提供を行う。
7. サブスクを継続する
サブスク期限の到来時に、処理項番5で保管しておいたレシート情報に基づいて、以下を実行する。
ノウハウ
課金の復元
- 復元機能を用意する
ユーザーが任意のタイミングで、課金状態を反映する機能を用意しないと、Appleアプリ審査通過できない。
前述の流れに従って、ユーザーに提供できていない商品をチェック・提供する機能が必要。
商品提供関連
-
事前チェック
ユーザーの状態(性別や年齢層など)に応じて、提供商品が異なる場合、早めにチェックする。
6. 商品提供を行う でエラーを返しても、決済が取り消されることはない。
返金対応などの業務が発生するため、アプリ側で課金する前に防ぐ。 -
返金の考慮
iOSアプリでの課金はユーザーが直接Appleに対して返金をリクエストできる。
サービスとApple側でユーザーの課金状態にズレが生じる可能性があるため、考慮が必要。
例えば、以下の方法で対応する。
・アプリ起動時に必ず課金状態をチェックするようにして、同期を取る
・バッチを用意して全Apple決済サブスクのチェックを定期的に行う
・App Store Server Notifications を利用する? -
決済保留の考慮
カード有効期限切れなどが原因で、Apple側で決済が保留になる可能性がある。
ユーザーが決済情報を更新した場合、引き続きサブスクが有効になるため、一定期間監視が必要。
支払いが認められた場合、トランザクション情報が更新されるため、商品提供を行う必要がある。 -
商品の多重提供対策
商品提供を行う前に、トランザクションIDを利用した提供済みチェックは慎重に行う。
同一AppleIDを決済アカウントとして、複数のアカウント登録しているユーザーがいる可能性がある。
ユーザーID×トランザクションIDで当該チェックを実施すると、同一決済の商品を多重に提供しかねない。
トランザクションIDで一意とする方が良いと思う。
Sandbox関連
-
Sandboxのサブスク期限
Sandboxのサクブス期限は5〜60分程度と短い。
サービス側でサブスク期限を独自に定義している場合(※)は、検証方法の考慮が必要。
※トランザクション情報の期限の当日23:59までなど -
売上計上関連
前述の通り、AppleレビュアーはSandboxレシートを送信してくる。
課金処理で売上関連テーブルに登録している場合は、注意が必要。(売り上げとして計上すべきでない)
本番検証関連
- 本番検証用のカード
本番検証を行う際に、AppleIDに法人カードを紐付けると課金できない可能性がある。
iTunesカード(Apple Gift Card)などを利用するか、個人のカードを利用して後で請求するとか、考慮が必要。