Grani Engineering Blog

株式会社グラニはC#を中心として、ASP.NET、Unity、VR開発を行っています。

Prime31 Ver2.17以降のStoreKitプラグインを利用したiOS課金実装(レシートの取扱について)

 はじめまして!アプリケーション開発部の長内です。 突然ですが、UnityでiOSアプリ内課金を実装するとしたら何を利用しますか? Unity5.3以降であればUnity IAPを検討する方もいるでしょう。

 Unity IAPであれば、iOS App Store や GooglePlay だけでなく Windows Store や Amazon Appstore といった様々なストアなどに対応しており、さらにプラットフォーム毎の固有実装を気にせず共通のインターフェイスでゲーム側に組み込めるのも魅力的です。

実際は?

 しかし、現状の殆どの場合はUnityアセットストアで手に入るプラグインを利用する方が多いかと思います。

 そのアセットストアでiOSのアプリ内課金といえば、Prime31 さんからリリースされていますiOS StoreKit In App Purchase Pluginではないでしょうか。

 このアセットは、Unityがまだ3.2、3.3のバージョン頃からあらゆるスタジオなどでも採用され続けている古株のアセットです。

 やはり、昔から利用されているだけあってGoogleでUnityによるiOSアプリ内課金を検索すると候補として上がる程です。

見えないユーザーとの売買

 脱線したお話になりますが、通常レシート(以下領収書データ)は、プラットフォームストア(ここではGooglePlayStoreやiTunesStoreを指します)からユーザーの端末に直接送信されます。

 この時には既に決済は完了しているので、ゲームとしては対応する商品を付与してしまっても、売買のやり取りとしては成立しているのでOKです。

リアル環境なら

 では、簡単なシーケンス図で表してみましょう。

f:id:yosagrapon:20170525153637p:plain

 これが、現実世界でコンビニやスーパーマーケットなどであれば全く問題はありませんね、直接お客様とお店との取引をしていますので、これで成立します。

領収書をもらったことにしよう!

 しかし、このやり取りが電子世界になった途端、そうではいけなくなってしまいます。例えば次のパターンの時はどうでしょうか。

f:id:yosagrapon:20170525153319p:plain

 マズイです、実にマズイです。お客様はなんの売買取引をしていないのに、ゲームサーバーに領収書をもらったとしか言っていないだけでゲームサーバーは言われたアイテムを付与してしまっています。

貰ったことを言うのではなく、その物を渡しましょう

 では、領収書をもらったと言うのではなく、領収書そのものを渡しましょう。

f:id:yosagrapon:20170525153323p:plain

 お客様はちゃんと売買取引を行い、プラットフォームストアから受け取った領収書を渡しているので、ゲームサーバーとしても領収書を受け取っているのなら問題はなさそうです。

はたしてそれは本物?

 しかし、お客様の中でとてつもなくプラットフォームストアの領収書データに精通し有りもしない領収書データを作れた場合はどうなるでしょうか?

f:id:yosagrapon:20170525153327p:plain

 ダメです。実にダメです。結局、お客様は売買取引をせず偽物の領収書データを作って、あたかも売買取引したかのように振る舞った領収書をゲームサーバーに送りつけゲームサーバーは領収書があるから大丈夫と思い、アイテムを付与してしまいました。

本人確認は重要ですね

 では、この問題に対してどう対処するかというと、至極単純です。その領収書を発行した本人に確認を取れば良いのです。あらゆるサイトでも紹介されていてデファクトスタンダードな実装の簡易シーケンスを見てみましょう

f:id:yosagrapon:20170525153925p:plain

 本来はもっと複雑なフローなのですが、簡略的に書いた場合はだいたいこうなります。

また、これが本題では無いのでまたいつかということでおひとつ。

本題

 では、そんな昔からあるアセットの実装方法を今更何を書くのかというと、レシートの取り扱い方が変わったことについてです。

 Prime31さん公式サイトのあらゆるアセットのリリースノートに以下のような事が書かれています。

iOS StoreKit09-23-2016

Version 2.17

  • rebuilt against iOS 10 SDK

  • removed base64 encoded receipt as it has been deprecated by Apple since iOS 7

  • bug fix: avoid restoring the same transactions multiple times even if Apple sends them over and over again

このリリースノートでもっとも重要な内容は

removed base64 encoded receipt as it has been deprecated by Apple since iOS 7

の部分です。

 そうです。この base64 encoded receipt がiTunesStoreから送られてくるレシートそのものなのです。

これが削除されたということは、正しいレシートをゲームサーバーに送ることが出来ませんし Unityマニュアルでもこんな(base64エンコードされたレシートを送信)ことを書いているので削除されたら何も出来ません と見せかけて、そんなことはありませんでした。

 そのかわりのAPIとして

public static string getAppStoreReceiptLocation()

が増えましたので、こちらを使います。

 この関数は、iTunesStoreから受け取ったレシートデータをローカルストレージに保存した"生のレシートデータ"へのファイルパスが返されます。

 なので、このままファイルを読み込んでそのまま送ったとしても結局不正なレシートとして扱われてしまいますのでBase64エンコードから送信までこちらで実装します。

/// <summary>
/// StoreKitManager の purchaseSuccessfulEvent イベントから監視オブジェクトとして使います
/// </summary>
/// <returns>purchaseSuccessfulEvent イベントの監視オブジェクトを返します</returns>
private static IObservable<StoreKitTransaction> PurchaseSuccessfulEventAsObservable()
{
    // event機構からObservableへ
    return Observable.FromEvent<StoreKitTransaction>(x => StoreKitManager.purchaseSuccessfulEvent += x, x => StoreKitManager.purchaseSuccessfulEvent -= x);
}

グラニでは、UniRxを使った実装をしていますので、StoreKitライブラリのpurchaseSuccessfulEventを監視するようにします。

var successObservable = PurchaseSuccessfulEventAsObservable().First().PublishLast().RefCount()
    .SelectMany(transaction =>
    {
        // レシートデータを読み込んでBase64エンコードする
        var receiptUri = new Uri(StoreKitBinding.getAppStoreReceiptLocation());
        var rawReceipt = File.ReadAllBytes(receiptUri.LocalPath);
        var base64Receipt = Convert.ToBase64String(rawReceipt);


        // レシートを生成して検証する
        var receipt = new PlatformStoreReceipt(storeItem, base64Receipt, string.Empty, userOption);
        return ValidateReceiptAsync(receipt);
    })

 そうです。ここで実装している"base64Receipt"こそが、2.17より前のバージョンで存在していた"base64EncodedTransactionReceipt"の写し身です。

 あとは、ゲームサーバーとゲームクライアントの検証結果を元にどうするかという実装を、ゲーム側仕様に則って実装します。

 以上で、まさかの本題より前書きのほうが長い(尺稼ぎ)記事になってしまいましたが、iOS10であるとか新しいバージョンにしなければいけなくなったが突如APIインターフェイスが変わった! と慌てず、まずは今回の実装を参考に実装を修正してみて下さい。殆どのケースでは上手くいきます。(絶対にとは言っていない)