質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.65%

Unity IAP + GooglePlay で消費型アイテム購入時に「ライブラリに追加しました」と表示される

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 3
  • VIEW 8,892

HiroshiWatanabe

score 2060

Unity 5.3.5p4 で Unity IAP を使い GooglePlay のアイテム課金(消費型)のテストをしていますが

・購入時に GooglePlay から受け取る情報内から orderId が無くなり、TransactionId も空で返ってきます
(SandboxのIDで購入成功時にはどちらも得られるんですが自前の商品のIDだとその情報が欠落します)

・テストアカウントで自前の商品を何度も購入できているので消費型としては機能しているように見えます
(消費型でなければ2回目の購入ができないらしいので)

・購入時に表示される GooglePlay の支払いましたパネル(Sandbox時)が出ず「ライブラリに追加しました」というパネルが表示されます

・Validate() を呼び出すとその中でクラッシュしてしまいます

検索しても原因や対策がわかりそうな情報がみつからず困っています。
似たような症状に遭遇した方や原因に見当がつく方がいましたら解決に向けて情報を頂けると助かります。
よろしくお願い致します。

---以下6/27追記(実際に使っている物から不要箇所削除しID類情報を変更するなど加工して掲載)---

#if UNITY_ANDROID || UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_TVOS
//#define RECEIPT_VALIDATION
#endif

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
#if RECEIPT_VALIDATION
using UnityEngine.Purchasing.Security;
#endif

public class Unity5Purchase : MonoBehaviour, IStoreListener {
    private static IStoreController m_StoreController;
    private static IExtensionProvider m_StoreExtensionProvider;
#if RECEIPT_VALIDATION
    private CrossPlatformValidator validator;
#endif

    // GooglePlayライセンスコード.
    const string API_KEY = "(ないしょ)";

    void Start() {
        if (m_StoreController == null) {
            InitializePurchasing();
        }
    }

    public void InitializePurchasing() {
        if (IsInitialized()) {
            return;// 初期化済み.
        }
        var module = StandardPurchasingModule.Instance();
        var builder = ConfigurationBuilder.Instance(module);
        builder.Configure<IGooglePlayConfiguration>().SetPublicKey(API_KEY);// GooglePlayのライセンスキー登録.
        // (以下のID等は実際に使っている物とは違います今回のための仮記載情報です)
        builder.AddProduct("PRODUCT_ID1", ProductType.Consumable, new IDs {
            { "test01", GooglePlay.Name },
            { "TEST01", AppleApPStore.Name },
            { "Test01", MacAppStore.Name },
        });
        builder.AddProduct("PRODUCT_ID2", ProductType.Consumable, new IDs {
            { "test02", GooglePlay.Name },
            { "TEST02", AppleApPStore.Name },
            { "Test02", MacAppStore.Name },
        });
#if RECEIPT_VALIDATION
        validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.bundleIdentifier);
#endif
        UnityPurchasing.Initialize(this, builder);
    }

    private bool IsInitialized() {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }

    // 購入ボタン押下時にここを呼び出す.
    void BuyProductID(string productId) {
        if (IsInitialized()) {
            Product product = m_StoreController.products.WithID(productId);
            if (product != null && product.availableToPurchase) {
                m_StoreController.InitiatePurchase(product);
            }
        }
    }

    public void RestorePurchases() {
    }

    // 
    // --- IStoreListener
    // 

    // 初期化成功時.
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {
        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
    }

    // 初期化失敗.
    public void OnInitializeFailed(InitializationFailureReason error) {
    }

    [Serializable]
    class unityReceipt {
        public string Store;
        public string TransactionID;
        public string Payload;
        public unityReceipt() {
            this.Store = null;
            this.TransactionID = null;
            this.Payload = null;
        }
    }

    [Serializable]
    class googlePlayPayload {
        public string json;
        public string signature;
        public googlePlayPayload() {
            this.json = null;
            this.signature = null;
        }
    }
    [Serializable]
    class googlePayloadJson {
        public string orderId;
        public string packageName;
        public string productId;
        public uint purchaseTime;
        public uint purchaseState;
        public string purchaseToken;
        public googlePayloadJson() {
            this.orderId = null;
            this.packageName = null;
            this.productId = null;
            this.purchaseTime = 0;
            this.purchaseState = 0;
            this.purchaseToken = null;
        }
    }

    private string getReceiptPayload(string strReceipt) {
        unityReceipt u5r = JsonUtility.FromJson<unityReceipt>(strReceipt);
        if (u5r != null) {
            string receipt = null;
            googlePlayPayload gpp = JsonUtility.FromJson<googlePlayPayload>(u5r.Payload);
            if (gpp != null) {
                receipt = gpp.json;
                if (!string.IsNullOrEmpty(receipt)) {
                    googlePayloadJson googleJson = JsonUtility.FromJson<googlePayloadJson>(receipt);
                    if (googleJson != null) {
                        receipt = googleJson.orderId;// ←何故かSandboxでしか存在しない.
                    } else {
                        receipt = null;
                    }
                }
            }
#if RECEIPT_VALIDATION
            if (Application.platform == RuntimePlatform.Android) {
                try {
                    var result = validator.Validate(strReceipt);// ←この中でクラッシュ.
                    foreach (IPurchaseReceipt productReceipt in result) {
                        GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
                        if (null != google) {

                        }
                    }
                } catch (IAPSecurityException) {
                    receipt = null;// 不正なレシートだったので不成立とする.
                }
            }
#endif
            return receipt;// OK
        }
        return null;
    }
    // 購入実行(成功).
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) {
        bool given = false;
        if (String.Equals(args.purchasedProduct.definition.id, "PRODUCT_ID1", StringComparison.Ordinal)) {
            string receipt = getReceiptPayload(args.purchasedProduct.receipt, "test01");
            if (string.IsNullOrEmpty(receipt)) {
                // 不正?想定外?
            } else {
                // (ここで付与)
                given = true;
            }
        } else if (String.Equals(args.purchasedProduct.definition.id, "PRODUCT_ID2", StringComparison.Ordinal)) {
            string receipt = getReceiptPayload(args.purchasedProduct.receipt, "test02");
            if (string.IsNullOrEmpty(receipt)) {
                // 不正?想定外?
            } else {
                // (ここで付与)
                given = true;
            }
        }
        if (!given) {
            // (付与失敗)
        }
        return PurchaseProcessingResult.Complete;
    }

    // 購入失敗.
    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason) {
        // (付与失敗)
    }
}

