UnityIAPで課金を実装する (個人開発者向け)

# 概要

課金アイテムを実装してと言われてUnityIAPを触ることになった人に向けて備忘録として残しておきます。



# 目次
[1.前提](#1-前提)
[2.UnityIAPをプロジェクトに追加する](#2-unityiapをプロジェクトに追加する)
[3.実装](#3-実装)
[4.ストアの設定](#4-ストアの設定)
[5.おわりに](#5-おわりに)

# 1. 前提
CodelessIAPは使用しません。
レシート検証はこの記事では取り上げません。

# 2. UnityIAPをプロジェクトに追加する

Unityプロジェクトを開き、雲アイコンをクリックしてUnityGamingServicesを開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/18e1c421-74ce-7d85-efc0-2fb33ef02401.png)
InAppPurchasingをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/c7db1125-d43d-2c63-8369-fa3e10a8e473.png)

プロジェクトIDを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/ca53ca8a-4b46-9bc8-0b16-be484a96ba67.png)

アプリが13歳未満の子供を対象にしているか聞かれるので回答する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/7d623d8a-9a54-119a-e0c3-43f8bf043ffe.png)

OFFボタンをクリックしてONにして有効化する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/8b1c3225-c3ac-8b06-cbb9-b6dd5bf072d9.png)

InstallLatestVersionを押してインストール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/cf0c1e3c-39ea-bab2-9835-49b807b45cc7.png)

