Unity 产品内购系统的集成

660 阅读7分钟

前言

       游戏产品上线少不了产品内购这一块业务。得益于unity 官方推出的夸平台内购插件,使得我们集成内购就变得更加简单和方便。下面我们就一起利用Unity的内购插件[In App Purchasing]来设计、实现我们的游戏内购业务。

一、导入插件、阅读官方文档

       我们打开包管理器,安装内购插件,结果如图:

       对于接入第三方SDK、插件,我们还是需要过一遍官方文档,这样接入过程心中更加有数,并减少出错机率。官方文档如下:

地址一:docs.unity3d.com/2021.3/Docu…

地址二:docs.unity3d.com/Packages/co…

二、确定内购产品

       根据文档描述,我们可以确定内购产品一般主要分为三类:

       1、非一次性产品,也就是可以重复购买的产品。

       2、一次性产品,也就是只可以购买一次的产品。

       3、订阅产品,也就是购买时间段有效性的产品。

       根据这样的产品特性,我们可以设计这样的两张表,方便夸平台使用。例如,运营的同学只需要按照这两张表内的数据,可以往Google play、Apple store、Amazon store后台填上内购产品数据,客户端也是依据这两张表读取产品数据进行初始化内购系统,这样就可以做到数据统一化。数据表设计具有父子关系,如下图:

       对于数据表的导入和导出,生成为游戏所能读取的格式,不在本文所讨论的范围内,但一个完整的游戏,依据业务都会具备这些功能。

三、定义内购产品项结构

      我们需要对内购产品进行交互,就必须有一个持久化数据项,数据项如下:

    public class PurchasingItem
    {
        public string Id;
        /// <summary>
        /// 自定数据表内购项的ID
        /// </summary>
        public int ParentId;
        /// <summary>
        /// 内购产品的三种类型之一
        /// </summary>
        public ProductType PType;
        /// <summary>
        /// 已经本地化的价格字符
        /// </summary>
        public string PriceStr;
        /// <summary>
        /// 是否已购买过
        /// </summary>
        public bool Buyed = false;
    }

四、定义基本的属性、方法和产品列表

/// <summary>
/// 所有的产品
/// </summary>
private Dictionary<string, PurchasingItem> _items = new Dictionary<string, PurchasingItem>();
public Dictionary<string, PurchasingItem> Items => _items;
/// <summary>
/// 订阅的产品
/// </summary>
public Dictionary<string, PurchasingItem> SubscriptionItems { private set; get; } = new Dictionary<string, PurchasingItem>(); 
private IStoreController _controller;
private IExtensionProvider _extensions;
private IAppleExtensions _appleExtensions;
private CrossPlatformValidator _validator;
public event Action<Dictionary<string, PurchasingItem>> OnInitSuccess;
public event Action<string> OnInitFaile;
public event Action<PurchasingItem> OnPurchasingSuccess;
public event Action<PurchasingItem, string> OnPurchasingFaile;
private bool _isInit = false;


public void AddProduct(string id, int parentId, ProductType t)
{
    var item = new PurchasingItem { Id = id, ParentId = parentId, PType = t };
    if(!_items.ContainsKey(id))
        _items.Add(id, item); 
}

public void ClearProduct()
{
    _items.Clear();
}

public PurchasingItem GetItem(string pid)
{
    _items.TryGetValue(pid, out var item);
    return item;
}

public Product GetProduct(string pid)
{
    if(IsInitialized())
    {
        var product = _controller.products.WithID(pid);
        return product;
    }
    return null;
}

五、初始化内购系统

       值得注意的是,我们初始化内购系统时,必须先初始化内购服务,内购服务初始始化成功之后才能接着初始化内购系统,它俩是先后顺序的关系。如下:

/// <summary>
/// 初始化内购
/// </summary>
public void InitPurchasing()
{
    //先添加产品
    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} products {_items.Count}");
    _isInit = false;
    var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());          
    foreach(var item in _items)
    {
        var v = item.Value;
        builder.AddProduct(v.Id, v.PType);
        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} Purchasing product id {v.Id}, type {v.PType}");
    }
    //然后初始化内购服务
    InitServices(builder);
    
}