あと、6/20までに何度か購入テストしていた時のレシートメールは日本語の件名で
「テスト:Google Play のご注文明細(ご注文:2016/06/??) - Goole Play ご購入ありがとうご(以下略)」
のようにきていましたが6/24以降に来ているレシートメールは全て英語の件名で
「Test: Your Google Play Order Receipt from Jun ??,2016 - Google Play Thank you. You've made a purchase (以下略)」
のように変わっていました。(メールの内容(本文)は日本語のレシートですが件名だけ英語に変わった状態)

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • buibui80

    2016/06/27 15:25

    UnityIAPは情報が少ないので詳しい内容やソースを明記された方が回答を得られやすいと思います。特にレシート検証については情報がないので。

    キャンセル

  • HiroshiWatanabe

    2016/06/27 16:10

    追記しました。そのまま全部載せる訳にはいかないので改変してありますが概ねこんな感じです。

    キャンセル

  • buibui80

    2016/06/27 16:49

    自前の商品とはデベロッパーコンソールのアプリ内アイテムとして登録した商品のことでしょうか?

    キャンセル

  • HiroshiWatanabe

    2016/06/27 17:37 編集

    そうです。管理対象アイテムと定期購読と選べるので管理対象アイテムの方で登録されています。

    キャンセル

回答 3

checkベストアンサー

+1

https://developer.android.com/google/play/billing/billing_reference.html

A unique order identifier for the transaction. This identifier corresponds to the Google payments order ID. If the order is a test purchase made through the In-app Billing Sandbox, orderId is blank.

SandBoxだとorderIdは空になるようです。
よってTransactionIdも空になるのではないでしょうか?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/30 09:11

    ありがとうございます!
    orderIdが得られない件については了解致しました。
    本番で課金テストできるようになったら改めて確認しようと思います。
    他の問題は解決していませんがBAにさせて頂きます。

    キャンセル

  • 2016/06/30 11:43

    orderIdについての追加です。

    Note: For test purchases, leave the orderId field blank. You can use the purchaseToken field to identify test purchases.

    とあり、
    テストの際はorderIdのかわりにpurchaseTokenを代用する旨の記述がありました。
    Testing In-app Billingをしっかり読み込むとまだ何かあるかもしれませんね。

    「ライブラリに追加しました」の件は仕様変更があったみたいです。
    http://mtimsno.hatenablog.jp/entry/2016/06/27/213208

    キャンセル

  • 2016/06/30 13:14

    うぉー!なんと!…情報ありがとうございます!

    キャンセル

+1

Validate() を呼び出すとその中でクラッシュ件ですが、

#if UNITY_ANDROID || UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_TVOS
//#define RECEIPT_VALIDATION
#endif


これって RECEIPT_VALIDATION は定義されていますか?
UnityIAPのデモスクリプトではコメントアウトされていたのでそのまま利用していましたが、
常に IAPSecurityException になったので、

#define RECEIPT_VALIDATION


と常に定義する様にしたらValidateが通る様になりました。
最終的に以下の様にしています。

#if UNITY_ANDROID || UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_TVOS
#define RECEIPT_VALIDATION
#endif

