Unity 内购系统升级,Amazon商店内购的集成

256 阅读8分钟

前言

       最近由于运营需求,需要集成Amazon 商店的内购。由于项目之前已经接入了Google 与 apple 商店的内购,具体过程欢迎阅读前篇关于unity 内购的文章【Unity 产品内购系统的集成】。根据unity 官方文档描述,unity iap 也是支持 amazon 商店的,于是直接拿着amazon平板进行测试。想不到在amazon平台下unity iap居然存在一些明显的bug,最后不得不接入amazon官方的iap sdk。下面我们就一起来探讨一下如何接入的。 

一、阅读官方文档,安装插件

       接入SDK之前,我们最好细心阅读一到两篇官方文档,避免少走弯路。

       文档地址如下:

developer.amazon.com/zh/docs/in-…

       SDK包下载地址如下:

developer.amazon.com/zh/docs/app… 

       官方SDK包有两个版本,如图所示:

       Appstore SDK是新版的SDK,下面的是旧版SDK。两个版本的SDK API 通用,可以无缝切换。下面以旧版本为例,我们一起来探讨一下集成整个内购SDK的过程。至于为什么不用新版本,文章最后会说明。

      下载完SDK包后,会有如下图的文件:

       我们把它导入到Unity 工程,这样安装SDK插件就算成功了。

二、设计内购基类

       我们要把与内购业务无关的商品业务代码抽离出来放到其类里面,这样Amazon 平台和其它平台都可以用一套接口暴露给使用者。如下所示:

using System;
using System.Collections.Generic;

namespace Gamelogic
{
    public enum PurchaseItemType
    {
        None = 0,
        Consumable = 1,
        NonConsumable = 2,
        Subscription = 3,

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

    public enum PuchasingPlatformType
    {
        Amazon = 0,
        GoogleAndApple = 1
    }

    public class PurchasingBase
    {
        public bool IsInitialized { protected set; get; } = false;
        /// <summary>
        /// 所有的产品
        /// </summary>
        protected 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>();
        public event Action<Dictionary<string, PurchasingItem>> OnInitSuccess;
        public event Action<string> OnInitFaile;
        public event Action<PurchasingItem> OnPurchasingSuccess;
        public event Action<PurchasingItem, string> OnPurchasingFaile;

        public PuchasingPlatformType Platform { set; get; } = PuchasingPlatformType.GoogleAndApple;

        public void AddProduct(string id, int parentId, PurchaseItemType 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;
        }

        protected void CallOnInitSuccess(Dictionary<string, PurchasingItem> items)
        {
            OnInitSuccess?.Invoke(items);
        }

        protected void CallOnInitFaile(string errorMsg)
        {
            OnInitFaile?.Invoke(errorMsg);
        }

        protected void CallOnPurchasingSuccess(PurchasingItem item)
        {
            OnPurchasingSuccess?.Invoke(item);
        }
        protected void CallOnPurchasingFaile(PurchasingItem item, string errorMsg) 
        { 
            OnPurchasingFaile?.Invoke(item, errorMsg);
        }

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

        public virtual void Buy(string pid) { }

        public virtual void RestorePurchasing(Action<bool, string> callback) { }

    }
}

三、实现Amazon平台内购类

       Amazon内购类的实现,核心是监听四个事件回调。一是获取用户数据,二是获取产品数据,三是获取购买状态数据,四是购买回调。实现这四个回调的逻辑,内购类就实现了90%。代码如下:

#if(Amazon)
using com.amazon.device.iap.cpt;
using Simple.Log;
using System;
using System.Collections.Generic;

namespace Gamelogic
{
    public enum AmazonPurchaseResultType
    {
        None = -1,
        /// <summary>
        /// 成功
        /// </summary>
        Successful = 0,
        /// <summary>
        /// 失败
        /// </summary>
        Failed = 1,
        /// <summary>
        /// 无效的SKU
        /// </summary>
        InvalidSKU = 2,
        /// <summary>
        /// SKU已存在
        /// </summary>
        AlreadyPurchase = 3,
        /// <summary>
        /// 不支持
        /// </summary>
        NotSupported = 4
    }

