「Unity/課金/サンプル」の版間の差分
(→全部コードで処理する場合) |
(→全部コードで処理する場合) |
||
(同じ利用者による、間の34版が非表示) | |||
行8: | 行8: | ||
===iOSの場合=== | ===iOSの場合=== | ||
− | # | + | #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) | + | #納税情報を入れたところ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の場合=== | ||
− | # | + | #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 ( | + | 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(" | + | 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; | ||
} | } | ||
} | } | ||
行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の場合
- GooglePlayDeveloper/指定アプリ/ストアで表示/アプリ内サービス/管理対象のアイテム/管理対象のアイテム作成
- プロダクトidを(stone10)などで入れる(storeと連携してないと、購入ボタンを押しても商品が無いとなる)
- 有効にするを選択して、有効になっていることを確認
日本語を追加した場合は、日本語の説明も入れないと、保存されないので気を付ける。
iOSの場合
- AppStoreConnect管理画面/app内課金/管理/+をクリック
- 消耗型を選択し、id(stone10)などを入れてく
- 審査用の画像は(640 x 920)で登録すればよい
- ituneconnectから契約を選択して、有料Appの利用規約に同意
- 口座情報を入れる(ゆうちょなら"Japan Post Bank"で"9900-[口座番号]"
- 納税にアメリカ納税情報を入れる
- 納税情報を入れたところ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の場合
- GooglePlayDeveloper/指定アプリ/ストアで表示/アプリ内サービス/定期購入
- プロダクトidを(com.example.hogeapp.monthly)などで入れる(storeと連携してないと、購入ボタンを押しても商品が無いとなる)
- 有効にするを選択して、有効になっていることを確認
日本語を追加した場合は、日本語の説明も入れないと、保存されないので気を付ける。
iOSの場合
- AppStoreConnect管理画面/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倍っぽい。
Unityから課金用ボタン設定
課金の記述方法として以下2パターンがある
- 全部コードで処理する方法
- コードレス処理がある。
全部コードで処理する場合
- 以下課金呼び出しコードを記述(例: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
- 上記をPurchaser.csで保存する
- Unityのヒエラルキーに新規Objectを作成し、適当な名前で、Purchaser.csをアタッチする
コードレスの場合
(未完成です)
- unityメニュー/windows/UnityIAP/CreateIAPButtonでボタンを作成する
- unityメニュー/windows/IAP Catalogをクリックし以下のような詳細データを入れる
id:monthlyなど type:Subscription(自動課金)
- 作ったbuttonのプロパティのproductIdに先ほどいれたid(monthlyなど)いれる
- 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