# 3. 実装
IStoreListenerを継承したストアクラスでUnityIAPは様々なコールバックを受け取っている。
下記がそのコールバック一覧
```csharp:UnityIapCallBack.cs
        /// <summary>
        /// IAP初期化完了
        /// </summary>
        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            UnityDebugLogSender("アプリ内購入初期化成功");
            m_StoreController = controller;
            Extensions = extensions;
        }


        /// <summary>
        /// IAP初期化失敗
        /// </summary>
        public void OnInitializeFailed(InitializationFailureReason error)
        {
            UnityDebugErrorLogSender($"アプリ内購入初期化失敗: {error}");
        }

        /// <summary>
        /// 購入失敗
        /// </summary>
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            UnityDebugErrorLogSender($"アプリ内購入失敗 - Product: '{product.definition.id}', 購入失敗理由: {failureReason}");
            // 購入失敗時の処理
            switch (failureReason)
            {
                case PurchaseFailureReason.PurchasingUnavailable:   // 購入機能が無効
                    break;
                case PurchaseFailureReason.ExistingPurchasePending: // 既に購入処理が進行している
                    break;
                case PurchaseFailureReason.ProductUnavailable:      // 購入不可能なプロダクト
                    break;
                case PurchaseFailureReason.SignatureInvalid:        // レシートの署名検証に失敗
                    break;
                case PurchaseFailureReason.UserCancelled:           // 購入をキャンセル
                    break;
                case PurchaseFailureReason.PaymentDeclined:         // 支払いに問題
                    break;
                case PurchaseFailureReason.DuplicateTransaction:    // トランザクションの重複
                    break;
                case PurchaseFailureReason.Unknown:                 // 上記以外のエラー
                    break;
            }
        }

        /// <summary>
        /// 購入処理
        /// </summary>
        /// <param name="purchaseEvent">購入情報</param>
        /// <returns>成功ならPurchaseProcessingResult.Complete</returns>
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
        {
            var product = purchaseEvent.purchasedProduct;
            UnityDebugLogSender($"productid:{product.definition.id}");
            return PurchaseProcessingResult.Complete; //購入成功
            return PurchaseProcessingResult.Pending; //購入保留
        }

```
上記に合わせてアプリ内課金の初期化を行う。
```csharp:IapManagerTest.cs
//Singleton参考: https://qiita.com/okuhiiro/items/3d69c602b8538c04a479
    public class IapManagerTest : SingletonMonoBehaviour<IapManagerTest>, IStoreListener
    {
        IStoreController m_StoreController; // The Unity Purchasing system.

        IExtensionProvider Extensions { get; set; }//リストア処理など拡張機能で使用

        void Start() //まず最初にアプリ内課金の初期化処理を走らせる。
        {
#if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS
#else
//Androidとios以外では動いてほしくないため
            return;
#endif
            InitializePurchasing();
            DontDestroyOnLoad(this);
        }

        //アプリ内課金初期化
        void InitializePurchasing()
        {
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

            //Add products that will be purchasable and indicate its type.
            //課金アイテムを追加する時は下記を増やして対応
            builder.AddProduct("magicStone100", ProductType.Consumable);
            //builder.AddProduct("magicStone500", ProductType.Consumable);

            UnityPurchasing.Initialize(this, builder);
            UnityDebugLogSender("課金処理の初期化完了");
        }

        /// <summary>
        /// IAP初期化完了
        /// </summary>
        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            UnityDebugLogSender("アプリ内購入初期化成功");
            m_StoreController = controller;
            Extensions = extensions;
        }

        /// <summary>
        /// IAP初期化失敗
        /// </summary>
        public void OnInitializeFailed(InitializationFailureReason error)
        {
            UnityDebugErrorLogSender($"アプリ内購入初期化失敗: {error}");
        }

        /// <summary>
        /// 購入失敗
        /// </summary>
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            UnityDebugErrorLogSender($"アプリ内購入失敗 - Product: '{product.definition.id}', 購入失敗理由: {failureReason}");
            // 購入失敗時の処理
            switch (failureReason)
            {
                case PurchaseFailureReason.PurchasingUnavailable:   // 購入機能が無効になっている
                    break;
                case PurchaseFailureReason.ExistingPurchasePending: // 既に購入処理が進行している
                    break;
                case PurchaseFailureReason.ProductUnavailable:      // 購入不可能なプロダクト
                    break;
                case PurchaseFailureReason.SignatureInvalid:        // レシートの署名検証に失敗
                    break;
                case PurchaseFailureReason.UserCancelled:           // ユーザーが購入をキャンセルした
                    break;
                case PurchaseFailureReason.PaymentDeclined:         // 支払いに問題があった
                    break;
                case PurchaseFailureReason.DuplicateTransaction:    // トランザクションの重複
                    break;
                case PurchaseFailureReason.Unknown:                 // 上記以外の認識されていないエラー
                    break;
            }
        }

        /// <summary>
        /// 購入処理
        /// 購入して支払いさえ終えていれば直後にアプリが落とされても次回起動時等に復元処理が走ってアイテムが付与されます。
        /// </summary>
        /// <param name="purchaseEvent">購入情報</param>
        /// <returns>結果</returns>
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
        {
            var product = purchaseEvent.purchasedProduct;

            if (product.definition.id == "magicstone100")
            {
                //アイテムの付与を行う。
                //ObtainItem(product.definition.id);
                return PurchaseProcessingResult.Complete; //購入成功
            }

            return PurchaseProcessingResult.Pending; //購入保留状態にする。(次回以降もう一度課金処理が走る)
        }

        /// <summary>
        /// 購入する。(購入確認画面が開かれる)
        /// </summary>
        /// <param name="productId">対象プロダクトID</param>
        public void Purchase(string productId)
        {
            m_StoreController.InitiatePurchase(productId);
        }

        /// <summary>
        /// このアイテムは単位付きでいくらなのかを返す。
        /// ※ただしストアに登録されているアイテムのみ
        /// </summary>
        /// <param name="productId">対象プロダクトID</param>
        /// <returns>例えば1.2ドルのアイテムだった場合 $1.2</returns>
        public string GetPrice(string productId)
        {
            if (IsValidStoreController() || m_StoreController.products.WithID(productId) == null)
            {
                return "---";
            }
            return m_StoreController.products.WithID(productId).metadata.localizedPriceString;
        }

        /// <summary>
        /// ストアの課金アイテムの初期化が正しく取得できているかを返す
        /// </summary>
        /// <param name="productId">対象課金アイテムプロダクトID</param>
        /// <returns>trueなら成功</returns>
        public bool GetCompleteInitializeProduct(string productId)
        {
            if (IsValidStoreController() || m_StoreController.products.WithID(productId) == null)
            {
                return false;
            }
            return true;
        }


        /// <summary>
        /// 定期購入アイテムを購入しているかをチェックする
        /// </summary>
        /// <returns>購入して期間内ならtrue</returns>
        public bool IsSubscribe()
        {
            if (IsValidStoreController())
            {
                return false;
            }
            var subscriptionProduct = m_StoreController.products.WithID("YOURSUBSCRIBEITEMID");
            return IsSubscribedTo(subscriptionProduct);
        }

        bool IsSubscribedTo(Product subscription)
        {
            // If the product doesn't have a receipt, then it wasn't purchased and the user is therefore not subscribed.
            if (subscription.receipt == null)
            {
                return false;
            }

            //The intro_json parameter is optional and is only used for the App Store to get introductory information.
            var subscriptionManager = new SubscriptionManager(subscription, null);

            // The SubscriptionInfo contains all of the information about the subscription.
            // Find out more: https://docs.unity3d.com/Packages/com.unity.purchasing@3.1/manual/UnityIAPSubscriptionProducts.html
            var info = subscriptionManager.getSubscriptionInfo();

            return info.isSubscribed() == Result.True;
        }

        // Restore purchases previously made by this customer. Some platforms automatically restore purchases. Apple currently requires explicit purchase restoration for IAP.
        // 以前買ったことがある商品のリストア処理。いくつかのプラットフォームは自動的にリストアする。AppleはIAPの中で明示的に購入リストアを要求している
        public void RestorePurchases()
        {
            // If we are running on an Apple device ... 
            // Apple なら
            bool isApple = false;
#if UNITY_IOS || UNITY_EDITOR
            isApple = true;
#else
            isApple = false;
#endif
            if (isApple)
            {
                // ... begin restoring purchases
                // リストア開始
                UnityDebugLogSender("RestorePurchases started ...");

                // Fetch the Apple store-specific subsystem.
                // Apple特有のサブシステムを取ってくる
                var apple = Extensions.GetExtension<IAppleExtensions>();
                // Begin the asynchronous process of restoring purchases. Expect a confirmation response in the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
                // 購入リストアの非同期処理を開始。下記のAction<bool> の中で、確認プロセスが呼び出される。もし以前買っているのならProcessPurchaseが呼び出される
                apple.RestoreTransactions((result) =>
                {
                    // The first phase of restoration. If no more responses are received on ProcessPurchase then no purchases are available to be restored.
                    // リストア処理の最初のフェーズ。もし、ProcessPurchaseで反応がないのなら、リストアするべき購入はなかったということ
                    UnityDebugLogSender("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
                    if (result)
                    {
                        UnityDebugLogSender("リストア取得完了、購入処理を呼び出します");
                    }
                    else
                    {
                        UnityDebugLogSender("リストア取得失敗");
                    }
                });
            }
            // Otherwise ...
            else
            {
                // We are not running on an Apple device. No work is necessary to restore purchases.
                UnityDebugLogSender("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
            }
        }

        /// <summary>
        /// storecontrollerが有効かどうか
        /// 機内モード等で初期化が上手く行ってない時にnullエラーが出るのでそれの防止
        /// </summary>
        /// <returns>有効ならtrue</returns>
        bool IsValidStoreController()
        {
            return !(m_StoreController == null || m_StoreController.products == null);
        }

        void UnityDebugLogSender(string str)
        {
#if DEVELOPMENT_BUILD
            //本番環境で出てほしくないため
            Debug.Log(str);
#endif
        }
        void UnityDebugErrorLogSender(string str)
        {
#if DEVELOPMENT_BUILD
            //本番環境で出てほしくないため
            Debug.LogError(str);
#endif
        }
    }
```