/// <summary>
/// 然后初始化内购服务
/// </summary>
/// <param name="builder"></param>
private async void InitServices(ConfigurationBuilder builder)
{
    try
    {
        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} start");
        var options = new InitializationOptions().SetEnvironmentName("production");
        await UnityServices.InitializeAsync(options);
        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} success, begin init purchasing");
        UnityPurchasing.Initialize(this, builder);//内购服务初始化成功后方可初始化内购组件
    }
    catch(Exception e)
    {
      
        var message = e.Message;
        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)}, errorMsg {message}");
        OnInitFaile?.Invoke(message);
    }
}

public bool IsInitialized()
{
    //return _controller != null && _extensions != null;
    return _isInit;
}

六、初始化成功或失败后调用的方法

       初始化成功后,我们需要检查产品购买的状态,以更新我们的游戏。例如产品是否已购买过、是否可以购买、订阅项是否到期等。

/// <summary>
/// 初始化内购成功后会调用此方法
/// </summary>
/// <param name="controller"></param>
/// <param name="extensions"></param>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
    _controller = controller;
    _extensions = extensions;
    _appleExtensions = _extensions.GetExtension<IAppleExtensions>();

    _validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);

    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product count: {_controller.products.all.Length}");
    SubscriptionItems.Clear();
    foreach(var item in _controller.products.all)
    {                
        if (item.availableToPurchase)
        {
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, priceStr {item.metadata.localizedPriceString}, type {item.definition.type}");
            var pitem = GetItem(item.definition.storeSpecificId);
            if (pitem != null)
            {
                pitem.PriceStr = item.metadata.localizedPriceString;                     
                pitem.Buyed = item.hasReceipt;
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}, type {pitem.PType}");
            }

            if(item.receipt != null)
            {//检查已订阅的状态
                if(item.definition.type == ProductType.Subscription)
                {
                    if (CheckIfProductIsAvailableForSubscriptionManager(item.receipt))
                    {
                        var p = new SubscriptionManager(item, null);
                        var info = p.getSubscriptionInfo();
                        var pid = info.getProductId();
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product id is: " + pid);
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, purchase date is: " + info.getPurchaseDate());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription next billing date is: " + info.getExpireDate());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is subscribed? " + info.isSubscribed().ToString());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is expired? " + info.isExpired().ToString());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is cancelled? " + info.isCancelled());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is in free trial peroid? " + info.isFreeTrial());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is auto renewing? " + info.isAutoRenewing());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription remaining valid time until next billing date is: " + info.getRemainingTime());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is this product in introductory price period? " + info.isIntroductoryPricePeriod());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory localized price is: " + info.getIntroductoryPrice());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory price period is: " + info.getIntroductoryPricePeriod());
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());

                        if (info.isSubscribed() == Result.True && info.isExpired() == Result.False)
                        {
                            SubscriptionItems[pid] = _items[pid];
                        }
                    }
                }
            }
        }
    }

    if (Application.platform == RuntimePlatform.IPhonePlayer)
    {
        var hasRestore = HasRestorePurchasingKey();
        if (!hasRestore)
        {
          RestorePurchasing((a, b) => {
              _isInit = true;
              SaveRestorePurchasingKey();
              LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
              OnInitSuccess?.Invoke(_items);
          });
        }
        else
        {
            _isInit = true;
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
            OnInitSuccess?.Invoke(_items);
        }

    }
    else
    {
        _isInit = true;
        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
        OnInitSuccess?.Invoke(_items);
    }

}       


/// <summary>
/// 初始化失败会调用这方法
/// </summary>
/// <param name="error"></param>
/// <param name="message"></param>
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitializeFailed)} error {error}, errorMsg {message}");
    OnInitFaile?.Invoke(message);
}

