facebook twitter hatena line email

「Unity/課金/サンプル」の版間の差分

提供: 初心者エンジニアの簡易メモ
移動: 案内検索
(全部コードで処理する場合)
(全部コードで処理する場合)
 
(同じ利用者による、間の34版が非表示)
行8: 行8:
  
 
===iOSの場合===
 
===iOSの場合===
#ituneconnect管理画面/app内課金/管理/+をクリック
+
#AppStoreConnect管理画面/app内課金/管理/+をクリック
 
#消耗型を選択し、id(stone10)などを入れてく
 
#消耗型を選択し、id(stone10)などを入れてく
#審査用の画像は( 640 x 920)で登録すればよい
+
#審査用の画像は(640 x 920)で登録すればよい
 
#ituneconnectから契約を選択して、有料Appの利用規約に同意
 
#ituneconnectから契約を選択して、有料Appの利用規約に同意
 
#口座情報を入れる(ゆうちょなら"Japan Post Bank"で"9900-[口座番号]"
 
#口座情報を入れる(ゆうちょなら"Japan Post Bank"で"9900-[口座番号]"
 
#納税にアメリカ納税情報を入れる
 
#納税にアメリカ納税情報を入れる
#納税情報を入れたところOnInitializeFailed(NoProductsAvailable)の失敗イベントが呼ばれなくなり、正常にOnInitializedが呼ばれるようになった^^
+
#納税情報を入れたところOnInitializeFailed(NoProductsAvailable)の失敗イベントが呼ばれなくなり、正常にOnInitializedが呼ばれるようになった!
 
<pre>
 
<pre>
 
"Type of Income"は"Income from the sale of applications"
 
"Type of Income"は"Income from the sale of applications"
行25: 行25:
  
 
==定額課金==
 
==定額課金==
 +
サブスクリプション(サブスク)ともいう
 
===Androidの場合===
 
===Androidの場合===
 
#GooglePlayDeveloper/指定アプリ/ストアで表示/アプリ内サービス/定期購入
 
#GooglePlayDeveloper/指定アプリ/ストアで表示/アプリ内サービス/定期購入
行33: 行34:
  
 
===iOSの場合===
 
===iOSの場合===
#ituneconnect管理画面/app内課金/管理/+をクリック
+
#AppStoreConnect管理画面/app内課金/管理/+をクリック
 
#自動更新サブスクリプションを追加(自動更新サブスクリプションがない場合は、ユーザ権限のとこの有料Appの契約とアメリカ納税情報が入ってるか確認する)
 
#自動更新サブスクリプションを追加(自動更新サブスクリプションがない場合は、ユーザ権限のとこの有料Appの契約とアメリカ納税情報が入ってるか確認する)
 +
#参照名に製品名を適宜"プレミアム(継続)"など入れる
 +
#プロダクトidを(com.example.hogeapp.monthly)などに
 +
#期間を1ヶ月とかに設定する
 +
#app内課金/サブスクリプショングループができるので審査用の画面キャプチャなどを登録し、ステータスを送信準備完了にする。
 +
#プロジェクトビルドを設定する画面に、app内課金の項目が出るので、追加したapp内課金を選択して、app審査してもらう。
 +
 +
====iOSでサブスクのアイテム名が変更できない場合====
 +
翻訳の英語などを消したときなどは、英語のままになる、一度、日本語などの文字を更新すると良い。
 +
 +
===文言例===
 +
サブスクグループ名
 +
英語:flatrate
 +
日本語:継続課金
 +
月額
 +
製品id:com.example.hogeapp.monthly
 +
表示名日本語:プレミアム(毎月継続)
 +
表示名英語:Premium (monthly continued)
 +
説明日本語:月額で1ヶ月間広告削除できます。
 +
説明英語:You can delete ads for a month for a month.
 +
 +
年額
 +
製品id:com.example.hogeapp.yearly
 +
説明日本語:プレミアム(毎年継続)
 +
説明英語:Premium (yearly continued)
 +
説明日本語:年額で1年間広告削除できます。
 +
説明英語:You can delete ads for a year for a year.
 +
 +
===価格設定===
 +
applemusic例
 +
*月額:980円
 +
*年額:9800円
 +
*ファミリー月額:1480円
 +
年額は月額の10倍で、ファミリーは月額の1.5倍っぽい。
 +
 +
参考:https://www.kyodotokyo.com/music/apple-fee/#:~:text=%E6%9C%88%E9%A1%8D980%E5%86%86%EF%BC%88%E7%A8%8E%E8%BE%BC%EF%BC%89%E3%82%92,%E9%9D%9E%E5%B8%B8%E3%81%AB%E3%81%8A%E5%BE%97%E3%81%A7%E3%81%99%E3%80%82
  
 
==Unityから課金用ボタン設定==
 
==Unityから課金用ボタン設定==
行58: 行94:
 
#endif
 
#endif
  
 +
#define SUBSCRIPTION_MANAGER //Enables subscription product manager for AppleStore and GooglePlay store
 +
 +
using System.Collections.Generic;
 
using UnityEngine;
 
using UnityEngine;
 
using UnityEngine.Purchasing;
 
using UnityEngine.Purchasing;
行69: 行108:
 
     private IStoreController controller;
 
     private IStoreController controller;
 
     private IExtensionProvider extensions;
 
     private IExtensionProvider extensions;
 +
    private IAppleExtensions m_AppleExtensions;
 +
    private List<Product> products;
 +
    public bool initialized = false;
  
 
     public UnityIAPManager()
 
     public UnityIAPManager()
