Help us understand the problem. What is going on with this article?

Unity IAPを試してみた (Yet Another Purchaser.cs)

前提

  • Unity 2018.4.5f1
  • Unity IAP 1.22.0
  • Apple App Store、Google Play Store
  • この記事では、Unity IAPの一部機能を限定的に使用し、汎用性のない部分があります。
    • サーバレス、消費/非消費タイプ使用、購読タイプ未使用
  • この記事のソースは、実際のストアでテストしていますが、製品版での使用実績はありません。
    • ソース中のIDは実際と異なります。

公式ドキュメント

  • マニュアル
  • スクリプトリファレンス
    • 2019/08/02時点で、バージョンによって記述があったりなかったりします。
    • 記述あり 5.6、2017.1、2017.2、2017.3、2018.1、2018.2
    • 記述なし 2017.4、2018.3、2018.4、2019.1

ネームスペース

  • UnityEngine.Purchasing
    • 必須のネームスペースです。
  • UnityEngine.Purchasing.Security
    • レシートの検証で必要なネームスペースです。
    • スクリプトリファレンスに記述が見つかりません。
  • UnityEngine.Purchasing.Extension
    • この記事では扱いません。

初期化

初期化の開始

  • UnityPurchasing.Initialize ()を呼ぶことで、初期化を開始します。
UnityPurchasing.Initialize (Purchaser instance, ConfigurationBuilder builder);
  • 初期化の要求はブロックされず、後に、結果に応じたコールバックがあります。

イベントハンドラ

  • コールバックを受け取るために、IStoreListenerを継承したクラスのインスタンスが必要です。
    • 必ずしもMonoBehaviourを継承する必要はありません。
    • インターフェイスIStoreListenerでは、OnInitialized ()OnInitializeFailed ()OnPurchaseFailed ()ProcessPurchase ()の4つのイベントハンドラが必要になります。

準備

  • Initialize ()を呼ぶためには、ConfigurationBuilder builderのインスタンスを得る必要があります。
  • ConfigurationBuilder.Instance ()を呼ぶためには、IPurchasingModuleを継承したクラスのインスタンスが必要なようですが、この辺りを記載したドキュメントが見つかりません。
  • 付属のDemoでは、StandardPurchasingModuleがそのクラスにあたるようで、そのインスタンスを得て使われています。
  • 得られたインスタンスmoduleにストアの設定を行い、さらにbuilderインスタンスを得ます。
  • 得られたインスタンスbuilderに製品を登録し、検証器を生成して、初期化を開始します。
  • ここでは、クラスのコンストラクタで、準備から初期化の開始までを行っています。
    • コンストラクタがprivateなのは、シングルトンで使うためです。
Purchaser.cs
/// <summary>コンストラクタ</summary>
private Purchaser (IEnumerable<ProductDefinition> products) {
    Debug.Log ("Purchaser.Construct");
    var module = StandardPurchasingModule.Instance ();
    module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    isGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
    isAppleAppStoreSelected = Application.platform == RuntimePlatform.IPhonePlayer && module.appStore == AppStore.AppleAppStore;
    validator = new CrossPlatformValidator (GooglePlayTangle.Data (), AppleTangle.Data (), Application.identifier);
    var builder = ConfigurationBuilder.Instance (module);
    builder.AddProducts (products);
    UnityPurchasing.Initialize (this, builder);
}

製品定義

  • 先のコンストラクタが受け取ってbuilderに登録した製品定義は、製品のIDとタイプのセットです。
Sample.cs
var products = new [] {
    new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item1", ProductType.Consumable),
    new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item2", ProductType.NonConsumable),
    new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item3", ProductType.NonConsumable),
};
  • これらはストア・ダッシュボードでの設定と正しく呼応している必要があります。
    • Apple App Storeでは、IDと製品タイプの双方が設定されます。
    • Google Play Storeでは、IDが設定されますが、消費の有無についての設定はありません。
      • 消費タイプでは、アプリを消費したことを申告するだけです。
  • この記事では、消費タイプConsumableと非消費タイプNonConsumableだけを扱い、購読タイプは扱いません。