    public class AmazonPurchasingMgr : PurchasingBase
    {
        private IAmazonIapV2 _iapService;
        private string _scriptionParentId = "RemoveAdSet";
        private string _subscriptionChildId = "a83001";
        /// <summary>
        /// 初始化内购
        /// </summary>
        public override void InitPurchasing()
        {
            IsInitialized = false;
            _iapService = AmazonIapV2Impl.Instance;
            _iapService.AddGetUserDataResponseListener(GetUserDataEventHandler);
            _iapService.AddPurchaseResponseListener(PurchaseEventHandler);
            _iapService.AddGetProductDataResponseListener(GetProductDataEventHandler);
            _iapService.AddGetPurchaseUpdatesResponseListener(GetPurchaseUpdatesEventHandler);
 
            //获取用户数据
            var resUser = _iapService.GetUserData();
            LogMgr.Log($"{nameof(InitPurchasing)} GetUserData, requestId {resUser.RequestId}");    
          
        }

        private AmazonPurchaseResultType GetResultType(string status)
        {
            if (status == "SUCCESSFUL")
                return AmazonPurchaseResultType.Successful;
            else if (status == "NOT_SUPPORTED")
                return AmazonPurchaseResultType.NotSupported;
            else if (status == "FAILED")
                return AmazonPurchaseResultType.Failed;
            else if (status == "INVALID_SKU")
                return AmazonPurchaseResultType.InvalidSKU;
            else if (status == "ALREADY_PURCHASED")
                return AmazonPurchaseResultType.AlreadyPurchase;
            else
                return AmazonPurchaseResultType.None;        
        }


        /// <summary>
        /// 获取用户数据影响
        /// </summary>
        /// <param name="args"></param>
        private void GetUserDataEventHandler(GetUserDataResponse args)
        {
            string status = args.Status;
            LogMgr.Log($"{nameof(GetUserDataEventHandler)} status {status}, toJson {args.ToJson()}");
            var rt = GetResultType(status);
            if (rt == AmazonPurchaseResultType.Successful)
            {
                string requestId = args.RequestId;
                string userId = args.AmazonUserData.UserId;
                string marketplace = args.AmazonUserData.Marketplace;              
                LogMgr.Log($"{nameof(GetUserDataEventHandler)} requestId {requestId}, userId {userId}, marketplace {marketplace}");

                //更新购买状态
                var request = new ResetInput();
                request.Reset = true;
                var response = _iapService.GetPurchaseUpdates(request);
                LogMgr.Log($"{nameof(InitPurchasing)} GetPurchaseUpdates, requestId {response.RequestId}");

                //获取产品数据
                var reqSku = new SkusInput();
                reqSku.Skus = new List<string>();
                foreach (var item in _items)
                {
                    var v = item.Value;
                    reqSku.Skus.Add(v.Id);
                    LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(InitPurchasing)} Purchasing product id {v.Id}, type {v.PType}");
                }

                var resSku = _iapService.GetProductData(reqSku);
                LogMgr.Log($"{nameof(InitPurchasing)} GetProductData, requestId {resSku.RequestId}");
            }
            else
            {
                IsInitialized = false;
                CallOnInitFaile("GetUserData failed");
            }
        }        