行77: 行119:
 
     }
 
     }
  
 +
    /// <summary>
 +
    /// iOS Specific.
 +
    /// This is called as part of Apple's 'Ask to buy' functionality,
 +
    /// when a purchase is requested by a minor and referred to a parent
 +
    /// for approval.
 +
    ///
 +
    /// When the purchase is approved or rejected, the normal purchase events
 +
    /// will fire.
 +
    /// </summary>
 +
    /// <param name="item">Item.</param>
 +
    private void OnDeferred(Product item)
 +
    {
 +
        Debug.Log("Purchase deferred: " + item.definition.id);
 +
    }
 +
    public Product GetProductById(string productId)
 +
    {
 +
        foreach (Product product in products)
 +
        {
 +
            if (product.definition.id.Equals(productId)) {
 +
                return product;
 +
            }
 +
        }
 +
        return null; // error
 +
    }
 
     /// <summary>
 
     /// <summary>
 
     /// Unity IAP が購入処理を行える場合に呼び出されます
 
     /// Unity IAP が購入処理を行える場合に呼び出されます
行85: 行151:
 
         this.extensions = extensions;
 
         this.extensions = extensions;
 
         Debug.Log("UnityIAPManager OnInitialized");
 
         Debug.Log("UnityIAPManager OnInitialized");
         foreach (var product in controller.products.all)
+
        products = new List<Product>();
 +
 
 +
        m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
 +
        m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
 +
 
 +
#if SUBSCRIPTION_MANAGER
 +
        Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
 +
#endif
 +
 
 +
         foreach (Product item in controller.products.all)
 
         {
 
         {
             Debug.Log("Title=" + product.metadata.localizedTitle);
+
             if (item.availableToPurchase)
            Debug.Log("Description=" + product.metadata.localizedDescription);
+
            {
            Debug.Log("PriceString=" + product.metadata.localizedPriceString);
+
                products.Add(item);
 +
                Debug.Log("definition.id=" + item.definition.id);
 +
                Debug.Log("localizedTitle=" + item.metadata.localizedTitle);
 +
                Debug.Log("localizedDescription=" + item.metadata.localizedDescription);
 +
                Debug.Log("localizedPriceString=" + item.metadata.localizedPriceString);
 +
                Debug.Log("isoCurrencyCode=" + item.metadata.isoCurrencyCode);
 +
                Debug.Log("localizedPrice=" + item.metadata.localizedPrice.ToString());
 +
                Debug.Log("transactionID=" + item.transactionID);
 +
                Debug.Log("receipt=" + item.receipt);
 +
 
 +
#if SUBSCRIPTION_MANAGER
 +
                // this is the usage of SubscriptionManager class
 +
                if (item.receipt != null) {
 +
                    if (item.definition.type == ProductType.Subscription) {
 +
                        if (checkIfProductIsAvailableForSubscriptionManager(item.receipt)) {
 +
                            string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
 +
                            SubscriptionManager p = new SubscriptionManager(item, intro_json);
 +
                            SubscriptionInfo info = p.getSubscriptionInfo();
 +
                            Debug.Log("product id is: " + info.getProductId());
 +
                            Debug.Log("purchase date is: " + info.getPurchaseDate());
 +
                            Debug.Log("subscription next billing date is: " + info.getExpireDate());
 +
                            Debug.Log("is subscribed? " + info.isSubscribed().ToString());
 +
                            Debug.Log("is expired? " + info.isExpired().ToString());
 +
                            Debug.Log("is cancelled? " + info.isCancelled());
 +
                            Debug.Log("product is in free trial peroid? " + info.isFreeTrial());
 +
                            Debug.Log("product is auto renewing? " + info.isAutoRenewing());
 +
                            Debug.Log("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
 +
                            Debug.Log("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
 +
                            Debug.Log("the product introductory localized price is: " + info.getIntroductoryPrice());
 +
                            Debug.Log("the product introductory price period is: " + info.getIntroductoryPricePeriod());
 +
                            Debug.Log("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
 +
                        } else {
 +
                            Debug.Log("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
 +
                        }
 +
                    } else {
 +
                        Debug.Log("the product is not a subscription product");
 +
                    }
 +
                } else {
 +
                    Debug.Log("the product should have a valid receipt");
 +
                }
 +
#endif
 +
        initialized = true;
 
         }
 
         }
 
     }
 
     }
行101: 行217:
 
     public void OnInitializeFailed(InitializationFailureReason error)
 
     public void OnInitializeFailed(InitializationFailureReason error)
 
     {
 
     {
         Debug.Log("OnInitializeFailed");
+
         Debug.Log("OnInitializeFailed " + error.ToString());
 +
    }
 +
    public void OnInitializeFailed(InitializationFailureReason error, string message)
 +
    {
 +
        Debug.Log("OnInitializeFailed " + error.ToString() + " " + message);
 
     }
 
     }
  
行186: 行306:
 
     public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
 
     public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
 
     {
 
     {
         Debug.Log("OnPurchaseFailed product=" + i.ToString());
+
         Debug.Log("OnPurchaseFailed product=" + i.ToString() + " " + p.ToString());
 
         if (p == PurchaseFailureReason.PurchasingUnavailable)
 
         if (p == PurchaseFailureReason.PurchasingUnavailable)
 
         {
 
         {
行200: 行320:
 
         }
 
         }
 
     }
 
     }
 +
#if SUBSCRIPTION_MANAGER
 +
    private bool checkIfProductIsAvailableForSubscriptionManager(string receipt) {
 +
        var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
 +
        if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload")) {
 +
            Debug.Log("The product receipt does not contain enough information");
 +
            return false;
 +
        }
 +
        var store = (string)receipt_wrapper ["Store"];
 +
        var payload = (string)receipt_wrapper ["Payload"];
 +
 +
        if (payload != null ) {
 +
            switch (store) {
 +
            case GooglePlay.Name:
 +
                {
 +
                    var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
 +
                    if (!payload_wrapper.ContainsKey("json")) {
 +
                        Debug.Log("The product receipt does not contain enough information, the 'json' field is missing");
 +
                        return false;
 +
                    }
 +
                    var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
 +
                    if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload")) {
 +
                        Debug.Log("The product receipt does not contain enough information, the 'developerPayload' field is missing");
 +
                        return false;
 +
                    }
 +
                    var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
 +
                    var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
 +
                    if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial")) {
 +
                        Debug.Log("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
 +
                        return false;
 +
                    }
 +
                    return true;
 +
                }
 +
            case AppleAppStore.Name:
 +
            case AmazonApps.Name:
 +
            case MacAppStore.Name:
 +
                {
 +
                    return true;
 +
                }
 +
            default:
 +
                {
 +
                    return false;
 +
                }
 +
            }
 +
        }
 +
        return false;
 +
    }
 +
#endif
 +
 
}
 
}
 