七、购买产品

        购买产品之前,需要确保内购系统已经成功初始化完毕,如下:

        /// <summary>
        /// 购买商品
        /// </summary>
        /// <param name="pid"></param>
        public void Buy(string pid)
        { 
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized()}");
            if (!IsInitialized())
            {
                OnPurchasingFaile?.Invoke(null, "Purchasing not init.");
                return;
            }

            var p = GetProduct(pid);
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} produt id {pid} availableToPurchase {p.availableToPurchase}");
            if (p == null || !p.availableToPurchase)
            {
                OnPurchasingFaile?.Invoke(null, "Product is null or availableToPurchase.");
                return;
            }      

            _controller.InitiatePurchase(p);

        }

八、购买成功与失败调用的方法

        /// <summary>
        /// 购买失败会调用此方法
        /// </summary>
        /// <param name="product"></param>
        /// <param name="failureDescription"></param>
        public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
        {
            var pid = failureDescription.productId;
            var errorMsg = failureDescription.message;
            var item = GetItem(pid);
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnPurchaseFailed)} productId {pid}, reason {failureDescription.reason }, errorMsg {errorMsg}");
            OnPurchasingFaile?.Invoke(item, errorMsg);
        }
       
        /// <summary>
        /// 购买成功会调用此方法
        /// </summary>
        /// <param name="purchaseEvent"></param>
        /// <returns></returns>
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
        {          
            var item = purchaseEvent.purchasedProduct;   
            var pid = purchaseEvent.purchasedProduct.definition.storeSpecificId;
            var pitem = GetItem(pid);                   
            if (pitem != null)
            {
                if (pitem.PType == ProductType.Subscription)
                    SubscriptionItems[pid] = _items[pid];
                else
                {                   
                    pitem.PriceStr = item.metadata.localizedPriceString;
                    pitem.Buyed = item.hasReceipt;
                    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}");
                }             
            }

            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} success productId {pid}, type {pitem.PType}");
            OnPurchasingSuccess?.Invoke(pitem);
            return PurchaseProcessingResult.Complete;
        }

        必须要返回 PurchaseProcessingResult.Complete 类型,以通知unity服务,告知产品已成功购买完毕。

九、Apple store 的特殊处理

       Apple 商店在游戏启动时需要重设一下商店产品的状态,才会拿到准确的商品状态数据,代码如下:

/// <summary>
/// apple 商店在游戏启动时需要重设一下商店状态
/// </summary>
/// <param name="callback"></param>
public void RestorePurchasing(Action<bool, string> callback)
{
    if (Application.platform == RuntimePlatform.IPhonePlayer)
    {
        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} starting");
        if(_appleExtensions != null)
        {
            _appleExtensions.RestoreTransactions((a, b) => {
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} completed result ok {a} , result str {b}");
                callback?.Invoke(a, b);
            });

        }
    }
    else
    {
        callback?.Invoke(false, null);
    }
}

十、完整的代码设计

using System;
using System.Collections.Generic;
using UnityEngine.Purchasing.Security;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using Simple.Log;
using Unity.Services.Core;
using Unity.Services.Core.Environments;
using UnityEngine;
using Simple.AnalyticsSdk;

namespace Gamelogic
{
    public class PurchasingItem
    {
        public string Id;
        /// <summary>
        /// 自定数据表内购项的ID
        /// </summary>
        public int ParentId;
        /// <summary>
        /// 内购产品的三种类型之一
        /// </summary>
        public ProductType PType;
        /// <summary>
        /// 已经本地化的价格字符
        /// </summary>
        public string PriceStr;
        /// <summary>
        /// 是否已购买过
        /// </summary>
        public bool Buyed = false;
    }