初期化の完了

  • 初期化に成功したら、得られたIStoreControllerIExtensionProviderを保存します。
Purchaser.cs
/// <summary>初期化完了</summary>
public void OnInitialized (IStoreController controller, IExtensionProvider extensions) {
    Debug.Log ($"Purchaser.Initialized {controller}, {extensions}");
    appleExtensions = extensions.GetExtension<IAppleExtensions> ();
    appleExtensions.RegisterPurchaseDeferredListener (OnDeferred);
    googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions> ();
    this.controller = controller;
    this.extensions = extensions;
    Inventory = new Inventory { };
    foreach (var product in controller.products.all) {
        Inventory [product] = possession (product);
    }
}

/// <summary>初期化失敗</summary>
public void OnInitializeFailed (InitializationFailureReason error) {
    Debug.LogError ($"Purchaser.InitializeFailed {error}");
    Unavailable = true;
}
  • iOSの'Ask to buy'関連、OnDeferredはテストできていません。
  • Inventoryについては、後述します。

製品目録

  • 初期化に成功すると、controller.products.allで、製品目録を得ることができます。
Sample.cs
foreach (var product in Purchaser.Products.all) {
    Debug.Log (Purchaser.GetProductProperties (product));
}
Purchaser.cs
/// <summary>製品目録 初期化時の製品IDに対してストアから得た情報</summary>
public static ProductCollection Products => Valid ? instance.controller.products : null;
Purchaser.cs
/// <summary>製品諸元</summary>
public static string GetProperties (this Product product) {
    return string.Join ("\n", new [] {
        $"id={product.definition.id} ({product.definition.storeSpecificId})",
        $"type={product.definition.type}",
        $"enabled={product.definition.enabled}",
        $"available={product.availableToPurchase}",
        $"localizedTitle={product.metadata.localizedTitle}({product.metadata.shortTitle ()})",
        $"localizedDescription={product.metadata.localizedDescription}",
        $"isoCurrencyCode={product.metadata.isoCurrencyCode}",
        $"localizedPrice={product.metadata.localizedPrice}",
        $"localizedPriceString={product.metadata.localizedPriceString}",
        $"transactionID={product.transactionID}",
        $"Receipt has={product.hasReceipt}",
        $"Purchaser.Valid={Purchaser.Valid}",
        $"Receipt validation={Purchaser.ValidateReceipt (product)}",
        $"Possession={Purchaser.Inventory [product]}",
    });
}

目録の謎

※以下は、Google Play StoreとApple App Store (Sandbox)で確認した内容です。製品でのテストではありません。

  • もし、初期化の際に製品定義を渡さなかったらどうなるのでしょうか?
    • その場合、製品目録は基本的に空になります。ただし、購入済みの非消費製品は取得されます。
    • ストアから製品IDのカタログが得られるわけではありません。つまり、ストアに新製品を登録しただけでは、製品に組み込めないのです。
  • ProductDefinition.enabledは、スクリプトリファレンスでは"This flag indicates whether a product should be offered for sale. It is controlled through the cloud catalog dashboard."と説明されています。
    • これを見る限り、ストアのダッシュボードで設定されている有効/無効状態を取得できるように読めますが、実際には常にtrueになります。
    • 例え、ストアに登録されていないIDを指定した場合でもtrueです。全く役に立ちません。
      • ストアにない場合は、Product.availableToPurchaseFalseになります。
    • Play Storeで無効にされている製品を購入しようとすると「原因不明の購入エラー」になります。
    • App Storeで無効にされている製品でも、Sandboxでは購入できてしまいます。
  • つまり、以下の制約が生じます。
    • ストアに登録されている未知の製品を取得することはできません。
    • ストアでの状態(有効/無効)を取得することはできません。
    • 購入の失敗が、ストアでの無効設定によるものと判別できません。
  • その結果、以下のような使い方になります。
    • ストアとは別の手段(あらかじめ組み込む、自前のサーバから取得するなど)で製品定義を保持する必要があります。
    • ストアでの製品の有効/無効は、アプリの使用する製品定義に連動させます。
      • 緊急時以外は、ストア独自に製品を無効化しないようにします。