#endif // UNITY_PURCHASING
 
#endif // UNITY_PURCHASING
 
</pre>
 
</pre>
  
====処理ログ====
+
====android処理ログ====
 
<pre>
 
<pre>
 
// 購入初期化
 
// 購入初期化
行224: 行392:
 
07-13 02:36:40.526  5111  5247 I Unity  : google.purchaseState=Purchased
 
07-13 02:36:40.526  5111  5247 I Unity  : google.purchaseState=Purchased
 
07-13 02:36:40.534  5111  5247 I Unity  : google.purchaseToken=lahnjifjnlchkhmbjjlghhbn.AO-J1OyyXV_avfKglYJsff9pPoW_5sOC3YshbqKuj8fE52bAzgnVMjwtVCj45b97yDcg4CV5arAfDndgwo-CcgHTqJ_vMFNsjULWZOuS-6K3C1qXbeVLGh9qQ2cGdJUvgVJsy123456_
 
07-13 02:36:40.534  5111  5247 I Unity  : google.purchaseToken=lahnjifjnlchkhmbjjlghhbn.AO-J1OyyXV_avfKglYJsff9pPoW_5sOC3YshbqKuj8fE52bAzgnVMjwtVCj45b97yDcg4CV5arAfDndgwo-CcgHTqJ_vMFNsjULWZOuS-6K3C1qXbeVLGh9qQ2cGdJUvgVJsy123456_
 +
// 購入後再表示(定期課金の場合)
 +
07-15 22:30:32.488  9185  9280 I Unity  : UnityIAPManager OnInitialized
 +
07-15 22:30:32.626  9185  9280 I Unity  : the product should have a valid receipt
 +
07-15 22:30:32.645  9185  9280 I Unity  : definition.id=com.example.hogeapp1.monthly
 +
07-15 22:30:32.645  9185  9280 I Unity  : localizedTitle=1ヶ月間広告削除 (ホゲアプリ1)
 +
07-15 22:30:32.662  9185  9280 I Unity  : localizedDescription=1ヶ月間広告削除
 +
07-15 22:30:32.674  9185  9280 I Unity  : localizedPriceString=¥600
 +
07-15 22:30:32.686  9185  9280 I Unity  : isoCurrencyCode=JPY
 +
07-15 22:30:32.699  9185  9280 I Unity  : localizedPrice=600
 +
07-15 22:30:32.713  9185  9280 I Unity  : transactionID=GPA.3364-9546-9734-46033
 +