    public class PurchasingMgr : Singleton<PurchasingMgr>, IDetailedStoreListener
    {
        /// <summary>
        /// 所有的产品
        /// </summary>
        private Dictionary<string, PurchasingItem> _items = new Dictionary<string, PurchasingItem>();
        public Dictionary<string, PurchasingItem> Items => _items;
        /// <summary>
        /// 订阅的产品
        /// </summary>
        public Dictionary<string, PurchasingItem> SubscriptionItems { private set; get; } = new Dictionary<string, PurchasingItem>(); 
        private IStoreController _controller;
        private IExtensionProvider _extensions;
        private IAppleExtensions _appleExtensions;
        private CrossPlatformValidator _validator;
        public event Action<Dictionary<string, PurchasingItem>> OnInitSuccess;
        public event Action<string> OnInitFaile;
        public event Action<PurchasingItem> OnPurchasingSuccess;
        public event Action<PurchasingItem, string> OnPurchasingFaile;
        private bool _isInit = false;


        public void AddProduct(string id, int parentId, ProductType t)
        {
            var item = new PurchasingItem { Id = id, ParentId = parentId, PType = t };
            if(!_items.ContainsKey(id))
                _items.Add(id, item); 
        }

        public void ClearProduct()
        {
            _items.Clear();
        }

        public PurchasingItem GetItem(string pid)
        {
            _items.TryGetValue(pid, out var item);
            return item;
        }

