Purchase control feature
AutoyaのサポートするPurcahseには、RemoteとLocalの2種がある。
- サーバ有り Remote Purchase
- サーバなし Local Purchase
サーバあり版は認証とかがっつり絡んでいる。デフォルトの状態だとダミーサーバで動かせる。
サーバなしのやつは端末単体で動かせる。
どちらも、動くサンプルがここにあるんで、Autoyaを落としてPurchase.unityシーンを動かしてみるといいと思う。
ちなみにLocalPurchaseは通信がないのでAutoya経由ではなくモジュールを自前で使うことになる。Autoyaでサポートすべき範囲が狭い。
remote purchase
UnityIAPの機構を使った課金処理。
ゲームサーバと通信し、プレイヤーごとのきめ細かい課金処理を実装できる。
メリット
- 誰が何持ってるか管理可能
- この人にはこれ売ろができる
- アプデ無しでアイテムの追加
- お問い合わせに対応しやすい
デメリット
- 管理が面倒
- ハックされないわけではない
- つまりお問い合わせが来る
準備と動作のフロー
まずはクライアント側のみ。
- 各プラットフォームにアイテムをセット
- PurchaseSettings.csのURL調整
- アイテムを売る画面を作る
- Autoya経由で使う
1.各PFにアイテムをセット
指定するアイテム名についてはPF識別子(iOSとかAndroidとか)をつけとくと管理が楽になる。
2.PurchaseSettings.csのURL調整
設定ファイル上で、課金機構に関するURLを調整できる。
PurchaseSettings.cs
public const string PURCHASE_URL_READY = "https://httpbin.org/get";
public const string PURCHASE_CONNECTIONID_READY_PREFIX = "purchase_ready_";
public const string PURCHASE_URL_TICKET = "https://httpbin.org/post";
public const string PURCHASE_CONNECTIONID_TICKET_PREFIX = "purchase_start_";
public const string PURCHASE_URL_PURCHASE = "https://httpbin.org/post";
public const string PURCHASE_CONNECTIONID_PURCHASE_PREFIX = "purchase_succeeded_";
public const string PURCHASE_URL_PAID = "https://httpbin.org/post";
public const string PURCHASE_CONNECTIONID_PAID_PREFIX = "purchase_paid_";
public const string PURCHASE_URL_CANCEL = "https://httpbin.org/post";
public const string PURCHASE_CONNECTIONID_CANCEL_PREFIX = "purchase_cancelled_";
3.アイテムを売る画面を作る
開発者の方がんばってください。
4.Autoya経由で使う
次の3段階がある。
- 購入機構準備待ち
- 購入
- 購入後処理
(C/S全体のフローは後述)
購入機構準備待ち
IEnumerator Start () {
while (!Autoya.Purchase_IsReady()) {
Debug.Log("log:" + Autoya.Auth_IsAuthenticated());
Debug.Log("puc:" + Autoya.Purchase_IsReady());
yield return null;
}
Autoya.Purchase_IsReady()がtrueを返せば、準備完了。
初期化、起動時購入可能アイテムの取得処理は、Autoyaのほうで完備されている。 内部的には図のようなフローを経ている。
初期化に失敗した場合、bool Purchase_NeedAttemptReadyPurchase() 関数を実行することでストア処理の起動をする必要があるかどうか判断することができる。
結果 trueであれば、Purcahse_AttemptReadyPurchase 関数をApp側が実行する必要がある。
購入
準備完了後、クライアント中のどこでも、次のコードで購入処理を実行できる。
ちなみに同時に一件しか購入処理を実行できない。
Autoya.Purchase(
purchaseId, // 好きなidを振れる
"100_gold_coins", // 購入対象のプロダクト名をセット
pId => {// アイテム付与後のコールバック
Debug.Log("succeeded to purchase. id:" + pId);
},
(pId, err, reason, autoyaStatus) => {// 失敗後のコールバック
if (autoyaStatus.isAuthFailed) {
Debug.LogError("failed to auth.");
return;
}
if (autoyaStatus.inMaintenance) {
Debug.LogError("failed, service is under maintenance.");
return;
}
Debug.LogError("failed to purchase, id:" + pId + " err:" + err + " reason:" + reason);
}
);
アイテム付与後のコールバック
アイテム付与後のコールバック内で、次のことをするのを想定している。
- サーバに、このプレイヤーの最新のアイテム所持情報問い合わせ
- 終わったらローディングを停める とか想定
要はプレイヤーへと購入後の情報を見えるようにしようという処理を書く必要がある。
失敗後のコールバック
課金が失敗した時に呼ばれる。プレイヤーに理由を表示したりすると良い。
内部フロー
OverridePoints/OnTicketResponse を通過する。
クライアント(C)/サーバ(S)のやりとり全体のフロー
ここまでで、クライアント側だけ見た場合の課金処理の扱いは完了する。 ここからAutoyaが前提としているサーバ側のフローを含めた行程について解説する。
クライアント、サーバ合わせて、次の様な12段階の処理が存在する。
1. 購入機構初期化+リクエスト(クライアント)
2. ☆購入機構準備待ち(クライアント)
3. 購入可能アイテム一覧配布(サーバ)
4. ☆購入(クライアント)
5. 購入チケットリクエスト(クライアント)
6. 購入チケット記録(サーバ)
7. 購入チケットレスポンス(サーバ)
8. 購入確認リクエスト(クライアント)
9. 購入確認処理(サーバ)
10. 購入確認レスポンス(サーバ)
11. 購入後事前処理(クライアント)
12. ☆購入後処理(クライアント)
1. 購入機構初期化+リクエスト(クライアント)
クライアントが起動すると、課金機構の初期化には「どんな商品があるか」プラットフォームと確認する必要があるため、サーバに「このプレイヤーに提示するアイテム一覧くれ」というリクエストが行く。
ここでアイテム情報はApp全体で扱う全てを出す必要がなく、例えばプレイヤーごとにセール品を出す、など、調整したアイテム一覧をサーバが発行できると柔軟性が出る。
2. ☆購入機構準備待ち(C)
クライアントは準備完了まで待つ。
3. 購入可能アイテム一覧配布(サーバ)
サーバはリクエストを受け取ってアイテム一覧を返す。
クライアントは一覧を使って課金機構を初期化。準備完了となる。
サーバが返すべき購入可能アイテム一覧は
ProductInfos型
json例
{
"productInfos": [{
"productId": "100_gold_coins",
"platformProductId": "100_gold_coins_iOS",
"isAvailableToThisPlayer": true,
"info": "one hundled of coins."
}, {
"productId": "1000_gold_coins",
"platformProductId": "1000_gold_coins_iOS",
"isAvailableToThisPlayer": true,
"info": "one ton of coins."
}]
}
OverridePoint.csを書き換えて、サーバから受け取ったアイテムを使うように変更。
4. ☆購入(クライアント)
買いたいものを指定して、購入処理を開始。
どんな商品が購入可能かは、
var products = Autoya.Purchase_ProductInfos();
で取得できる。
購入時の商品のidは、
string product.productId
を使う。
5. 購入チケットリクエスト(C)
購入情報をサーバに生成してもらうため、リクエストを送る。
6. 購入チケット記録(サーバ)
サーバはリクエストを受け取り、購入チケットを生成。購入を記録開始。
7. 購入チケットレスポンス(S)
プレイヤーに購入を許可する場合はレスポンスコード200とチケットを返す。
それ以外ならエラーを返す。クライアント側は200以外を失敗とみなす。
チケットの要素は、
- GUID
- productId
- 日時
- もちろんuserId
- 状態(new, cancelled, done)
など。
クライアントに返すチケットの情報はGUIDだけでOK。
どんな何を買おうとしてるか、ここでサーバでしっかり保存しておくと、プレイヤーの意図確認をサーバに残せてよい感じになる。
クライアント側は、200とチケットを受け取ったら処理継続、それ以外ならFailハンドラで停まる。
- 購入チケットレスポンス(サーバ)
と
- 購入確認リクエスト(クライアント)
には隙間があって、
ここでプラットフォームの「買う? Y/N」画面が出る。
買うなら継続、買わないなら中止。
図にはないけれど、購入キャンセルの場合、キャンセルの通信がサーバへと飛び、処理はFailで中止される。
8. 購入確認リクエスト(C)
購入処理が終わったので、チケットとレシートをサーバへと送る。
9. 購入確認処理(サーバ)
チケットは本物か? 記録と照合。レシートは本物か? PFに問い合わせたり。本物だとして過去に重複したものはないか?
まとめてある
etc.
Androidの場合は、レシートにpayloadを入れてあるため、それを使った確認も可能。
10. 購入確認レスポンス(サーバ)
チェックがOKだったら、200を返す。それ以外ならそれ以外を。
11. 購入後事前処理(C)
200が来た場合は、PFに問い合わせて権利の解決を行う。
それ以外の場合は、独自に動作を書く。
ここはサーバ側チェックによって色々実装を変えるべき場所で、200以外で来た場合このコードならこう、とかを書く必要がある。
12. ☆購入後処理(クライアント)
というわけでここまでで一切の契約処理は完了した状態で、コールバックが呼ばれる。
イレギュラーケースについて
大量のイレギュラーケースがありえる。
なんたって通信が多い上に、状態を保持している地点が3箇所も存在する。
しかもクライアントはまあ落ちる。
クライアントが落ちると?
大きく3つ、落ちパターンがある。
- PFとの契約が発生する前に落ちるか、
- PFとの契約が発生した後解決前に落ちるか、
- PFとの契約が解決した後に落ちるか。
1. 契約発生前
チケットが無駄になるだけで済む。サーバは気にせず、クライアントが来たら新しいチケットを生成してあげるといい。
3. 契約解決後
さきに3を紹介。
こちらは、すでに契約解決がなされているので問題ない。
むしろなぜ落ちてしまったのかが気になる。
2. 契約発生後解決前
問題のところ。ここで落ちるパターンは2つある。
- アイテム配布前
- アイテム配布後
2-1 アイテム配布前
9の前、8の最中に落ちたらどうなるか。
購入は完了していて契約が残っていて、アプリが落ちた、という状態になっている。ここでいう記録は、プラットフォーム側(端末 + プラットフォームのサーバ)に残っている。
ストアを初期化する時、未完了の契約がPFから現れ、クライアントからサーバへと送付される。
通常のケースとの違いとしては、
- そもそもURLが違う
- チケットが送られてこない
あたり。
チケットは送られてこないので、レシートの検証だけして200を返せばいい、、のか?
とりあえず次を見てみよう。
2-2 アイテム配布後
10のあと、11完了までに落ちたらどうなるか。
アイテム配布後、契約が残った状態、になる。
ストアを初期化する時、未完了の契約がPFから現れて、クライアントからサーバへと送付される。
通常のケースとの違いとしては、
- そもそもURLが違う
- チケットが送られてこない
あたり。そして、
2-1との違いが本当に一箇所だけあり、もうアイテムは付与されている(赤字)。
というわけで、200を返すが、別にアイテムをさらに付与はしない、という感じになる。
2-1,2-2のケースは、同じルートで状況の異なる課金処理が発生することを意味している。
2-1はまだアイテム配布してない。2-2はもう配布が済んでる。
差はどこで見るか。
- 未完了Tiketの有無
- レシート検証時色々チェック
1. 未完了Tiketの有無
プレイヤーについて、未済のチケットがない場合、おかしい。購入意図を発揮するシーケンスを飛ばして購入処理が進むはずはない。
or
チケットとレシート内容が違う場合、おかしい。
などで対応。
2. レシート検証時色々チェック
チケットまで偽造して来る場合、
まとめてある
ちなみにこの2-1, 2-2の課金ルートは不正にも使われやすい。
怪しいプレイヤーはマークされることが多い。
local purchase
UnityIAPの機構を使った課金処理。
ゲームサーバと一切通信せずに課金処理を実装できる。(もちろんプラットフォームとは通信する。)
メリット
- サーバがいらない
デメリット
- アップデートでしか商品内容を更新できない
- ハックされ放題
- お問い合わせの不確実性が増す
準備と動作のフロー
- 各プラットフォームにアイテムをセット
- エディタでRVObfuscatorに情報をセット
- 商品情報をPurchaseSettings.csに記述
- アイテムを売る画面を作る
- LocalPurchaseRouterをnewして使う
1.各PFにアイテムをセット
指定するアイテム名についてはPF識別子(iOSとかAndroidとか)をつけとくと管理が楽。
具体例は3.あたりを。
2.Receipt Validation Obfuscatorに情報をセット
この工程でローカル検証用の暗号化コードが生成される。
3.商品情報をPurchaseSettings.csに記述
PurchaseSettings.cs
ここに書いたアイテムだけが売買される。
/*
immutable purchasable item infos.
*/
public static readonly ProductInfos IMMUTABLE_PURCHASE_ITEM_INFOS = new ProductInfos {
productInfos = new ProductInfo[] {
new ProductInfo("100_gold_coins", "100_gold_coins_iOS", true, "one hundled of coins."),
new ProductInfo("1000_gold_coins", "1000_gold_coins_iOS", true, "one ton of coins."),
new ProductInfo("10000_gold_coins", "10000_gold_coins_iOS", false, "ten tons of coins."),// this product setting is example of not allow to buy for this player, disable to buy but need to be displayed.
}
};
iOS/Androidの両方で出す場合:
/*
immutable purchasable item infos.
*/
public static readonly ProductInfos IMMUTABLE_PURCHASE_ITEM_INFOS = new ProductInfos {
productInfos = new ProductInfo[] {
#if UNITY_IOS
new ProductInfo("100_gold_coins", "100_gold_coins_iOS", true, "one hundled of coins."),
#elif UNITY_ANDROID
new ProductInfo("100_gold_coins", "100_gold_coins_Android", true, "one hundled of coins."),
#endif
などをやっておくと楽に対応できる。
4.アイテムを売る画面を作る
開発者さん頑張ってください。
5. 購買機構をnewして使う
ここからコードの話になる。
実際のゲーム中での購買処理は、
- LocalPurchaseRouterの初期化
- 購入
- 商品の提供
の3段階に分かれる。
LocalPurchaseRouter初期化
localPurchaseRouter = new LocalPurchaseRouter(
PurchaseSettings.IMMUTABLE_PURCHASE_ITEM_INFOS.productInfos,
() => {
Debug.Log("ready purchase.");
},
(err, reason) => {
Debug.LogError("failed to ready purchase. error:" + err + " reason:" + reason);
},
alreadyPurchasedProductId => {
/*
this action will be called when
the IAP feature found non-completed purchase record
&&
the validate result of that is OK.
need to deploy product to user.
*/
// deploy purchased product to user here.
}
);
解説していく。
localPurchaseRouter = new LocalPurchaseRouter(// アプリケーションの起動時にインスタンスを生成
PurchaseSettings.IMMUTABLE_PURCHASE_ITEM_INFOS.productInfos, // 設定ファイルに書かれているプロダクトをプラットフォームに
() => {// 購入準備が完了
Debug.Log("ready purchase.");
},
(err, reason) => {// 準備エラーが発生
Debug.LogError("failed to ready purchase. error:" + err + " reason:" + reason);
},
alreadyPurchasedProductId => {
/*
this action will be called when
the IAP feature found non-completed purchase record
&&
the validate result of that is OK.
need to deploy product to user.
*/
// deploy purchased product to user here.
}
);
準備エラー
まあ理由はさまざま
errに種類、reasonに理由が入るので、アイテムを購入したければこうするといいよ、みたいな旨を書こう。
解決せずに再度インスタンスを初期化すれば全く同じフローが発生する。
localPurchaseRouter = new LocalPurchaseRouter(
PurchaseSettings.IMMUTABLE_PURCHASE_ITEM_INFOS.productInfos,
() => {
Debug.Log("ready purchase.");
},
(err, reason) => {
Debug.LogError("failed to ready purchase. error:" + err + " reason:" + reason);
},
alreadyPurchasedProductId => {// このブロックは、特殊な状況下で実行される。
/*
this action will be called when
the IAP feature found non-completed purchase record
&&
the validate result of that is OK.
need to deploy product to user.
*/
// deploy purchased product to user here.
}
);
第四引数のハンドラについては、後述する。
インスタンスを作成して一つ目のハンドラが着火すれば、このインスタンスからアイテムの購入が可能になる。
購入
買いたいものを指定して、購入処理を開始。
どんな商品が購入可能かは、
var products = localPurchaseRouter.ProductInfos();
で取得できる。
次のようなコードで購入処理ができる。
localPurchaseRouter.PurchaseAsync(// PurchaseAsyncで購入開始
purchaseId, // 購入ごとにIDをセットすることが可能
"100_gold_coins", // 購入したい商品のプロダクトID文字列
purchasedId => {// 購入完了のハンドラ
Debug.Log("purchase succeeded, purchasedId:" + purchasedId + " purchased item id:" + "100_gold_coins");
// deploy purchased product to user here.
},
(purchasedId, error, reason) => {// 購入失敗のハンドラ
Debug.LogError("failed to purchase Id:" + purchasedId + " failed, error:" + error + " reason:" + reason);
}
);
第2引数には買う商品のproductIdを入れる。
ここでは100_gold_coinsを買う。ちゃんと設定にあるもの以外は設定できない。
購入完了のハンドラが着火した場合
プレイヤーへとこのアイテムの付与を行おう。例えばアイテムの所持数を増やして表示、とか。purchasedIdには第一引数に入れたのが来る。
購入失敗した場合
プレイヤーに何かを伝えるチャンス。すでに購入処理はキャンセルされている。
これで購買処理の話は終わり。
購入失敗について
alreadyPurchasedProductId => {
/*
this action will be called when
the IAP feature found non-completed purchase record
&&
the validate result of that is OK.
need to deploy product to user.
*/
// deploy purchased product to user here.
}
インスタンス初期化の第4引数の解説をする。 このハンドラが着火するケースはかなり厄介で、Purchase処理中にAppが落ちた後、再度起動した際に着火する。
- Q.購入処理中にアプリが落ちたらどうなるの
- A.購入処理が彷徨う。
どう彷徨うかは、どのタイミングで落ちたかによる。
内部的には次のようなフェーズがある。
- 購入前
- 購入後
- アイテム付与後
1.購入前
プレイヤーに、OS提供の「購入しますか」が現れる前 or 現れている状態。
ここでアプリが落ちても平気。課金は始まってない。
プレイヤーがyesって押すと、PFとの通信後、2.購入後に遷移する。
2.購入後
購入が終わり、プレイヤーに価値 = お金の対価を渡す必要がある地点。
ここで
localPurchaseRouter.PurchaseAsync(
purchaseId,
"100_gold_coins",
purchasedId => {// アイテム配布完了
Debug.Log("purchase succeeded, purchasedId:" + purchasedId + " purchased item id:" + "100_gold_coins");
// deploy purchased product to user here.
},
アイテム配布完了時の関数が呼ばれる。
この部分でプレイヤーにお金の価値を渡したのち、3.アイテム付与後 に遷移する。
ここで対価付与前にアプリが落ちると、お金は払ったんだけど対価をもらってない 状態になる。
つまり権利が行使されないまま彷徨う。
で、再度アプリを起動すると、
localPurchaseRouter = new LocalPurchaseRouter(
PurchaseSettings.IMMUTABLE_PURCHASE_ITEM_INFOS.productInfos,
() => {
Debug.Log("ready purchase.");
},
(err, reason) => {
Debug.LogError("failed to ready purchase. error:" + err + " reason:" + reason);
},
alreadyPurchasedProductId => {// ここが着火する
/*
this action will be called when
the IAP feature found non-completed purchase record
&&
the validate result of that is OK.
need to deploy product to user.
*/
// deploy purchased product to user here.
}
);
第四引数のハンドラに、未処理の課金済みの情報 が来る。
PurchaseAsyncで単に購入がfailしたのなら来ない。
successしててかつ付与が終わらずにいると、来る。
来たらどうする?
課金済んでるんで、アイテムを配布。
このハンドラが呼ばれるということは、いろんなチェックが済んでいるのでアイテムを付与して問題ないことを指す。
いろいろなチェックについては次項を参照。
いろいろなチェック
local purchaseのデメリット
- アップデートでしか商品内容を更新できない
- ハックされ放題++
課金処理は改ざんのメッカで、根本的には諦めと諦めさせの話になる。
よくある手口
- 通信レスポンスを偽造
- セーブデータを弄ってry
- アプリを解析してメモリに直に足す
通信はMATE攻撃できちゃう
サーバもいないし自由。
セーブデータに関して
暗号化して嫌がらせ。
でもアプリを解析
いかんともしがたい。頑張って耐タンパー。具体的にいうと対策ツール(なんちゃらProofとか)を使ってAppを耐タンパーにしよう。
詳しい記事
セキュリティエンジニアからみたUnityのこと
スマートフォンゲームのチート事情
どれが課金関連?
全部関連する。
一応防ごうというレベルでいうと、MATEで通信いじった時にレシートを偽造されても、そのデータを受け付けない、みたいなところまではいける。
特に防ぐといいこと
同一レシート問題
- 処理時、処理したレシートを記録
- 過去の記録と照合して蹴る
- OKであれば追記
今回の課金機構には、内部で「同一レシートが来たかどうかチェックする機能が入る場所」が開けてある。
カスタマイズして備えてください。