07-15 22:30:32.723  9185  9280 I Unity  : receipt={"Store":"GooglePlay","TransactionID":"GPA.3364-9546-9734-46033","Payload":"{\"json\":\"{\\\"orderId\\\":\\\"GPA.3364-9546-9734-46033\\\",\\\"packageName\\\":\\\"com.example.hogeapp1\\\",\\\"productId\\\":\\\"com.example.hogeapp1.monthly\\\",\\\"purchaseTime\\\":1594818391067,\\\"purchaseState\\\":0,\\\"developerPayload\\\":\\\"{\\\\\\\"developerPayload\\\\\\\":\\\\\\\"\\\\\\\",\\\\\\\"is_free_trial\\\\\\\":false,\\\\\\\"has_introductory_price_trial\\\\\\\":false,\\\\\\\"is_updated\\\\\\\":false,\\\\\\\"accountId\\\\\\\":\\\\\\\"\\\\\\\"}\\\",\\\"purchaseToken\\\":\\\"lhcgpmbhnhbgcemjphdcgcpf.AO-J1OyNfcQCd1PdWqDIS9JJt3QXUV5f9gWXag_ABxHLk4w_nGLn7OVdsEufD-7-FAsvcvvdK4Ouw-XgwJH9VCTZ4BXrZAE2UYc78BM6p7br2PAh9s1Rpa0QymVjLFEooCbNeAj0BqbDQRW0S1ELlzWC-3odxYqs8w\\\",\\\"autoRenewing\\\":true}\",\"signature\":\"cQzMST2TT3tduG7mqHu8Q1AmSgWFnh1nXpZWYOvD6dRWSRz+OCfdrxOkZIiDGO8tVsXhb9XNq8vIMbMdzzV7\\/rh5LjQDbs1vm1ojxY5Id2UNMw\\/MghnNfCj78kkJHF1N8Ea+pQdVyErIjmWJvLLo6Vdx7aPH8dGnRXqMiXdoRC9utsvjWCegntAnTPM
 +
07-15 22:30:32.735  9185  9280 I Unity  : product id is: com.example.hogeapp1.monthly
 +
07-15 22:30:32.746  9185  9280 I Unity  : purchase date is: 07/15/2020 13:06:31
 +
07-15 22:30:32.758  9185  9280 I Unity  : subscription next billing date is: 08/15/2020 13:06:31
 +
07-15 22:30:32.769  9185  9280 I Unity  : is subscribed? True
 +
07-15 22:30:32.780  9185  9280 I Unity  : is expired? False
 +
07-15 22:30:32.794  9185  9280 I Unity  : is cancelled? False
 +
07-15 22:30:32.808  9185  9280 I Unity  : product is in free trial peroid? False
 +
07-15 22:30:32.826  9185  9280 I Unity  : product is auto renewing? True
 +
07-15 22:30:32.843  9185  9280 I Unity  : subscription remaining valid time until next billing date is: 30.23:35:58.3409010
 +
07-15 22:30:32.860  9185  9280 I Unity  : is this product in introductory price period? False
 +
07-15 22:30:32.875  9185  9280 I Unity  : the product introductory localized price is: not available
 +
07-15 22:30:32.888  9185  9280 I Unity  : the product introductory price period is: 00:00:00
 +
07-15 22:30:32.903  9185  9280 I Unity  : the number of product introductory price period cycles is: 0
 
</pre>
 
</pre>
 +
 +
====ios処理ログ====
 +
<pre>
 +
// 購入初期化
 +
UnityIAPManager OnInitialized
 +
localizedTitle=
 +
localizedDescription=
 +
localizedPriceString=¥600
 +
isoCurrencyCode=JPY
 +
localizedPrice=600
 +
transactionID=
 +
receipt=
 +
// 購入確認
 +
OnPurchaseClicked productId=com.example.hogeapp1.monthly
 +
// 購入後
 +
PurchaseProcessingResult
 +
RECEIPT_VALIDATION
 +
5.6 appIdentifier=com.example.hogeapp1
 +
Receipt is valid. Contents:
 +
Receipt productID=com.example.hogeapp1.monthly
 +
Receipt purchaseDate=2020/07/15 12:18:16
 +
Receipt transactionID=1000000693275565
 +
apple.originalTransactionIdentifier=1000000693275565
 +
apple.subscriptionExpirationDate=2020/07/15 12:23:16
 +
apple.cancellationDate=0001/01/01 0:00:00
 +
apple.quantity=1
 +
// 購入後再表示(定期課金の場合)
 +
UnityIAPManager OnInitialized
 +
definition.id=com.example.hogeapp1.monthly
 +
localizedTitle=
 +
localizedDescription=
 +
localizedPriceString=¥600
 +
isoCurrencyCode=JPY
 +
localizedPrice=600
 +
transactionID=1000000693295003
 +
receipt={"Store":"AppleAppStore","TransactionID":"1000000693295003","Payload":"MIIUCwYJKoAQBhdMzH・・・TxhlpfU="} // Payloadは6000文字ぐらいある
 +
product id is: com.example.hogeapp1.monthly
 +
purchase date is: 2020/07/15 12:49:45
 +
subscription next billing date is: 2020/07/15 12:54:45
 +
is subscribed? True
 +
is expired? False
 +
is cancelled? False
 +
product is in free trial peroid? False
 +
product is auto renewing? True
 +
subscription remaining valid time until next billing date is: 00:02:19.6844300
 +
is this product in introductory price period? False
 +
the product introductory localized price is: not available
 +
the product introductory price period is: 00:00:00
 +
the number of product introductory price period cycles is: 0
 +
</pre>
 +
 +
公式:CrossPlatformValidatorの使い方:https://docs.unity3d.com/ja/2019.4/Manual/UnityIAPValidatingReceipts.html
  
 
===全部コードで処理する場合(その2)===
 
===全部コードで処理する場合(その2)===
行243: 行487:
 
#作ったbuttonのプロパティのproductIdに先ほどいれたid(monthlyなど)いれる
 
#作ったbuttonのプロパティのproductIdに先ほどいれたid(monthlyなど)いれる
 
#Assets/Plugins/UnityPurchasing/script/CodelessIAPStoreListener.csにイベントが発生するので確認する
 
#Assets/Plugins/UnityPurchasing/script/CodelessIAPStoreListener.csにイベントが発生するので確認する
 +
 +
==公式デモ==
 +
課金プラグインを入れると、以下に入ってるので確認する
 +
*Assets/Plugins/UnityPurchasing/scenes/IAP Demo.unity
 +
*Assets/Plugins/UnityPurchasing/script/IAPDemo.cs
 +
==AndroidのPlayStoreの為替レートによる課金設定で7D7DE1A7が出る場合==
 +
エラー詳細
 +
予期しないエラーが発生しました。もう一度お試しください(7D7DE1A7)
 +
レバノンだけを抜いておけば、このエラーが出なくなる
  
 
==参考==
 
==参考==
行250: 行503:
 
===全部コードの場合の参考===
 
===全部コードの場合の参考===
 
https://docs.unity3d.com/ja/current/Manual/UnityIAPSettingUp.html
 
https://docs.unity3d.com/ja/current/Manual/UnityIAPSettingUp.html
 +
 +
http://www.kyucon.com/blog/2018/12/unity-playfab.html
  
 
https://gist.github.com/YoshihideSogawa/f7c118127ce50e593a5b4a12e8426d6e
 
https://gist.github.com/YoshihideSogawa/f7c118127ce50e593a5b4a12e8426d6e

2023年6月2日 (金) 11:35時点における最新版

都度購入

Androidの場合

  1. GooglePlayDeveloper/指定アプリ/ストアで表示/アプリ内サービス/管理対象のアイテム/管理対象のアイテム作成
  2. プロダクトidを(stone10)などで入れる(storeと連携してないと、購入ボタンを押しても商品が無いとなる)
  3. 有効にするを選択して、有効になっていることを確認

日本語を追加した場合は、日本語の説明も入れないと、保存されないので気を付ける。

iOSの場合

  1. AppStoreConnect管理画面/app内課金/管理/+をクリック
  2. 消耗型を選択し、id(stone10)などを入れてく
  3. 審査用の画像は(640 x 920)で登録すればよい
  4. ituneconnectから契約を選択して、有料Appの利用規約に同意
  5. 口座情報を入れる(ゆうちょなら"Japan Post Bank"で"9900-[口座番号]"
  6. 納税にアメリカ納税情報を入れる
  7. 納税情報を入れたところOnInitializeFailed(NoProductsAvailable)の失敗イベントが呼ばれなくなり、正常にOnInitializedが呼ばれるようになった!
"Type of Income"は"Income from the sale of applications"
Capacity in which acting欄に "Self"

参考:https://re35.org/release-ios-app/

参考:http://lab.studioheat.com/?p=335

定額課金

サブスクリプション(サブスク)ともいう

Androidの場合

  1. GooglePlayDeveloper/指定アプリ/ストアで表示/アプリ内サービス/定期購入
  2. プロダクトidを(com.example.hogeapp.monthly)などで入れる(storeと連携してないと、購入ボタンを押しても商品が無いとなる)
  3. 有効にするを選択して、有効になっていることを確認

日本語を追加した場合は、日本語の説明も入れないと、保存されないので気を付ける。

iOSの場合

  1. AppStoreConnect管理画面/app内課金/管理/+をクリック
  2. 自動更新サブスクリプションを追加(自動更新サブスクリプションがない場合は、ユーザ権限のとこの有料Appの契約とアメリカ納税情報が入ってるか確認する)
  3. 参照名に製品名を適宜"プレミアム(継続)"など入れる
  4. プロダクトidを(com.example.hogeapp.monthly)などに
  5. 期間を1ヶ月とかに設定する
  6. app内課金/サブスクリプショングループができるので審査用の画面キャプチャなどを登録し、ステータスを送信準備完了にする。
  7. プロジェクトビルドを設定する画面に、app内課金の項目が出るので、追加したapp内課金を選択して、app審査してもらう。

iOSでサブスクのアイテム名が変更できない場合

翻訳の英語などを消したときなどは、英語のままになる、一度、日本語などの文字を更新すると良い。

文言例

サブスクグループ名

英語:flatrate
日本語:継続課金

月額

製品id:com.example.hogeapp.monthly
表示名日本語:プレミアム(毎月継続)
表示名英語:Premium (monthly continued)
説明日本語:月額で1ヶ月間広告削除できます。
説明英語:You can delete ads for a month for a month.

年額

製品id:com.example.hogeapp.yearly
説明日本語:プレミアム(毎年継続)
説明英語:Premium (yearly continued)
説明日本語:年額で1年間広告削除できます。
説明英語:You can delete ads for a year for a year.

価格設定

applemusic例

  • 月額:980円
  • 年額:9800円
  • ファミリー月額:1480円

年額は月額の10倍で、ファミリーは月額の1.5倍っぽい。

参考:https://www.kyodotokyo.com/music/apple-fee/#:~:text=%E6%9C%88%E9%A1%8D980%E5%86%86%EF%BC%88%E7%A8%8E%E8%BE%BC%EF%BC%89%E3%82%92,%E9%9D%9E%E5%B8%B8%E3%81%AB%E3%81%8A%E5%BE%97%E3%81%A7%E3%81%99%E3%80%82

Unityから課金用ボタン設定

課金の記述方法として以下2パターンがある

  • 全部コードで処理する方法
  • コードレス処理がある。

全部コードで処理する場合

  1. 以下課金呼び出しコードを記述(例:stone10はproductID)
UnityIAPManager manager;
manager = new UnityIAPManager();
manager.OnPurchaseClicked("stone10");

UnityIAPManager.cs

#if UNITY_PURCHASING

#if UNITY_ANDROID || UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_TVOS
// You must obfuscate your secrets using Window > Unity IAP > Receipt Validation Obfuscator
// before receipt validation will compile in this sample.
#define RECEIPT_VALIDATION
#endif

#define SUBSCRIPTION_MANAGER //Enables subscription product manager for AppleStore and GooglePlay store

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;

#if RECEIPT_VALIDATION
using UnityEngine.Purchasing.Security;
#endif

public class UnityIAPManager : IStoreListener
{
    private IStoreController controller;
    private IExtensionProvider extensions;
    private IAppleExtensions m_AppleExtensions;
    private List<Product> products;
    public bool initialized = false;

    public UnityIAPManager()
    {
        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.AddProduct("stone10", ProductType.Consumable);
        UnityPurchasing.Initialize(this, builder);
    }

    /// <summary>
    /// iOS Specific.
    /// This is called as part of Apple's 'Ask to buy' functionality,
    /// when a purchase is requested by a minor and referred to a parent
    /// for approval.
    ///
    /// When the purchase is approved or rejected, the normal purchase events
    /// will fire.
    /// </summary>
    /// <param name="item">Item.</param>
    private void OnDeferred(Product item)
    {
        Debug.Log("Purchase deferred: " + item.definition.id);
    }
    public Product GetProductById(string productId)
    {
        foreach (Product product in products)
        {
            if (product.definition.id.Equals(productId)) {
                return product;
            }
        }
        return null; // error
    }
    /// <summary>
    /// Unity IAP が購入処理を行える場合に呼び出されます
    /// </summary>
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        this.controller = controller;
        this.extensions = extensions;
        Debug.Log("UnityIAPManager OnInitialized");
        products = new List<Product>();

        m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
        m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);

#if SUBSCRIPTION_MANAGER
        Dictionary<string, string> introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
#endif

        foreach (Product item in controller.products.all)
        {
            if (item.availableToPurchase)
            {
                products.Add(item);
                Debug.Log("definition.id=" + item.definition.id);
                Debug.Log("localizedTitle=" + item.metadata.localizedTitle);
                Debug.Log("localizedDescription=" + item.metadata.localizedDescription);
                Debug.Log("localizedPriceString=" + item.metadata.localizedPriceString);
                Debug.Log("isoCurrencyCode=" + item.metadata.isoCurrencyCode);
                Debug.Log("localizedPrice=" + item.metadata.localizedPrice.ToString());
                Debug.Log("transactionID=" + item.transactionID);
                Debug.Log("receipt=" + item.receipt);

#if SUBSCRIPTION_MANAGER
                // this is the usage of SubscriptionManager class
                if (item.receipt != null) {
                    if (item.definition.type == ProductType.Subscription) {
                        if (checkIfProductIsAvailableForSubscriptionManager(item.receipt)) {
                            string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId];
                            SubscriptionManager p = new SubscriptionManager(item, intro_json);
                            SubscriptionInfo info = p.getSubscriptionInfo();
                            Debug.Log("product id is: " + info.getProductId());
                            Debug.Log("purchase date is: " + info.getPurchaseDate());
                            Debug.Log("subscription next billing date is: " + info.getExpireDate());
                            Debug.Log("is subscribed? " + info.isSubscribed().ToString());
                            Debug.Log("is expired? " + info.isExpired().ToString());
                            Debug.Log("is cancelled? " + info.isCancelled());
                            Debug.Log("product is in free trial peroid? " + info.isFreeTrial());
                            Debug.Log("product is auto renewing? " + info.isAutoRenewing());
                            Debug.Log("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
                            Debug.Log("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
                            Debug.Log("the product introductory localized price is: " + info.getIntroductoryPrice());
                            Debug.Log("the product introductory price period is: " + info.getIntroductoryPricePeriod());
                            Debug.Log("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
                        } else {
                            Debug.Log("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class.");
                        }
                    } else {
                        Debug.Log("the product is not a subscription product");
                    }
                } else {
                    Debug.Log("the product should have a valid receipt");
                }
#endif
        initialized = true;
        }
    }

    /// <summary>
    ///  Unity IAP 回復不可能な初期エラーに遭遇したときに呼び出されます。
    ///
    /// これは、インターネットが使用できない場合は呼び出されず、
    /// インターネットが使用可能になるまで初期化を試みます。
    /// </summary>
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log("OnInitializeFailed " + error.ToString());
    }
    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        Debug.Log("OnInitializeFailed " + error.ToString() + " " + message);
    }

    /// <summary>
    /// 購入が終了したときに呼び出されます。
    ///
    ///  OnInitialized() 後、いつでも呼び出される場合があります。
    /// </summary>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    {
        Debug.Log("PurchaseProcessingResult " + e.purchasedProduct.metadata.localizedTitle);
        bool validPurchase = true; // R.V. のないプラットフォームに有効です

        // Unity IAP の検証ロジックはこれらのプラットフォームにのみ含まれます。
#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
        // エディターの難読化ウィンドウで準備した機密を持つ
        // バリデーターを準備します。

#if RECEIPT_VALIDATION
        Debug.Log("RECEIPT_VALIDATION");
        string appIdentifier;
#if UNITY_5_6_OR_NEWER
        appIdentifier = Application.identifier;
        Debug.Log("5.6 appIdentifier=" + appIdentifier);
#else
        appIdentifier = Application.bundleIdentifier;
        Debug.Log("other appIdentifier=" + appIdentifier);
#endif
        var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), appIdentifier);

        try
        {
            // Google Play で、result は 1 つの product ID を取得します
            // Apple stores で、receipts には複数のプロダクトが含まれます
            var result = validator.Validate(e.purchasedProduct.receipt);
            // 情報提供の目的で、ここにレシートをリストします
            Debug.Log("Receipt is valid. Contents:");
            foreach (IPurchaseReceipt productReceipt in result)
            {
                Debug.Log("Receipt productID=" + productReceipt.productID);
                Debug.Log("Receipt purchaseDate=" + productReceipt.purchaseDate);
                Debug.Log("Receipt transactionID=" + productReceipt.transactionID);

                GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
                if (null != google)
                {
                    // ここに Google のオーダー ID
                    //  sandbox でテストする場合は null にするように注意
                    // なぜなら、Google の sandbox はオーダー IDを発行しないため
                    Debug.Log("google.transactionID=" + google.transactionID);
                    Debug.Log("google.purchaseState=" + google.purchaseState);
                    Debug.Log("google.purchaseToken=" + google.purchaseToken);
                }

                AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
                if (null != apple)
                {
                    Debug.Log("apple.originalTransactionIdentifier=" + apple.originalTransactionIdentifier);
                    Debug.Log("apple.subscriptionExpirationDate=" + apple.subscriptionExpirationDate);
                    Debug.Log("apple.cancellationDate=" + apple.cancellationDate);
                    Debug.Log("apple.quantity=" + apple.quantity);
                }
            }
        }
        catch (IAPSecurityException)
        {
            Debug.Log("Invalid receipt, not unlocking content");
            validPurchase = false;
        }