        public Product GetProduct(string pid)
        {
            if(IsInitialized())
            {
                var product = _controller.products.WithID(pid);
                return product;
            }
            return null;
        }
        /// <summary>
        /// 初始化内购
        /// </summary>
        public void InitPurchasing()
        {
            //先添加产品
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} products {_items.Count}");
            _isInit = false;
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());          
            foreach(var item in _items)
            {
                var v = item.Value;
                builder.AddProduct(v.Id, v.PType);
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} Purchasing product id {v.Id}, type {v.PType}");
            }
            //然后初始化内购服务
            InitServices(builder);
            
        }

        /// <summary>
        /// 然后初始化内购服务
        /// </summary>
        /// <param name="builder"></param>
        private async void InitServices(ConfigurationBuilder builder)
        {
            try
            {
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} start");
                var options = new InitializationOptions().SetEnvironmentName("production");
                await UnityServices.InitializeAsync(options);
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)} success, begin init purchasing");
                UnityPurchasing.Initialize(this, builder);//内购服务初始化成功后方可初始化内购组件
            }
            catch(Exception e)
            {
              
                var message = e.Message;
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitServices)}, errorMsg {message}");
                OnInitFaile?.Invoke(message);
            }
        }

        public bool IsInitialized()
        {
            //return _controller != null && _extensions != null;
            return _isInit;
        }

        /// <summary>
        /// 购买商品
        /// </summary>
        /// <param name="pid"></param>
        public void Buy(string pid)
        { 
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized()}");
            if (!IsInitialized())
            {
                OnPurchasingFaile?.Invoke(null, "Purchasing not init.");
                return;
            }

            var p = GetProduct(pid);
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} produt id {pid} availableToPurchase {p.availableToPurchase}");
            if (p == null || !p.availableToPurchase)
            {
                OnPurchasingFaile?.Invoke(null, "Product is null or availableToPurchase.");
                return;
            }      

            _controller.InitiatePurchase(p);

        }

        /// <summary>
        /// 获取订阅项
        /// </summary>
        /// <param name="pid"></param>
        /// <returns></returns>
        public PurchasingItem GetSubscriptionItem(string pid)
        {
            SubscriptionItems.TryGetValue(pid, out var item);
            return item;
        }

        private bool CheckIfProductIsAvailableForSubscriptionManager(string receipt)
        {
            var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
            if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload"))
            {
                LogMgr.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"))
                            {
                                LogMgr.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"))
                            //{
                            //    LogMgr.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"))
                            //{
                            //    LogMgr.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;
        }

        #region 过时的
        /// <summary>
        /// 过时的方法
        /// </summary>
        /// <param name="error"></param>
        public void OnInitializeFailed(InitializationFailureReason error)
        {

        }
        /// <summary>
        /// 过时的方法
        /// </summary>
        /// <param name="product"></param>
        /// <param name="failureReason"></param>
        /// <exception cref="NotImplementedException"></exception>
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {

        }
        #endregion

        /// <summary>
        /// 初始化内购成功后会调用此方法
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="extensions"></param>
        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            _controller = controller;
            _extensions = extensions;
            _appleExtensions = _extensions.GetExtension<IAppleExtensions>();

            _validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);

            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product count: {_controller.products.all.Length}");
            SubscriptionItems.Clear();
            foreach(var item in _controller.products.all)
            {                
                if (item.availableToPurchase)
                {
                    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, priceStr {item.metadata.localizedPriceString}, type {item.definition.type}");
                    var pitem = GetItem(item.definition.storeSpecificId);
                    if (pitem != null)
                    {
                        pitem.PriceStr = item.metadata.localizedPriceString;                     
                        pitem.Buyed = item.hasReceipt;
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}, type {pitem.PType}");
                    }

                    if(item.receipt != null)
                    {//检查已订阅的状态
                        if(item.definition.type == ProductType.Subscription)
                        {
                            if (CheckIfProductIsAvailableForSubscriptionManager(item.receipt))
                            {
                                var p = new SubscriptionManager(item, null);
                                var info = p.getSubscriptionInfo();
                                var pid = info.getProductId();
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product id is: " + pid);
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, purchase date is: " + info.getPurchaseDate());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription next billing date is: " + info.getExpireDate());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is subscribed? " + info.isSubscribed().ToString());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is expired? " + info.isExpired().ToString());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is cancelled? " + info.isCancelled());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is in free trial peroid? " + info.isFreeTrial());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, product is auto renewing? " + info.isAutoRenewing());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, subscription remaining valid time until next billing date is: " + info.getRemainingTime());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, is this product in introductory price period? " + info.isIntroductoryPricePeriod());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory localized price is: " + info.getIntroductoryPrice());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the product introductory price period is: " + info.getIntroductoryPricePeriod());
                                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info, the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());

                                if (info.isSubscribed() == Result.True && info.isExpired() == Result.False)
                                {
                                    SubscriptionItems[pid] = _items[pid];
                                }
                            }
                        }
                    }
                }
            }

            if (Application.platform == RuntimePlatform.IPhonePlayer)
            {
                var hasRestore = HasRestorePurchasingKey();
                if (!hasRestore)
                {
                  RestorePurchasing((a, b) => {
                      _isInit = true;
                      SaveRestorePurchasingKey();
                      LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
                      OnInitSuccess?.Invoke(_items);
                  });
                }
                else
                {
                    _isInit = true;
                    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
                    OnInitSuccess?.Invoke(_items);
                }

            }
            else
            {
                _isInit = true;
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
                OnInitSuccess?.Invoke(_items);
            }
        
        }       

        /// <summary>
        /// 初始化失败会调用这方法
        /// </summary>
        /// <param name="error"></param>
        /// <param name="message"></param>
        public void OnInitializeFailed(InitializationFailureReason error, string message)
        {
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitializeFailed)} error {error}, errorMsg {message}");
            OnInitFaile?.Invoke(message);
        }

        /// <summary>
        /// 购买失败会调用此方法
        /// </summary>
        /// <param name="product"></param>
        /// <param name="failureDescription"></param>
        public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
        {
            var pid = failureDescription.productId;
            var errorMsg = failureDescription.message;
            var item = GetItem(pid);
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnPurchaseFailed)} productId {pid}, reason {failureDescription.reason }, errorMsg {errorMsg}");
            OnPurchasingFaile?.Invoke(item, errorMsg);
        }
       
        /// <summary>
        /// 购买成功会调用此方法
        /// </summary>
        /// <param name="purchaseEvent"></param>
        /// <returns></returns>
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
        {          
            var item = purchaseEvent.purchasedProduct;   
            var pid = purchaseEvent.purchasedProduct.definition.storeSpecificId;
#if (Amazon)//亚马逊平台后台商店管理填写错误,特殊处理
            if (pid == "RemoveAdSet")
                pid = "a83001";
#endif
            var pitem = GetItem(pid);                   
            if (pitem != null)
            {
                if (pitem.PType == ProductType.Subscription)
                    SubscriptionItems[pid] = _items[pid];
                else
                {                   
                    pitem.PriceStr = item.metadata.localizedPriceString;
                    pitem.Buyed = item.hasReceipt;
                    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} product id is: {item.definition.storeSpecificId}, buyed {pitem.Buyed}");
                }             
            }

            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(ProcessPurchase)} success productId {pid}, type {pitem.PType}");
            OnPurchasingSuccess?.Invoke(pitem);
            return PurchaseProcessingResult.Complete;
        }

        /// <summary>
        /// apple 商店在游戏启动时需要重设一下商店状态
        /// </summary>
        /// <param name="callback"></param>
        public void RestorePurchasing(Action<bool, string> callback)
        {
            if (Application.platform == RuntimePlatform.IPhonePlayer)
            {
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} starting");
                if(_appleExtensions != null)
                {
                    _appleExtensions.RestoreTransactions((a, b) => {
                        LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(RestorePurchasing)} completed result ok {a} , result str {b}");
                        callback?.Invoke(a, b);
                    });

                }
            }
            else
            {
                callback?.Invoke(false, null);
            }
        }

        public bool HasRestorePurchasingKey()
        {
            var key = "AppleRestorePurchasing";
            if (PlayerPrefs.HasKey(key))
            {
                var v = PlayerPrefs.GetInt(key);
                return v == 1;
            }
            return false;
        }

        public void SaveRestorePurchasingKey()
        {
            var key = "AppleRestorePurchasing";
            PlayerPrefs.SetInt(key, 1);
        }
    }
}