        /// <summary>
        /// 获取产品数据响应
        /// </summary>
        /// <param name="args"></param>
        private void GetProductDataEventHandler(GetProductDataResponse args)
        {
            LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetProductDataEventHandler)} status {args.Status}, toJson {args.ToJson()}");
            string status = args.Status;
            var rt = GetResultType(status);
            if(rt == AmazonPurchaseResultType.Successful)
            {
                var productDataMap = args.ProductDataMap;
                foreach (var productkv in productDataMap)
                {
                    var product = productkv.Value;
                    var item = GetItem(product.Sku);
                    if(item != null)
                    {
                        item.PriceStr = product.Price;
                    }
                    else
                    {
                        LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetProductDataEventHandler)} sku is null {product.Sku}");
                    }
                }
                IsInitialized = true;
                CallOnInitSuccess(_items);
            }
            else
            {
                IsInitialized = false;
                CallOnInitFaile("GetUserData failed");
            }
        }

        /// <summary>
        /// 更新购买产品数据响应
        /// </summary>
        /// <param name="args"></param>
        private void GetPurchaseUpdatesEventHandler(GetPurchaseUpdatesResponse args)
        {
            LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} status {args.Status}, toJson {args.ToJson()}");
            var status = args.Status;   
            var rt = GetResultType(status);
            if(rt == AmazonPurchaseResultType.Successful)
            {
                SubscriptionItems.Clear();
                var receipts = args.Receipts;
                foreach( var receipt in receipts )
                {
                    LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} toJson {args.ToJson()}");
                    var pid = receipt.Sku;
                    if (pid == _scriptionParentId)
                    {
                        pid = _subscriptionChildId;
                    }
                    var item = GetItem(pid);
                    if (item != null)
                    {
                        if(item.PType == PurchaseItemType.Subscription)
                        {
                            var subOk = false;
                            var cancelDate = receipt.CancelDate;
                            if (cancelDate != 0)
                            {//退出时间不为0,表示用户取消了订阅,但还有剩余的时间没过期。
                             //如果需要,需要用PurchaseDate + 有效时间差,算出过期时间,再对比当前时间,小于当前时间没过期,大于过期
                             //公式如下:已过期 = PurchaseDate + 有效时间差 >= system.now
                                //var purchaseDate = receipt.PurchaseDate;
                                //var dtStart = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Local);
                                //var lt = long.Parse(purchaseDate + "0000");
                                //var toRealyTime = new TimeSpan(lt);
                                //var pdt = dtStart.Add(toRealyTime);
                                ////pdt = pdt.Add("有效时间");
                                //LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, type {item.PType}, buyed {item.Buyed}");
                                //LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, cancelDate {cancelDate}, purchaseDate {purchaseDate} {pdt}");
                                //var now = DateTime.Now;
                                //if (DateTime.Compare(now, pdt) < 0)
                                //{
                                //    subOk = true;
                                //    LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} pid {pid} subscription true, cancelDate {pdt}");
                                //}
                            }
                            else
                            {
                                subOk = true;
                                LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} pid {pid} subscription true, cancelDate {cancelDate}");
                            }
                         
                            if(subOk)
                            {//订阅有效                               
                                SubscriptionItems[pid] = item;                               
                            }         
                            else
                            {//订阅过期
                                LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} pid {pid} subscription false");
                            }
                            
                        }                        
                        item.Buyed = true;
                        LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(GetPurchaseUpdatesEventHandler)} pid {pid} buyed {item.Buyed}");
                    }
                }             
            }
        }

        /// <summary>
        /// 购买响应
        /// </summary>
        /// <param name="args"></param>
        private void PurchaseEventHandler(PurchaseResponse args)
        {
            LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} status {args.Status}, toJson {args.ToJson()}");
            var status = args.Status;
            var rt = GetResultType(status);
            if (rt == AmazonPurchaseResultType.Successful)
            {
                var item = args.PurchaseReceipt;
                var pid = item.Sku;
                if (item.Sku == _scriptionParentId)
                {
                    pid = _subscriptionChildId;
                }
                                
                var pitem = GetItem(pid);
                if (pitem != null)
                {
                    pitem.Buyed = true;
                    if (pitem.PType == PurchaseItemType.Subscription)
                    {                        
                        SubscriptionItems[pid] = _items[pid];
                        LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, type {pitem.PType}, buyed {pitem.Buyed}");

                        //var cancelDate = args.PurchaseReceipt.CancelDate;
                        //var purchaseDate = args.PurchaseReceipt.PurchaseDate;
                        //var cdt = new DateTime(cancelDate);
                        //var pdt = new DateTime(purchaseDate);
                        //LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, cancelDate {cancelDate} {cdt}, purchaseDate {purchaseDate} {pdt}");
                    }

                    //通知Amazon您的应用程序已将购买的商品交付给用户
                    var request = new NotifyFulfillmentInput();
                    request.ReceiptId = args.PurchaseReceipt.ReceiptId;
                    request.FulfillmentResult = "FULFILLED";                
                    _iapService.NotifyFulfillment(request);             

                    LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success productId {pid}, type {pitem.PType}, buyed {pitem.Buyed}");
                }
                else
                {
                    LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(PurchaseEventHandler)} success but sku local is null,  productId {pid}");
                }

                CallOnPurchasingSuccess(pitem);
            }
            else
            {
                CallOnPurchasingFaile(null, null);
            }
        }

        /// <summary>
        /// 购买商品
        /// </summary>
        /// <param name="pid"></param>
        public override void Buy(string pid)
        {
            LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized}");
            if (!IsInitialized)
            {             
                return;
            }

            var item = GetItem(pid);
            var request = new SkuInput();          
            request.Sku = item.Id;                                   
            var response = _iapService.Purchase(request);
            LogMgr.Log($"{nameof(AmazonPurchasingMgr)}.{nameof(Buy)} produt id {pid} requestId {response.RequestId}");               
        }       
    }
}
#endif

       注意,它必须继承基类PurchasingBase。