購入

購入の開始

  • IStoreController.InitiatePurchase ()Productを渡すことで、購入が開始されます。
Purchaser.cs
/// <summary>課金開始</summary>
private bool purchase (Product product) {
    if (product != null && product.Valid ()) {
        Debug.Log ($"Purchaser.InitiatePurchase {product.definition.id} {product.metadata.localizedTitle} {product.metadata.localizedPriceString}");
        controller.InitiatePurchase (product);
        return true;
    }
    return false;
}
  • 購入の要求はブロックされず、後に、結果に応じたコールバックがあります。

購入の完了

  • 課金結果のコールバックでは、購入に関わる処理が全て完了したら、PurchaseProcessingResult.Completeを返します。
    • 消費タイプの場合は、消費が実行されます。
Purchaser.cs
/// <summary>課金失敗</summary>
public void OnPurchaseFailed (Product product, PurchaseFailureReason reason) {
    Debug.LogError ($"Purchaser.PurchaseFailed Reason={reason}\n{product.GetProperties ()}");
}

/// <summary>課金結果 有効な消耗品なら保留、それ以外は完了とする</summary>
public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs eventArgs) {
    var validated = ValidateReceipt (eventArgs.purchasedProduct);
    Debug.Log ($"Purchaser.ProcessPurchase {(validated ? "Validated" : "ValidationError")} {eventArgs.purchasedProduct.GetProperties ()}");
    Inventory [eventArgs.purchasedProduct] = validated;
    return (validated && eventArgs.purchasedProduct.definition.type == ProductType.Consumable) ? PurchaseProcessingResult.Pending : PurchaseProcessingResult.Complete;
}
  • このコードでは、消費タイプではPurchaseProcessingResult.Pendingを返し、それ以外ではCompleteを返します。
    • Pendingを返すと、消費は保留されます。
  • この保留状態は、(謎の)クラウドで保持されるためアプリが中断しても失われず、起動毎にProcessPurchase ()へのコールバックが繰り返されます。

    • 保留状態を終わらせるには、ProcessPurchase ()Completeを返すか、別途IStoreController.ConfirmPendingPurchase (product)を呼びます。
  • Product.hasReceiptは、起動直後に未購入または消費済みであればfalseとなり、購入完了時にはtrueに変化します。

    • しかし、Completeを返した場合も、消費を促すConfirmPendingPurchase (product)を行おうとも、その場ではfalseには戻りません。
    • つまり、hasReceiptを見て消費完了を知ることはできません。
    • また、ConfirmPendingPurchase (product)には、結果を知らせるコールバックがありません。
  • 従って、保留と消費の状態を判別するためには、Unity-IAPの外側で所持状態を管理する必要があります。

  • なお、非消費タイプでは、購入済みのhasReceiptは常にtrueになります。

所有状態の管理

  • このコードでは、InventoryというDictionary派生クラスを用意して、製品所有状態を管理しています。
    • 初期化完了のコールバック中で初期化しています。
    • Inventory [string 製品ID]またはInventory [Product 製品]で真偽値を得ることができます。
Purchaser.cs
/// <summary>productID基準でProductの在庫を表現する辞書</summary>
public class Inventory : Dictionary<string, bool> {

    /// <summary>Productによるアクセス</summary>
    public bool this [Product product] {
        get { return base [product.definition.id]; }
        set { base [product.definition.id] = value; }
    }

}

復元

  • Appleの既定では、ユーザーがこの処理を明示的に行える必要があるのですが、これを呼ばなくてもUnity-IAPが自動的に復元をしているようなので、それ以上の意味はないように思われます。正直よく分かりません。
Purchaser.cs
/// <summary>復元</summary>
private void restore (Action<bool> onRestored = null) {
    Debug.Log ("Purchaser.Restore");
    Action<bool> onTransactionsRestored = success => { OnTransactionsRestored (success); onRestored?.Invoke (success); };
    if (isGooglePlayStoreSelected) {
        googlePlayStoreExtensions.RestoreTransactions (onTransactionsRestored);
    } else if (isAppleAppStoreSelected) {
        appleExtensions.RestoreTransactions (onTransactionsRestored);
    }
}
Purchaser.cs
/// <summary>復元完了</summary>
private void OnTransactionsRestored (bool success) {
    Debug.Log ($"Purchaser.Restored {success}");
}