これで初期化はOK。
上記スクリプトをタイトル画面等の最初に必ず通るシーンのオブジェクトにアタッチしておく。
購入画面を呼び出す時は
```
UnityIapCallBack.Instance.Purchase("YourProductId");
```
で呼び出せる。

# 4. ストアの設定
GooglePlayConsoleを開きアプリ内アイテムを開く。
(収益化->商品->アプリ内アイテム)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/8b45ea7a-4232-f884-6724-d67e444d4f91.png)

アイテムを作成をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/8826b566-4781-364a-0f16-b173cbd435e6.png)

アイテムを登録します。アイテムIDは変更できないので慎重に
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/51495f94-0d16-f127-c363-c452ddbb2e88.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/1268d515-70e9-2df1-8602-81c0c50910e4.png)

これで登録はOK
課金テストを行う場合はすべてのアプリを表示している所に戻ってライセンステスターを追加してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308814/1fda5193-b56c-3446-bf89-4b02a2ece483.png)


# 5. おわりに
要領が分かればすぐに実装できるので分からない所があっても公式ドキュメントとにらめっこすればなんとかなる印象です。

# 参考
https://docs.unity3d.com/ja/current/Manual/UnityIAPSettingUp.html

https://docs.unity3d.com/Packages/com.unity.purchasing@3.1/manual/UnityIAPSubscriptionProducts.html