四、其它平台内购类的实现(GooglePlay、Apple)

       基它平台内购的实现,是利用Unity 官方插件【In App Purchasing】实现的,具体过程请参考文章【Unity 产品内购系统的集成】,代码如下:

#if (AppStore || GooglePlay)
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 PurchasingMgr : PurchasingBase, IDetailedStoreListener
    {             
        private IStoreController _controller;
        private IExtensionProvider _extensions;
        private IAppleExtensions _appleExtensions;
        private CrossPlatformValidator _validator;                  

        public Product GetProduct(string pid)
        {
            if(IsInitialized)
            {
                var product = _controller.products.WithID(pid);
                return product;
            }
            return null;
        }
        /// <summary>
        /// 初始化内购
        /// </summary>
        public override void InitPurchasing()
        {
            //先添加产品
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(InitPurchasing)} products {_items.Count}");
            IsInitialized = false;
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());          
            foreach(var item in _items)
            {
                var v = item.Value;
                var pt = ProductType.Consumable;
                if (v.PType == PurchaseItemType.NonConsumable)
                    pt = ProductType.NonConsumable;
                else if(v.PType== PurchaseItemType.Consumable)
                    pt = ProductType.Consumable;
                else if(v.PType == PurchaseItemType.Subscription) 
                    pt = ProductType.Subscription;    

                builder.AddProduct(v.Id, pt);
                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}");                
                CallOnInitFaile(message);
            }
        }
   
        /// <summary>
        /// 购买商品
        /// </summary>
        /// <param name="pid"></param>
        public override void Buy(string pid)
        { 
            LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(Buy)} IsInitialized {IsInitialized}");
            if (!IsInitialized)
            {      
                CallOnPurchasingFaile(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)
            {
                CallOnPurchasingFaile(null, "Product is null or availableToPurchase.");
                return;
            }      

            _controller.InitiatePurchase(p);

        }

        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];
                                    LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} subscript info pid {pid}");
                                }
                            }
                        }
                    }
                }
            }

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

            }
            else
            {
                IsInitialized = true;
                LogMgr.Log($"{nameof(PurchasingMgr)}.{nameof(OnInitialized)} success");
                CallOnInitSuccess(_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}");
            CallOnInitFaile(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}");
            CallOnPurchasingFaile(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 == PurchaseItemType.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}");
            CallOnPurchasingSuccess(pitem);
            return PurchaseProcessingResult.Complete;
        }

        /// <summary>
        /// apple 商店在游戏启动时需要重设一下商店状态
        /// </summary>
        /// <param name="callback"></param>
        public override 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);
        }
    }
}
#endif

五、使用方式

先初始化内购:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System;
using Newtonsoft.Json;
using Simple.Config;

namespace Gamelogic
{
    public class GameProcess : Singleton<GameProcess>
    {       
        public PurchasingBase PurchMgr { private set; get; }