コード全容

Purchaser.cs
//  Copyright© tetr4lab.

using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;

/// <summary>UnityIAPを使う</summary>
namespace UnityInAppPuchaser {

    /// <summary>課金処理</summary>
    public class Purchaser : IStoreListener {

#region Static

        /// <summary>シングルトン</summary>
        private static Purchaser instance;

        /// <summary>在庫目録 製品の課金状況一覧、消費タイプは未消費を表す</summary>
        public static Inventory Inventory { get; private set; }

        /// <summary>有効 初期化が完了している</summary>
        public static bool Valid => (instance != null && instance.valid);

        /// <summary>使用不能 初期化に失敗した</summary>
        public static bool Unavailable { get; private set; }

        /// <summary>製品目録 初期化時の製品IDに対してストアから得た情報</summary>
        public static ProductCollection Products => Valid ? instance.controller.products : null;

        /// <summary>クラス初期化 製品のIDとタイプの一覧を渡す</summary>
        public static void Init (IEnumerable<ProductDefinition> products) {
            if (instance == null || Unavailable) {
                instance = new Purchaser (products);
            }
        }

        /// <summary>所有検証 有効なレシートが存在する</summary>
        private static bool possession (Product product) {
            return product.hasReceipt && Purchaser.ValidateReceipt (product);
        }

        /// <summary>レシート検証</summary>
        public static bool ValidateReceipt (string productID) {
            return (!string.IsNullOrEmpty (productID) && instance.validateReceipt (instance.controller.products.WithID (productID)));
        }

        /// <summary>レシート検証</summary>
        public static bool ValidateReceipt (Product product) {
            return (instance != null && instance.validateReceipt (product));
        }

        /// <summary>課金 指定製品の課金処理を開始する</summary>
        public static bool Purchase (string productID) {
            if (!string.IsNullOrEmpty (productID) && Valid) {
                return instance.purchase (instance.controller.products.WithID (productID));
            }
            return false;
        }

        /// <summary>課金 指定製品の課金処理を開始する</summary>
        public static bool Purchase (Product product) {
            if (product != null && Valid) {
                return instance.purchase (product);
            }
            return false;
        }

        /// <summary>保留した課金の完了 消費タイプの指定製品の保留していた消費を完了する</summary>
        public static bool ConfirmPendingPurchase (string productID) {
            if (!string.IsNullOrEmpty (productID) && Valid) {
                return instance.confirmPendingPurchase (instance.controller.products.WithID (productID));
            }
            return false;
        }

        /// <summary>保留した課金の完了 消費タイプの指定製品の保留していた消費を完了する</summary>
        public static bool ConfirmPendingPurchase (Product product) {
            if (product != null && Valid) {
                return instance.confirmPendingPurchase (product);
            }
            return false;
        }

        /// <summary>復元 課金情報の復元を行い、結果のコールバックを得ることができる</summary>
        public static void Restore (Action<bool> onRestored = null) {
            if (Valid) { instance.restore (onRestored); }
        }

#endregion

        /// <summary>コントローラー</summary>
        private IStoreController controller;
        /// <summary>拡張プロバイダ</summary>
        private IExtensionProvider extensions;
        /// <summary>Apple拡張</summary>
        private IAppleExtensions appleExtensions;
        /// <summary>Google拡張</summary>
        private IGooglePlayStoreExtensions googlePlayStoreExtensions;
        /// <summary>AppleAppStore</summary>
        private bool isAppleAppStoreSelected;
        /// <summary>GooglePlayStore</summary>
        private bool isGooglePlayStoreSelected;
        /// <summary>検証機構</summary>
        private CrossPlatformValidator validator;
        /// <summary>有効</summary>
        private bool valid => (controller != null && controller.products != null);