#endif
#endif

        if (validPurchase)
        {
            // 適当なコンテンツをここにアンロックします
        }

        return PurchaseProcessingResult.Complete;
    }

    /// <summary>
    /// 購入が失敗したときに呼び出されます。
    /// </summary>
    public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
    {
        Debug.Log("OnPurchaseFailed product=" + i.ToString() + " " + p.ToString());
        if (p == PurchaseFailureReason.PurchasingUnavailable)
        {
            // デバイス設定で IAP が無効である場合があります。
        }
    }
    public void OnPurchaseClicked(string productId)
    {
        Debug.Log("OnPurchaseClicked productId=" + productId);
        if (controller != null)
        {
            controller.InitiatePurchase(productId);
        }
    }
#if SUBSCRIPTION_MANAGER
    private bool checkIfProductIsAvailableForSubscriptionManager(string receipt) {
        var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
        if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload")) {
            Debug.Log("The product receipt does not contain enough information");
            return false;
        }
        var store = (string)receipt_wrapper ["Store"];
        var payload = (string)receipt_wrapper ["Payload"];

        if (payload != null ) {
            switch (store) {
            case GooglePlay.Name:
                {
                    var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
                    if (!payload_wrapper.ContainsKey("json")) {
                        Debug.Log("The product receipt does not contain enough information, the 'json' field is missing");
                        return false;
                    }
                    var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
                    if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload")) {
                        Debug.Log("The product receipt does not contain enough information, the 'developerPayload' field is missing");
                        return false;
                    }
                    var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
                    var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
                    if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial")) {
                        Debug.Log("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
                        return false;
                    }
                    return true;
                }
            case AppleAppStore.Name:
            case AmazonApps.Name:
            case MacAppStore.Name:
                {
                    return true;
                }
            default:
                {
                    return false;
                }
            }
        }
        return false;
    }
