その課金処理、そんな実装で大丈夫か？

まとめようと思いつつ数年経ってしまい、今では当たり前かもしれませんが、課金処理における勘所をまとめます。

ソーシャルゲームによくある仮想通貨をサーバー側で持つ様な、Consumable タイプの課金処理について書いていきます。

## 課金処理が成功したら？

「課金処理が成功したらローカルに保存してトランザクションをクローズします」というような紹介をしているサイトをいくつも見てきましたが、これは Consumable タイプの実装においては間違いです。Apple, Google のドキュメントに、そのような内容は一切かかれていません。以下に Apple, Google のドキュメントを抜粋します。

### [In-App Purchase Programming Guide - Finishing the Transaction](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html#//apple_ref/doc/uid/TP40008267-CH5-SW10)

> Note: Don’t try to call the finishTransaction: method before the transaction is actually completed, attempting to use some other mechanism in your app to track the transaction as unfinished. StoreKit isn’t designed to be used this way. Doing so prevents your app from downloading Apple-hosted content and can lead to other issues.

***トランザクションが実際に完了する前に "finishTransaction:" メソッドを呼び出してはいけません。*** とありますが、***トランザクションが実際に完了する*** とは ***クライアントアプリケーションが購入情報をサーバーへ通知し、正常に仮想通貨を付与し、正常なレスポンスをクライアントアプリケーションが受け取ること*** です。

### [In-app Billing API - Consuming In-app Products](https://developer.android.com/google/play/billing/api.html#consume)

> Important: Before provisioning the consumable managed product in your application, you must send a consumption request to Google Play and receive a successful response indicating that the consumption was recorded.

*アプリケーションで消耗品管理商品をプロビジョニングする前に、消費をGoogle Playに送信し、消費が記録されたことを示す正常な応答を受け取る必要があります。・・・・・・* 

偉そうに大口を叩いてしまいましたが、反対のことを書いてありますね。。。
しかし、サーバー側でデータを持つ場合は、AppStore と同様、サーバー側での処理完了後に消費を送信する方が安心だと思われます。

そもそも、ローカルに保存したファイルが破損しない保証がありません。書き込み中に何らかの理由によりアプリがクラッシュしたり、デバイスの電源が強制的に落ちた場合、ファイルが破損する可能性があります。また、アプリ空間に保存したファイルはアプリが削除されると消えてしまい、二度と購入処理を完了することが出来ません。

未消費のトランザクションを取得する API は以下に記載されています。
[iOS - In App Purchase](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions)
[Android - In App Billing](https://developer.android.com/google/play/billing/billing_reference.html#getPurchases)

Web の情報を鵜呑みにせず、オフィシャルの情報を必ず確認しましょう。
現在は両プラットフォームとも日本語が用意されているようです。

## 様々なイレギュラー状態を考慮する

何を実装するにおいても正常系の実装は簡単です。
難しいのは、発生しうるイレギュラーな状態を補足し、リカバリーできる状態を作ることです。

様々な「こういう場合どうなる？」を考えひたすらテストして実装しましょう。
ファイルに読み書きしないだけでも考えることが減りましたね。
あとは、通信エラーとAPIのエラーをしっかり洗い出すだけです。

通信は失敗するものなので、失敗する前提で設計しましょう。
また、サーバーとの通信中や API 実行中に強制終了したり、ネットワークを切断したらどうなるか確認しましょう。
API の中のことは API の責任ですが、イレギュラーな操作が行われた際に、どのようなレスポンスを返すのか確認しておくことは重要です。

## 正しく実装できればそれで良い？

課金処理において、ドキュメント通りに正しく実装できているだけでは不十分です。

もっとも重要なのは、***イレギュラー発生時にユーザーが不安に思わないように振る舞うこと*** です。実際に請求が発生するので当然です。購入した仮想通貨がどのような状態であるのか、通知する必要があります。

- 購入処理が正常に終了したのか
- サーバーと通信中にエラーが発生し、リトライが必要なのか
- すでに購入処理中で、サーバー側での処理が未完了のため、通信だけ行うのか
- 親デバイスへの購入承認待ちなのか
- 購入上限額に達しているのか

などなど、まだありますが、これらのイレギュラー発生時のフローを丁寧に作り込み、「はい」「いいえ」を選択するだけで、購入処理を継続または中断をユーザーが簡単に選択できるようになっていれば、不安を感じさせることはないでしょうし、CS への問い合わせも大きく減らせることでしょう。もちろん中断の場合は、あとから購入処理を再開できることとその手順をお知らせします。

イレギュラーの一つとして、サーバーとの通信が正常終了し、購入完了をユーザーに通知後、プラットフォームへのトランザクションクローズが失敗する事がありえます。この場合、購入処理のリトライにより、もう一度サーバーへ通信が行われますので、レシートIDなどにより一度のみ実行されるようにしておきます。ユーザーへはもう一度購入完了通知が行われますが、大多数のユーザーはラッキーと思うくらいです。重要なのは ***ユーザーが不安に思わないように振る舞うこと*** です。

また、アプリ起動時やスリープからの復帰時に暗黙的なリトライ処理を入れるのがオススメです。


## まとめ

一般的に無料で遊べるソーシャルゲームアプリケーションにおいて、実際に請求の発生する課金処理は、アプリや提供元パブリッシャーへの信頼度を大きく左右することになります。ユーザーが安心して遊べるように、丁寧な実装を心がけましょう。