C#は疎いので正しい記述かわかりませんが
RECEIPT_VALIDATION が定義されているか確認してみては如何でしょうか?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/07/01 16:26

    NullReferenceExceptionの件ですが、
    該当するかわかりませんがAnalytics&UnityIAP絡みの問題を
    Unity5.3.5p5で1つ対応されているみたいです。

    Analytics: Fixed a NullReferenceException when stores inform Unity IAP of purchase events without providing metadata for the purchased product.

    https://unity3d.com/jp/unity/qa/patch-releases

    キャンセル

  • 2016/07/01 17:57

    ありがとうございます!Unityをアップデートしてみます。

    ログはAndroid実機をUSBでPC(Windows)に接続して adb.exe logcat > file でファイルに保存したのを見ています。

    キャンセル

  • 2016/07/01 20:25

    すっかりlogcatのことを忘れてました。
    AndroidSDK付属のAndroid Device Monitor使えば実購入テスト中もリアルタイムにロギング出来ますね。

    キャンセル

+1

ログを見るとAを購入成功後にAの消費処理をしているようなのですがAを何度か連続購入してBを購入してAを購入するという流れだとAの購入に戻った時の消費処理では何故かAの消費ではなくBの消費処理をしようとする…みたいな混乱が生じてしまうようです。そうなると消費できていないので次にそれを買おうとすると所有しているからと購入できなくなります。

こちらは改善されました?
というのも当方でも同様の症状を確認しております。

--- 追記:7/4 15:10 ---

以下確認した問題になります。
誤りや追加がありましたら共有をお願いします。


(1) 消費型プロダクトの購入の際、
ProcessPurchase で PurchaseProcessingResult.Complete を返すか、
ConfirmPendingPurchase で購入トランザクションを完了させた後に
UnityIAPプラグインが自動実行する消費型プロダクトの消費処理にて、
消費対象となるプロダクトではなく購入済み他プロダクトの消費処理を勝手に行っている。
さらに購入済み他プロダクトが消費型プロダクト以外の場合も消費処理を実行しており当然ながら失敗している。
結果として消費対象のプロダクトは消費処理が行われないので再購入できない問題が発生する。


(2) 消費されていない消費型プロダクトを再購入する場合など、
ProcessPurchase で PurchaseProcessingResult.Complete を返した後に
OnPurchaseFailed が呼ばれることがある。
(失敗理由は PurchaseFailureReason.Unknown )
その為 ProcessPurchase 内で購入プロダクトの数量・アンロック管理を行う場合に不整合が発生してしまう。
一般的なエラーの場合は ProcessPurchase が呼ばれず直接 OnPurchaseFailed が呼ばれるので、
OnPurchaseFailed 内で数量・アンロック管理を行うことも不整合が発生する。


(3) 消費型プロダクトの消費処理はUnityIAP側で自動処理され、
その結果用のイベントリスナーが無いため消費の成否及び何を消費したのか把握するすべが無い。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/07/14 09:24

    Startでの初期化をやめてAwakeで初期化すると購入最初で発生していたエラーログが出なくなりました!ありがとうございます!

    キャンセル

  • 2016/07/14 14:40

    ValidateでNull Exceptionが出る原因がわかりました。
    Stripping Level の問題だったようです。
    テスト用に用意したプロジェクトではそこが Disable になっていたため動作していましたが、組み込んで使っていたプロジェクトでは Strip Byte Code に設定してあったため、最適化されて消え去ってしまった未使用と誤認した何かを参照してしまっているという状態になっていたようです。link.xmlでSystem.Security.Cryptographyだけ禁止にしてやるという措置を取るとNullエラー出なくなりました。

    キャンセル

  • 2016/07/14 16:57

    情報ありがとうございます。
    マニュアルには Stripping Level を Use micro mscorlib にすると
    Security が除かれると明記されていますが Strip Byte Code でも同様なんですね。
    自分は Stripping Level の存在自体知らなかったので参考になりました。

    備忘録的にURL貼っときます。
    http://docs.unity3d.com/ja/current/Manual/iphone-playerSizeOptimization.html
    http://docs.unity3d.com/ja/current/Manual/class-PlayerSettingsAndroid.html
    http://fspace.hatenablog.com/entry/2016/02/23/180816

    ちなみに、
    ProcessPurchase で Pending を返して購入を保留した状態で
    同じプロダクトを再購入すると ProcessPurchase の後でOnPurchaseFailed が呼ばれ
    正常処理が行えない症状がありますが、
    サポートからはアプリ側でPending管理を行い
    ユーザーが購入操作できない様にして欲しい旨の回答がありました。
    常にCompleteを返す場合関係無いですがサーバー連携を行う際は注意が必要です。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.65%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる
  • トップ
  • C#に関する質問
  • Unity IAP + GooglePlay で消費型アイテム購入時に「ライブラリに追加しました」と表示される