#endif

}
#endif // UNITY_PURCHASING

android処理ログ

// 購入初期化
07-12 22:25:15.646 19383 19444 I Unity   : UnityIAPManager OnInitialized
07-12 22:25:15.664 19383 19444 I Unity   : Title=This is stone10 (Flick Typing input practice app)
07-12 22:25:15.681 19383 19444 I Unity   : Description=This is stone10!!
07-12 22:25:15.698 19383 19444 I Unity   : PriceString=¥100
// 購入確認
07-12 22:25:17.046 19383 19444 I Unity   : OnPurchaseClicked productId=stone10
// 購入後
07-12 22:25:27.994 19383 19444 I Unity   : PurchaseProcessingResult This is stone10 (Flick Typing input practice app)
07-13 01:15:54.698 31878 31955 I Unity   : RECEIPT_VALIDATION
07-13 01:15:54.715 31878 31955 I Unity   : 5.6 appIdentifier=com.example.hogeapp
07-13 01:28:23.648  1078  1232 I Unity   : Receipt is valid. Contents:
07-13 01:28:23.659  1078  1232 I Unity   : Receipt productID=stone10
07-13 01:28:23.672  1078  1232 I Unity   : Receipt purchaseDate=07/12/2020 16:28:23
07-13 01:28:23.683  1078  1232 I Unity   : Receipt transactionID=GPA.3366-6958-2462-12345
07-13 02:36:40.517  5111  5247 I Unity   : google.transactionID=GPA.3366-6958-2462-12345
07-13 02:36:40.526  5111  5247 I Unity   : google.purchaseState=Purchased
07-13 02:36:40.534  5111  5247 I Unity   : google.purchaseToken=lahnjifjnlchkhmbjjlghhbn.AO-J1OyyXV_avfKglYJsff9pPoW_5sOC3YshbqKuj8fE52bAzgnVMjwtVCj45b97yDcg4CV5arAfDndgwo-CcgHTqJ_vMFNsjULWZOuS-6K3C1qXbeVLGh9qQ2cGdJUvgVJsy123456_
// 購入後再表示(定期課金の場合)
07-15 22:30:32.488  9185  9280 I Unity   : UnityIAPManager OnInitialized
07-15 22:30:32.626  9185  9280 I Unity   : the product should have a valid receipt
07-15 22:30:32.645  9185  9280 I Unity   : definition.id=com.example.hogeapp1.monthly
07-15 22:30:32.645  9185  9280 I Unity   : localizedTitle=1ヶ月間広告削除 (ホゲアプリ1)
07-15 22:30:32.662  9185  9280 I Unity   : localizedDescription=1ヶ月間広告削除
07-15 22:30:32.674  9185  9280 I Unity   : localizedPriceString=¥600
07-15 22:30:32.686  9185  9280 I Unity   : isoCurrencyCode=JPY
07-15 22:30:32.699  9185  9280 I Unity   : localizedPrice=600
07-15 22:30:32.713  9185  9280 I Unity   : transactionID=GPA.3364-9546-9734-46033
07-15 22:30:32.723  9185  9280 I Unity   : receipt={"Store":"GooglePlay","TransactionID":"GPA.3364-9546-9734-46033","Payload":"{\"json\":\"{\\\"orderId\\\":\\\"GPA.3364-9546-9734-46033\\\",\\\"packageName\\\":\\\"com.example.hogeapp1\\\",\\\"productId\\\":\\\"com.example.hogeapp1.monthly\\\",\\\"purchaseTime\\\":1594818391067,\\\"purchaseState\\\":0,\\\"developerPayload\\\":\\\"{\\\\\\\"developerPayload\\\\\\\":\\\\\\\"\\\\\\\",\\\\\\\"is_free_trial\\\\\\\":false,\\\\\\\"has_introductory_price_trial\\\\\\\":false,\\\\\\\"is_updated\\\\\\\":false,\\\\\\\"accountId\\\\\\\":\\\\\\\"\\\\\\\"}\\\",\\\"purchaseToken\\\":\\\"lhcgpmbhnhbgcemjphdcgcpf.AO-J1OyNfcQCd1PdWqDIS9JJt3QXUV5f9gWXag_ABxHLk4w_nGLn7OVdsEufD-7-FAsvcvvdK4Ouw-XgwJH9VCTZ4BXrZAE2UYc78BM6p7br2PAh9s1Rpa0QymVjLFEooCbNeAj0BqbDQRW0S1ELlzWC-3odxYqs8w\\\",\\\"autoRenewing\\\":true}\",\"signature\":\"cQzMST2TT3tduG7mqHu8Q1AmSgWFnh1nXpZWYOvD6dRWSRz+OCfdrxOkZIiDGO8tVsXhb9XNq8vIMbMdzzV7\\/rh5LjQDbs1vm1ojxY5Id2UNMw\\/MghnNfCj78kkJHF1N8Ea+pQdVyErIjmWJvLLo6Vdx7aPH8dGnRXqMiXdoRC9utsvjWCegntAnTPM
07-15 22:30:32.735  9185  9280 I Unity   : product id is: com.example.hogeapp1.monthly
07-15 22:30:32.746  9185  9280 I Unity   : purchase date is: 07/15/2020 13:06:31
07-15 22:30:32.758  9185  9280 I Unity   : subscription next billing date is: 08/15/2020 13:06:31
07-15 22:30:32.769  9185  9280 I Unity   : is subscribed? True
07-15 22:30:32.780  9185  9280 I Unity   : is expired? False
07-15 22:30:32.794  9185  9280 I Unity   : is cancelled? False
07-15 22:30:32.808  9185  9280 I Unity   : product is in free trial peroid? False
07-15 22:30:32.826  9185  9280 I Unity   : product is auto renewing? True
07-15 22:30:32.843  9185  9280 I Unity   : subscription remaining valid time until next billing date is: 30.23:35:58.3409010
07-15 22:30:32.860  9185  9280 I Unity   : is this product in introductory price period? False
07-15 22:30:32.875  9185  9280 I Unity   : the product introductory localized price is: not available
07-15 22:30:32.888  9185  9280 I Unity   : the product introductory price period is: 00:00:00
07-15 22:30:32.903  9185  9280 I Unity   : the number of product introductory price period cycles is: 0

ios処理ログ

// 購入初期化
UnityIAPManager OnInitialized
localizedTitle=
localizedDescription=
localizedPriceString=¥600
isoCurrencyCode=JPY
localizedPrice=600
transactionID=
receipt=
// 購入確認
OnPurchaseClicked productId=com.example.hogeapp1.monthly
// 購入後
PurchaseProcessingResult 
RECEIPT_VALIDATION
5.6 appIdentifier=com.example.hogeapp1
Receipt is valid. Contents:
Receipt productID=com.example.hogeapp1.monthly
Receipt purchaseDate=2020/07/15 12:18:16
Receipt transactionID=1000000693275565
apple.originalTransactionIdentifier=1000000693275565
apple.subscriptionExpirationDate=2020/07/15 12:23:16
apple.cancellationDate=0001/01/01 0:00:00
apple.quantity=1
// 購入後再表示(定期課金の場合)
UnityIAPManager OnInitialized
definition.id=com.example.hogeapp1.monthly
localizedTitle=
localizedDescription=
localizedPriceString=¥600
isoCurrencyCode=JPY
localizedPrice=600
transactionID=1000000693295003
receipt={"Store":"AppleAppStore","TransactionID":"1000000693295003","Payload":"MIIUCwYJKoAQBhdMzH・・・TxhlpfU="} // Payloadは6000文字ぐらいある
product id is: com.example.hogeapp1.monthly
purchase date is: 2020/07/15 12:49:45
subscription next billing date is: 2020/07/15 12:54:45
is subscribed? True
is expired? False
is cancelled? False
product is in free trial peroid? False
product is auto renewing? True
subscription remaining valid time until next billing date is: 00:02:19.6844300
is this product in introductory price period? False
the product introductory localized price is: not available
the product introductory price period is: 00:00:00
the number of product introductory price period cycles is: 0

公式:CrossPlatformValidatorの使い方:https://docs.unity3d.com/ja/2019.4/Manual/UnityIAPValidatingReceipts.html

全部コードで処理する場合(その2)

こちらを使ってもいける。

https://gist.github.com/YoshihideSogawa/f7c118127ce50e593a5b4a12e8426d6e

  1. 上記をPurchaser.csで保存する
  2. Unityのヒエラルキーに新規Objectを作成し、適当な名前で、Purchaser.csをアタッチする

コードレスの場合

(未完成です)

  1. unityメニュー/windows/UnityIAP/CreateIAPButtonでボタンを作成する
  2. unityメニュー/windows/IAP Catalogをクリックし以下のような詳細データを入れる
id:monthlyなど
type:Subscription(自動課金)
  1. 作ったbuttonのプロパティのproductIdに先ほどいれたid(monthlyなど)いれる
  2. Assets/Plugins/UnityPurchasing/script/CodelessIAPStoreListener.csにイベントが発生するので確認する

公式デモ

課金プラグインを入れると、以下に入ってるので確認する

  • Assets/Plugins/UnityPurchasing/scenes/IAP Demo.unity
  • Assets/Plugins/UnityPurchasing/script/IAPDemo.cs

AndroidのPlayStoreの為替レートによる課金設定で7D7DE1A7が出る場合

エラー詳細

予期しないエラーが発生しました。もう一度お試しください(7D7DE1A7)

レバノンだけを抜いておけば、このエラーが出なくなる

参考

コードレスの場合の参考

http://it-happens.info/unity-purchase/

全部コードの場合の参考

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

http://www.kyucon.com/blog/2018/12/unity-playfab.html

https://gist.github.com/YoshihideSogawa/f7c118127ce50e593a5b4a12e8426d6e

https://qiita.com/tetr4lab/items/50d6817065c0ce2c9f04

https://docs.google.com/presentation/d/1An-HVm72fukut6LJ6n6XL1ArtlPqek0Eb_CpyXHUfOY/edit#slide=id.g49a6ed640d_4_22