        /// <summary>コンストラクタ</summary>
        private Purchaser (IEnumerable<ProductDefinition> products) {
            Debug.Log ("Purchaser.Construct");
            var module = StandardPurchasingModule.Instance ();
            module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
            isGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
            isAppleAppStoreSelected = Application.platform == RuntimePlatform.IPhonePlayer && module.appStore == AppStore.AppleAppStore;
            validator = new CrossPlatformValidator (GooglePlayTangle.Data (), AppleTangle.Data (), Application.identifier);
            var builder = ConfigurationBuilder.Instance (module);
            builder.AddProducts (products);
            UnityPurchasing.Initialize (this, builder);
        }

        /// <summary>レシート検証</summary>
        private bool validateReceipt (Product product) {
            if (!valid || !product.hasReceipt) { return false; }
#if UNITY_EDITOR
            return true;
#else
            try {
                var result = validator.Validate (product.receipt);
                Debug.Log ("Purchaser.validateReceipt Receipt is valid. Contents:");
                return true;
            } catch (IAPSecurityException ex) {
                Debug.LogError ($"Purchaser.validateReceipt Invalid receipt {product.definition.id}, not unlocking content. {ex}");
                return false;
            }
#endif
        }

        /// <summary>課金開始</summary>
        private bool purchase (Product product) {
            if (product != null && product.Valid ()) {
                Debug.Log ($"Purchaser.InitiatePurchase {product.definition.id} {product.metadata.localizedTitle} {product.metadata.localizedPriceString}");
                controller.InitiatePurchase (product);
                return true;
            }
            return false;
        }

        /// <summary>保留した課金の完了</summary>
        private bool confirmPendingPurchase (Product product) {
            if (product != null && Inventory [product] && possession (product)) {
                controller.ConfirmPendingPurchase (product);
                Inventory [product] = false;
                Debug.Log ($"Purchaser.ConfirmPendingPurchase {product.GetProperties ()}");
                return true;
            }
            return false;
        }

        /// <summary>復元</summary>
        private void restore (Action<bool> onRestored = null) {
            Debug.Log ("Purchaser.Restore");
            Action<bool> onTransactionsRestored = success => { OnTransactionsRestored (success); onRestored?.Invoke (success); };
            if (isGooglePlayStoreSelected) {
                googlePlayStoreExtensions.RestoreTransactions (onTransactionsRestored);
            } else if (isAppleAppStoreSelected) {
                appleExtensions.RestoreTransactions (onTransactionsRestored);
            }
        }

#region Event Handler

        /// <summary>復元完了</summary>
        private void OnTransactionsRestored (bool success) {
            Debug.Log ($"Purchaser.Restored {success}");
        }

        /// <summary>iOS 'Ask to buy' 未成年者の「承認と購入のリクエスト」 承認または却下されると通常の購入イベントが発生する</summary>
        private void OnDeferred (Product product) {
            Debug.Log ($"Purchaser.Deferred {product.GetProperties ()}");
        }

        /// <summary>初期化完了</summary>
        public void OnInitialized (IStoreController controller, IExtensionProvider extensions) {
            Debug.Log ($"Purchaser.Initialized {controller}, {extensions}");
            appleExtensions = extensions.GetExtension<IAppleExtensions> ();
            appleExtensions.RegisterPurchaseDeferredListener (OnDeferred);
            googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions> ();
            this.controller = controller;
            this.extensions = extensions;
            Inventory = new Inventory { };
            foreach (var product in controller.products.all) {
                Inventory [product] = possession (product);
            }
        }

        /// <summary>初期化失敗</summary>
        public void OnInitializeFailed (InitializationFailureReason error) {
            Debug.LogError ($"Purchaser.InitializeFailed {error}");
            Unavailable = true;
        }

        /// <summary>課金失敗</summary>
        public void OnPurchaseFailed (Product product, PurchaseFailureReason reason) {
            Debug.LogError ($"Purchaser.PurchaseFailed Reason={reason}\n{product.GetProperties ()}");
        }

