スマートフォン課金実装(Unity IAP)について

#イントロ
課金実装にはアプリケーションからの購入処理、サーバ側での購入情報の検証処理/アイテムやゲーム内通貨の付与など、クライアントとバックエンドの連携が必要となります。
この投稿は、アプリ課金の基本事項の解説となります。

#プラットフォームについて
AndroidはGooglePlay、iOSはAppStoreでの購入となるため、それぞれのプラットフォームごとに購入の仕組みはことなります。
プラットフォーム事の差分を含め解説していきます。

#購入の種類について
まず、アプリ内課金にはいくつかの方式があります。
Android/iOSどちらも以下の購入実装が可能です。
####1.消費型購入(Consumable)
何度でも購入可能で、主にゲーム内通貨や消費型アイテムの購入などに使用されます。購入の詳細に関してはこの消費型購入を例に解説していきます。
####2.非消費型購入(Non-Consumable)
1度だけ購入可能で、主に広告削除、無料体験版などから製品版へのアンロックなどに使用されます。
####3.自動更新購読型購入(Auto-Renewable Subscription)
定期購読購入。後述する更新購読も同様ですが、期限付きの購入となりますが、ユーザー解除しない限り、自動で購入が更新されます。
月額会員サービスなどの場合に利用します。
####3.非更新購読型購入(Non-Renewable Subscription)
こちらも期限付きの購入となりますが、自動更新型とことなり自動での購入の更新は行われません。時間ベースでの機能解放などに利用できます。

#消費型購入の実装/動作フロー

###おそらくアプリゲーム業界的に一番使用するであろう消費型購入を例に購入フローを例に開設します。