十一、使用方式

       内购的初始化需要联网,我们需要尽可能的早让内购初始化完成,否则玩家进入到游戏后可能看不到商品或者购买不了。所以我们在进入游戏的时候就需要初始化内购,待进度条走完,内购也初始化成功了。如下:

        /// <summary>
        /// 进入游戏
        /// </summary>
        public void EnterGame()
        {         

#if (AppStore || GooglePlay || Amazon)
            if(!PurchasingMgr.Instance.IsInitialized())
            {
                //内购,读取我们设计的商品表初始化内购
                PurchasingMgr.Instance.ClearProduct();
                var shopItems = ConfigMgr.Instance.GetList<ShopCategoryConfig>();
                foreach (var item in shopItems)
                {
                    if (item.CategoryType == ShopCategoryTypeEnum.Prop)
                        continue;

                    var ptype = ProductType.Consumable;
                    if (item.PurchasingType == ProductPurchasingTypeEnum.Consumable)
                        ptype = ProductType.Consumable;
                    else if (item.PurchasingType == ProductPurchasingTypeEnum.NonConsumable)
                        ptype = ProductType.NonConsumable;
                    else if (item.PurchasingType == ProductPurchasingTypeEnum.Subscription)
                        ptype = ProductType.Subscription;

                    PurchasingMgr.Instance.AddProduct(item.PurshasingId, item.Id, ptype);
                }

                
                PurchasingMgr.Instance.OnInitSuccess -= OnInitPurchasingCompleted;
                PurchasingMgr.Instance.OnInitSuccess += OnInitPurchasingCompleted;
                PurchasingMgr.Instance.InitPurchasing();
            }
#endif


        }

十二、测试效果

       测试之前商店后台必须要添加完产品,并审核通过。我们以Apple store 商店为例来演示一下测试过程。我们打开游戏内的商品列表,点击购买产品,如下图:

稍等一回后,会弹出如下图的页面:

点击购买,会提示输入密码,如下图:

输入正确的密码后,点击登录后稍等一会,就返回购买成功了。如下图:

       Google 平台的测试过程类似Apple,但Amazon平台测试会稍为复杂一点,需要下载官方的【App tester 】APP,并必须是Amazon的设备,文档如下:developer.amazon.com/zh/docs/in-…。在此就不再描述其它平台的测试方式。

       感谢阅读!