        public void Init()
        {        
#if (AppStore || GooglePlay)         
            PurchMgr = new PurchasingMgr();
            PurchMgr.Platform = PuchasingPlatformType.GoogleAndApple;
#elif(Amazon)
            PurchMgr = new AmazonPurchasingMgr();
            PurchMgr.Platform = PuchasingPlatformType.Amazon;
           
#endif
  
        /// <summary>
        /// 进入游戏
        /// </summary>
        public void EnterGame()
        {           
#if (AppStore || GooglePlay || Amazon)
            if (!PurchMgr.IsInitialized)
            {
                //内购
                PurchMgr.ClearProduct();
                var shopItems = ConfigMgr.Instance.GetList<ShopCategoryConfig>();
                foreach (var item in shopItems)
                {
                    if (item.CategoryType == ShopCategoryTypeEnum.Prop)
                        continue;

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

                    PurchMgr.AddProduct(item.PurshasingId, item.Id, ptype);
                }


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


#if (AppStore || GooglePlay || Amazon)
        private void OnInitPurchasingCompleted(Dictionary<string, PurchasingItem> products)
        {
            foreach (var item in products)
            {
                var p = item.Value;
                if(p.PType == PurchaseItemType.NonConsumable && p.Buyed)
                {
                    LogMgr.Log($"{nameof(GameProcess)}.{nameof(OnInitPurchasingCompleted)} product id is: {p.Id}, buyed {p.ParentId}, type {p.PType}");
                    var buyCount = GameData.Instance.PlayerData.GetGiftBuys(p.ParentId);
                    if (buyCount <= 0)
                    {
                        GameData.Instance.PlayerData.AddGiftBuys(p.ParentId, 1);
                        LogMgr.Log($"{nameof(GameProcess)}.{nameof(OnInitPurchasingCompleted)} GetGiftBuys id is: {p.Id}, buyed {p.ParentId}, type {p.PType}");
                    }
                }
            }
        }
#endif     
                                  
        public void Dispose()
        {                    
#if (AppStore || GooglePlay || Amazon)
            PurchMgr.OnInitSuccess -= OnInitPurchasingCompleted;
#endif
        }
        
    }
}

然后才可以购买产品,注意要监听购买成功和失败的事件。

        private void OnBuy(UIStageGiftBinder binder)
        {           
#if (AppStore || GooglePlay || Amazon)
                if(GameProcess.Instance.PurchMgr.IsInitialized)
                {
                    UIMgr.Instance.Open<AdLoadingContext>();
                    GameProcess.Instance.PurchMgr.Buy(_curBuyGift.Config.PurshasingId);                 
                }              
#endif         
        }

#if (AppStore || GooglePlay || Amazon)
        public void OnPurchasingSuccess(PurchasingItem pitem)
        {
            UIMgr.Instance.Close<AdLoadingContext>();               
        }

        public void OnPurchasingFaile(PurchasingItem pitem, string error)
        {
            UIMgr.Instance.Close<AdLoadingContext>();
        }

#endif

六、添加配置节点

       接入大多数SDK都需要在AndroidManifest.xml添加一些节点,以开启某些权限或服务,Amazon 也不例外。我们打开AndroidMainfest.xml,在application 节点下添加如下红框节点,并将主Activity 添加属性android:exported="true",如图:

七、旧版Amazon SDK内购测试

       要进行测试,我们必须要遵循以下的步骤,否则有可能测试失败。

       1、使用Amazon的设备,手机、平板都可以

       2、从应用商店下载[App tester ] app 安装。

       3、接着从已添加商品的后台管理系统中下载一个名为【amazon.sdktester.json】的文件。

       4、把【amazon.sdktester.json】放到设备sd卡根目录下。

       5、安装游戏。

       6、打开CMD 或 PowerShell 窗口,输入 【./adb shell setprop debug.amazon.sandboxmode debug】命令,回车。如下图:

      7、打开游戏,按正常操作进行内购。

      8、按照以上7步走,基本上都可以测试一次成功。

      关于[App tester ]的使用,具体可以参考官方文档,在此不在赘述。

八、为何Aamzon内购要独立于Untiy 官方插件【In App Purchasing】实现

       经过测试,Unity【In App Purchasing】在google play和apple store 平台下都运行良好,但在amazon stroe 平台下却存在bug。bug 具体的表现是,购买商品成功后重新启动游戏却获取不到商品的正确购买状态。经多次测试,均是如此,最后不得不单独接入Amazon官方内购SDK。

       接入 Amazon官方内购SDK也是一波三折,按文档说明接入新版SDK后,进行 App tester 本地测试或进行LAT动态测试均没任何效果。监听三个事件的回调结果,如获取用户数据、获取产品数据、获取购买状态数据,均是失败状态。经多翻折腾,都摸不到问题所在,最后不得不尝试更换为旧版SDK。令人意外的是,旧版SDK一经导入,不作任何改动居然一次测试成功。

       感谢阅读!