        /// <summary>課金結果 有効な消耗品なら保留、それ以外は完了とする</summary>
        public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs eventArgs) {
            var validated = ValidateReceipt (eventArgs.purchasedProduct);
            Debug.Log ($"Purchaser.ProcessPurchase {(validated ? "Validated" : "ValidationError")} {eventArgs.purchasedProduct.GetProperties ()}");
            Inventory [eventArgs.purchasedProduct] = validated;
            return (validated && eventArgs.purchasedProduct.definition.type == ProductType.Consumable) ? PurchaseProcessingResult.Pending : PurchaseProcessingResult.Complete;
        }

        /// <summary>破棄</summary>
        ~Purchaser () {
            Debug.Log ("Purchaser.Destruct");
            if (instance == this) {
                instance = null;
                Inventory = null;
                Unavailable = false;
            }
        }

#endregion

    }

    /// <summary>製品拡張</summary>
    public static class ProductExtentions {

        /// <summary>製品諸元</summary>
        public static string GetProperties (this Product product) {
            return string.Join ("\n", new [] {
                $"id={product.definition.id} ({product.definition.storeSpecificId})",
                $"type={product.definition.type}",
                $"enabled={product.definition.enabled}",
                $"available={product.availableToPurchase}",
                $"localizedTitle={product.metadata.localizedTitle}({product.metadata.shortTitle ()})",
                $"localizedDescription={product.metadata.localizedDescription}",
                $"isoCurrencyCode={product.metadata.isoCurrencyCode}",
                $"localizedPrice={product.metadata.localizedPrice}",
                $"localizedPriceString={product.metadata.localizedPriceString}",
                $"transactionID={product.transactionID}",
                $"Receipt has={product.hasReceipt}",
                $"Purchaser.Valid={Purchaser.Valid}",
                $"Receipt validation={Purchaser.ValidateReceipt (product)}",
                $"Possession={Purchaser.Inventory [product]}",
            });
        }

        /// <summary>有効性 製品がストアに登録されていることを示すが、ストアで有効かどうかには拠らない</summary>
        public static bool Valid (this Product product) {
            return (product.definition.enabled && product.availableToPurchase);
        }

        /// <summary>アプリ名を含まないタイトル</summary>
        public static string shortTitle (this ProductMetadata metadata) {
            return (metadata != null && !string.IsNullOrEmpty (metadata.localizedTitle)) ? (new Regex (@"\s*\(.+\)$")).Replace (metadata.localizedTitle, "") : string.Empty;
        }

    }

    /// <summary>productID基準でProductの在庫を表現する辞書</summary>
    public class Inventory : Dictionary<string, bool> {

        /// <summary>Productによるアクセス</summary>
        public bool this [Product product] {
            get { return base [product.definition.id]; }
            set { base [product.definition.id] = value; }
        }

    }

}
Sample.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Purchasing;
using UnityInAppPuchaser;

public class Sample : MonoBehaviour {

    [SerializeField] private Transform CatalogHolder = default;
    [SerializeField] private Button RestoreButton = default;

    /// <summary>製品目録</summary>
    private readonly ProductDefinition [] products = new [] {
            new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item1", ProductType.Consumable),
            new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item2", ProductType.NonConsumable),
            new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item3", ProductType.NonConsumable),
        };

    /// <summary>起動</summary>
    private void Start () {
        StartCoroutine (initPurchaser ());
    }

    /// <summary>開始処理</summary>
    private IEnumerator initPurchaser () {
        RestoreButton.interactable = false;
        Purchaser.Init (products);
        yield return new WaitUntil (() => Purchaser.Valid || Purchaser.Unavailable); // 初期化完了を待つ
        if (Purchaser.Valid) {
            Catalog.Create (CatalogHolder);
            foreach (var product in Purchaser.Products.all) {
                CatalogItem.Create (Catalog.ScrollRect.content, product);
            }
        }
        RestoreButton.interactable = true;
    }

    /// <summary>復元ボタン</summary>
    public void OnPushRestoreButton () {
        if (Purchaser.Unavailable) {
            StartCoroutine (initPurchaser ());
        } else if (Purchaser.Valid) {
            Purchaser.Restore (success => {
                if (!success) { ModalDialog.Create (transform.parent, "リストアに失敗しました。\nネットワーク接続を確認してください。"); }
            });
        }
    }

}
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away