![000.png](https://qiita-image-store.s3.amazonaws.com/0/311234/58c2d451-4556-a5f1-4d7b-b1b1534706ed.png)


**1**.　まず購入の前準備として各プラットフォームに購入プロダクト情報の取得をかけます。購入プロダクトは事前に各プラットフォームにて登録しておく必要があります。

**2**,　1のプロダクト取得リクエストに対して、プラットフォームのストアはプロダクト情報を返します。購入のタイプ/値段/プロダクトの有効無効などの情報が含まれます。

**3**.　2の情報をもとにアプリから各プラットフォームストアに購入依頼処理をかけます。こちらの処理をかけると各ＯＳの対応プラットフォームの購入画面が表示され、決済確認等が行われます。各プラットフォーム側での決済処理に関してはアプリ側から割り込みをかけるなどがガイドラインで禁止されており、決済完了、決済失敗、購入のキャンセルなどの情報がアプリに戻されるまで待つことになります。

**4**.　購入が成功した場合、そのプロダクトの購入情報が戻ってきます(レシート)。このレシートが購入を実際に行ったという証明になるため、とても重要なものになります。

**5**.　4で取得した購入レシート正規のものであるのか、どのプロダクトの購入を証明するのか、購入自体が有効なのかを検証する必要があります。近年アプリ側で直接レシートを検証する手段を確立されていますが、消費型アイテム購入かつゲーム内通貨/アイテムの管理をバックグラウンドのサーバ側で行っている場合、購入情報をサーバ側で検証するのが安全です。**購入レシートをサーバー側へ送信します。**
非消費型購入や定期購読などのプラットフォーム側ストアに購入情報が保持されており、購入レシートの取得、復旧(リストア)が可能な場合はローカル検証でもいいかもしれません。

**6**.　レシートを受け取ったサーバはそのレシートが正規のものなのか検証します。各プラットフォームごとに検証方法が用意されていて、Appleの場合ではレシート検証用のサーバーが用意されています。
Androidの場合はレシート情報とPublicKeyを使用し、OpenSSL認証などを行うため、Platformのレシート検証サーバに問い合わせたりはありません。以下の7のリザルトもありません。

**7**.　検証の結果が返ってきます。正規のものであれば購入に対する対価/動作などを行います。レシート情報が不正なものの場合は該当するエラーなどが返ってきます。

**8**.　レシート検証の結果購入が正規のものであると判定された場合に、購入に対する対価の付与機能の解放などを通知します。消費型アイテムの場合正常に処理が完了された場合にアプリ側でプロダクトの消費などを行う必要があります。
消費処理をかけることで同じプロダクトの再購入がかけられる状態となります。

#UnityIAPセットアップ

![001.png](https://qiita-image-store.s3.amazonaws.com/0/311234/e8e7ffd2-7c64-9fd5-11d0-3fe64050249e.png)
まずServicve ウインドウを開きます


![002.png](https://qiita-image-store.s3.amazonaws.com/0/311234/81796e62-634a-cc90-15f1-96143793a8a6.png)
Unityアカウントとのプロジェクト連携ができていない場合(作成時に有効にしていなかった場合)
作成を求めるのでアカウントを選択してCreateします。
(UnityIAPを使うためにはUnityアカウントとの連携が必要ですので複数人作業時などアカウント周り注意)

![003.png](https://qiita-image-store.s3.amazonaws.com/0/311234/e17cebc4-3f72-fc08-c36c-1a4572edc56c.png)
In App Purchasingを有効化します。
(同時にAnalyticsも有効化されます。)

![004.png](https://qiita-image-store.s3.amazonaws.com/0/311234/40e11757-5402-2141-625d-33f977c186c4.png)
有効にします。


![005.png](https://qiita-image-store.s3.amazonaws.com/0/311234/61e8d4c3-d39f-01ed-26d4-5478939501ee.png)
13歳未満を対象としたアプリか確認があります。
メインターゲットが幼児向けなどの場合はチェックを入れましょう。

![006.png](https://qiita-image-store.s3.amazonaws.com/0/311234/2d352292-e4b1-c7e2-7c68-1fb34805179d.png)
セットアップは完了です。必要なアセット類をインポートしましょう。
※青枠部分はGoogle Play Consoleでアプリ事に発行されるPublicKeyの入力欄です。


#基本実装
消費アイテムを購入するためにアプリ側で実装するものとして最小で以下の実装例で解説します。UnityIAPを利用すれば非常にシンプルな実装とすることができます。


実装としては以下のようなコンポーネントを作成します。

```c#:TestBilling.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;

public class TestBilling : MonoBehaviour, IStoreListener
{

    //購入実行に使用
    IStoreController Controller { get; set; }
    //リストア処理など拡張機能で使用
    IExtensionProvider Extensions { get; set; }

    // Use this for initialization
    void Initialize()
    {

        //builderを作成しプロダクトを登録する。
        StandardPurchasingModule module = StandardPurchasingModule.Instance();
        //Uエディタ検証用の表示を作ってくれる
        //module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser; 
        ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);

        //②プロダクト(アイテム)の登録第
        builder.AddProduct("jp.hogehoge.1yen", ProductType.Consumable);
        //②以下のようにAndroid、iOSで別のproduct_idでもアプリ上で一つのプロダクトとして登録することもできる。
       /* 
        * builder.AddProduct("jp.hogehoge.1yen", ProductType.Consumable, new IDs
                    {
                    { "jp.hogehoge.android.1yen", GooglePlay.Name },
                    { "jp.hogehoge.ios.1yen", AppleAppStore.Name }

        });
        */


        //IAP初期化
        UnityPurchasing.Initialize(this, builder);

    }

 
    //初期化成功イベント
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        //成功すると購入やリストアに使用するインターフェースが返ってくるので保持しよう

        Controller = controller;
        Extensions = extensions;
    }

    //初期化失敗イベント
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        //初期化失敗の場合errorには以下の場合がある。
        //エラーによって警告表示やストア表示を変えるなどの分岐、ログ取得などを記述するといい


        //ストアがアプリケーションを認識していない場合
        //InitializationFailureReason.AppNotKnown 

        //有効なプロダクトがない場合
        //InitializationFailureReason.NoProductsAvailable

        //端末の購入が許可になっていない場合
        // InitializationFailureReason.PurchasingUnavailable
        
    }

    //アプリ内での購入ボタンを押したときなどの処理
    public void Purchasing()
    {
        //登録したプロダクトで購入をかける
        var product = Controller.products.WithID("jp.hogehoge.1yen");
        Controller.InitiatePurchase(product);

        //上記はproductを取得し、購入をかけているが実際にはしたのようにproduct}_idの指定だけでも購入はかけられる。
        // Controller.InitiatePurchase("jp.hogehoge.1yen");
        
    }

    //購入成功イベント
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    {
        //Productを取得する。
        //レシート情報などが格納されている
        Product product = e.purchasedProduct;
        string receipt = e.purchasedProduct.receipt;
        //通貨コード(Currencyなどが取得可能)
        ProductMetadata metaData = product.metadata;
        //↓通貨コード。サーバで購入時通貨など収集するときは送ってあげたりする
        //metaData.isoCurrencyCode
        //isoCurrencyCodeでの値段
        //metaData.localizedPrice

        //プロダクトの詳細(UnityIAPのID、ストアにおけるID、プロダクトのタイプのみ)
        ProductDefinition definition = product.definition;
        //↓こっちはUnityIAP上のID
        //definition.id   
        //↓こっちは各プラットフォームストアごとのプロダクトのID 
        //definition.storeSpecificId


        //ここらへんでアプリのバックエンドサーバにレシートを渡すなどして、レシート検証とアイテムの付与などを行わせる。

          
        //PurchaseProcessingResult.Pending とした場合には購入完了扱いにはならない(サーバでのレシート検証を待ってから終了とする場合など)
        //ConfirmPendingPurchase(明示的に消費処理)がかけられるまでProcessPurchaseが呼び出され続けるようになる
         //PurchaseProcessingResult.Completeはすぐに購入消費完了扱いとなりアイテムの消費などが行われる
        return PurchaseProcessingResult.Pending;
    }

    //購入失敗イベント
    public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
    {

        //購入処理が失敗した場合に呼び出される
        //以下のようなパターンがある

        //購入機能自体が利用できなかったとき
        //PurchaseFailureReason.PurchasingUnavailable

        //購入処理中に再度購入をかけた場合
        //「無料で復元できます～」の場合もこれだといいなぁ
        //もしそうだった場合消費型アイテムでも事実上productからリストア処理かけられるかも ?(要検証)
       // PurchaseFailureReason.ExistingPurchasePending

        //購入をかけたプロダクトが無効だった場合など
        //PurchaseFailureReason.ProductUnavailable

        //購入情報のsignatureに問題があった場合
        //PurchaseFailureReason.SignatureInvalid

        //ユーザーが購入をキャンセルしたとき
        //PurchaseFailureReason.UserCancelled

        //支払い時に問題があった(決済の失敗など)
        //PurchaseFailureReason.PaymentDeclined)

        //他エラーに該当しない場合これが返る(たぶん)
        //PurchaseFailureReason.Unknown)

    }

}
```
##初期化
IAPの初期化にIStoreLisnterインターフェースを継承したクラスが必要でなため継承させます。
今回は初期化、購入まですべてこのクラスで行っていきます。

IAPを初期化する処理がStart()内に書かれてております。
プロダクトの登録とストア接続などの初期化処理を行います。
プロダクトの登録はbuilderのAddProduct()にて行います。
iOS/Androidのマルチプラットフォームを想定する場合、プロダクトIDは各アイテムごとに共通のものを使用されるかと思いますが、もしプラットフォーム事に異なるプロダクトIDを作成したとしても同一プロダクトとしてIAP処理を管理することができます。

```c#:TestBilling.cs

// ~~~~~~~~~~~~略~~~~~~~~~~~~

void Initialize()
{

    //builderを作成しプロダクトを登録する。
    StandardPurchasingModule module = StandardPurchasingModule.Instance();
    //Uエディタ検証用の表示を作ってくれる
    //module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser; 
    ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);

    //②プロダクト(アイテム)の登録第
    builder.AddProduct("jp.hogehoge.1yen", ProductType.Consumable);
    //②以下のようにAndroid、iOSで別のproduct_idでもアプリ上で一つのプロダクトとして登録することもできる。
    /* 
    builder.AddProduct("jp.hogehoge.1yen", ProductType.Consumable, new IDs {
        { "jp.hogehoge.android.1yen", GooglePlay.Name },
        { "jp.hogehoge.ios.1yen", AppleAppStore.Name }
    });
    */


    //IAP初期化
    UnityPurchasing.Initialize(this, builder);
}

// ~~~~~~~~~~~~略~~~~~~~~~~~~

```
ここでthisを渡しておりますが、IStoreLisnerを別コンポーネントとしている時などはそちらを話つようにします。

初期化の実行結果を受け取るため以下のメソッドを実装します。両方ともIStoreLisnterインターフェースの実装であるため必須なものとなります。
・OnInitialized()
・OnInitializeFailed()

メソッド名の通りそれぞれ成功時、失敗時にどちらかのイベントが呼ばれるます。
成功時は購入処理などに必要なIStoreController、IExtensionProviderが受け取れるので保持しておきます。
失敗時はその理由が判定できるためそれぞれ適切な処理(ユーザーへの警告の表示やリトライ処理など)を行います。

初期化が成功し、IStoreControllerが取得できたところで購入処理が可能になります。

##購入処理
購入処理は今回の場合シンプルに
Purchase()と実装しました。こちらは各プロジェクトごとに適切なものを用意しましょう。
今回の場合例えばOnPurchase()はj"jp.hogehoge.1yen"のプロダクトを購入する専用のメソッドとして書かれています。
こちらUGUIのボタンなどに設定して、連打などされてしまい２回呼び出されてしまうと課金イベントも複数回呼び出されてしまうなど面倒な状態になるため注意してください。


```c#:TestBilling.cs

// ~~~~~~~~~~~~略~~~~~~~~~~~~

public void Purchasing()
{
        //登録したプロダクトで購入をかける
        var product = Controller.products.WithID("jp.hogehoge.1yen");
        Controller.InitiatePurchase(product);

        //上記はproductを取得し、購入をかけているが実際にはしたのようにproduct}_idの指定だけでも購入はかけられる。
        // Controller.InitiatePurchase("jp.hogehoge.1yen");
        
}
// ~~~~~~~~~~~~略~~~~~~~~~~~~

```

Controller.InitiatePurchase()でかけた購入の結果を受け取る以下のイベントの実装が必要となります。
・ ProcessPurchase()
・OnPurchaseFailed()

こちらも名称のとおりそれぞれ成功時、失敗時によばれます。
ProcessPurchase()に関してはメソッド内の戻り値で
 return PurchaseProcessingResult.Pending;
とした場合に、ConfirmPendingPurchase()をかけていない状態で再度アプリを起動(IAPの初期化)が行われたときにも呼ばれます。

成功時にはサーバでのレシート検証、アイテムの付与、KPI収集に必要な情報などを取り出し、各アプリケーションのサーバーに送信するなり、アプリ事に適切な実装が必要となります。

サーバーなどでのレシート検証を行う場合、検証の結果にが得られるまでプロダクト購入の消費を行わないようにするべきですので、ProcessPurchase()では
 return PurchaseProcessingResult.Pending;
としております。
サーバへのレシート送信処理などを用意し、検証結果が得られてからonfirmPendingPurchase()を呼べば購入は完了となります。

#おまけ(レシート検証について)
※バックエンドがPHPなどで動いている想定です。

##iOS
ProcessPurchaseイベントにて取得できる
string receipt = e.purchasedProduct.receipt;
にて取り出したレシートが以下のようなJSONになっています。
AppSotreの用意するレシート検証サーバへPayloadのTokenが正しいものかcurlなどで問い合わせます。
AppStoreの場合、アイテムの審査などはSandBox購入、ユーザーリリース後はProduction購入という風に購入が別れています。
どちらのTokenでも正常に検証を行えるようにまずProductionの検証サーバーにレシートを投げるようにします。
もしProductionの検証サーバーへSandBoxのレシートを問い合わせた場合、レスポンスのstatusに21005で戻ってくるため、その場合はSandBoxの検証サーバーへ問い合わせるようにします。
検証結果のレスポンスのstatusが0の場合、レシートの検証結果は正しいものとなりますので、アイテムの付与などを行うようにします。

```json
{
    "Store": "AppleAppStore",   
    "TransactionID": "TRANSACTION_ID",  
    "Payload": "TOKEN”   // 検証用Token
}
```

##Android
ProcessPurchaseイベントにて取得できる
string receipt = e.purchasedProduct.receipt;
にて取り出したレシートが以下のようなJSONになっています。
iOSと異なり、Payloadの中身がJSONのstringになっております。

```json
{
    "Store": "GooglePlay",   /
    "TransactionID": "TRANSACTION_ID",   
    "Payload": "{JSON}"   
}

```
Payloadの中身のJSONは以下のようになっております。


```json
{
    "json": "{RECEIPT_JSON}",   
    "signature": "SIGNATURE",  
    "skuDetails": "{DETAIL_JSON}",  
    "isPurchaseHistorySupported": "BOOL"   
}

```

Androidの検証にはPemに変換したPublicKeyとjson(data),siginature(署名)を使用しOpenSSL認証で署名が正しいか確認します。
署名が正しい場合、正常な購入となりますので、アイテムの付与等を行うようにします。