1010 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			C#
		
	
	
			
		
		
	
	
			1010 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			C#
		
	
	
| namespace Guru
 | ||
| {
 | ||
|     using System;
 | ||
|     using System.Linq;
 | ||
|     using UnityEngine;
 | ||
|     using UnityEngine.Purchasing;
 | ||
|     using UnityEngine.Purchasing.Security;
 | ||
|     using System.Collections.Generic;
 | ||
|     using Firebase.Crashlytics;
 | ||
|     
 | ||
|     public abstract class IAPServiceBase<T>: IStoreListener where T: IAPServiceBase<T> , new()
 | ||
|     {
 | ||
| 
 | ||
|         public static readonly int OrderRequestTimeout = 10;
 | ||
|         public static readonly int OrderRequestRetryTimes = 3;
 | ||
|         
 | ||
|         #region 属性定义
 | ||
| 
 | ||
|         private const string Tag = "[IAP]";
 | ||
|         private const string DefaultCategory = "Store";
 | ||
| 
 | ||
|         private static bool _showLog;
 | ||
|         
 | ||
|         private ConfigurationBuilder _configBuilder; // 商店配置创建器
 | ||
|         
 | ||
|         private IStoreController _storeController;
 | ||
|         private IExtensionProvider _storeExtensionProvider;
 | ||
|         private IAppleExtensions _appleExtensions;
 | ||
|         private IGooglePlayStoreExtensions _googlePlayStoreExtensions;
 | ||
|         
 | ||
|         private CrossPlatformValidator _validator;
 | ||
|         private Dictionary<string, ProductInfo> _products;
 | ||
|         protected Dictionary<string, ProductInfo> Products => _products;
 | ||
| 
 | ||
|         public bool IsInitialized => _storeController != null && _storeExtensionProvider != null;
 | ||
| 
 | ||
|         private Product _curPurchasingProduct = null;
 | ||
| 
 | ||
|         private IAPModel _model;
 | ||
|         /// <summary>
 | ||
|         /// 是否是首次购买
 | ||
|         /// </summary>
 | ||
|         public int PurchaseCount
 | ||
|         {
 | ||
|             get => _model.PurchaseCount;
 | ||
|             set => _model.PurchaseCount = value;
 | ||
|         }
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 是否是首个IAP
 | ||
|         /// </summary>
 | ||
|         public bool IsFirstIAP => PurchaseCount == 0;
 | ||
| 
 | ||
|         private byte[] _googlePublicKey;
 | ||
|         private byte[] _appleRootCert;
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 服务初始化回调
 | ||
|         /// </summary>
 | ||
|         public event Action<bool> OnInitResult;
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 恢复购买回调
 | ||
|         /// </summary>
 | ||
|         public event Action<bool, string> OnRestored;
 | ||
| 
 | ||
|         public event Action<string> OnBuyStart;
 | ||
|         public event Action<string, bool> OnBuyEnd;
 | ||
|         public event Action<string, string> OnBuyFailed;
 | ||
|         public event Action<string, string, bool> OnGetProductReceipt;
 | ||
| 
 | ||
| #if UNITY_IOS
 | ||
|         /// <summary>
 | ||
|         /// AppStore 支付, 处理苹果支付延迟反应
 | ||
|         /// </summary>
 | ||
|         /// <returns></returns>
 | ||
|         public Action<Product> OnAppStorePurchaseDeferred;
 | ||
| #endif
 | ||
| 
 | ||
|         #endregion
 | ||
|         
 | ||
|         #region 单利模式
 | ||
|         
 | ||
|         protected static T _instance;
 | ||
|         private static object _locker = new object();
 | ||
|         
 | ||
|         public static T Instance
 | ||
|         {
 | ||
|             get
 | ||
|             {
 | ||
|                 if (null == _instance)
 | ||
|                 {
 | ||
|                     lock (_locker)
 | ||
|                     {
 | ||
|                         _instance = Activator.CreateInstance<T>();
 | ||
|                         _instance.OnCreatedInit();
 | ||
|                     }
 | ||
|                 }
 | ||
|                 return _instance;
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 组件创建初始化
 | ||
|         /// </summary>
 | ||
|         protected virtual void OnCreatedInit()
 | ||
|         {
 | ||
|             Debug.Log("--- IAPService Init");
 | ||
|         }
 | ||
|         
 | ||
| 
 | ||
|         #endregion
 | ||
|         
 | ||
|         #region 初始化
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 初始化支付服务
 | ||
|         /// </summary>
 | ||
|         public virtual void Initialize(bool showLog = false)
 | ||
|         {
 | ||
|             _showLog = showLog;
 | ||
|             InitPurchasing();
 | ||
|         }
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 带有校验器的初始化
 | ||
|         /// </summary>
 | ||
|         /// <param name="googlePublicKey"></param>
 | ||
|         /// <param name="appleRootCert"></param>
 | ||
|         /// <param name="showLog"></param>
 | ||
|         public virtual void InitWithKeys(byte[] googlePublicKey, byte[] appleRootCert, bool showLog = false)
 | ||
|         {
 | ||
|             _googlePublicKey = googlePublicKey;
 | ||
|             _appleRootCert = appleRootCert;
 | ||
|             InitModel();
 | ||
|             Initialize(showLog);
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 初始化支付插件
 | ||
|         /// </summary>
 | ||
|         protected virtual void InitPurchasing()
 | ||
|         {
 | ||
|             _configBuilder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
 | ||
|             // 注入初始商品产品列表
 | ||
|             var settings = GetProductSettings();
 | ||
|             if (null != settings)
 | ||
|             {
 | ||
|                 int len = settings.Length;
 | ||
|                 
 | ||
|                 if(_products != null) _products.Clear();
 | ||
|                 _products = new Dictionary<string, ProductInfo>(len);
 | ||
|                 
 | ||
|                 ProductSetting item;
 | ||
|                 IDs ids;
 | ||
|                 bool emptyIDs = false;
 | ||
|                 for (int i = 0; i < len; i++)
 | ||
|                 {
 | ||
|                     item = settings[i];
 | ||
|                     ids = new IDs();
 | ||
|                     if (!string.IsNullOrEmpty(item.GooglePlayProductId))
 | ||
|                     {
 | ||
|                         ids.Add(item.GooglePlayProductId, GooglePlay.Name);
 | ||
|                     }
 | ||
|                     else
 | ||
|                     {
 | ||
| #if UNITY_ADNROID
 | ||
|                         emptyIDs = true;
 | ||
|                         LogE($"[IAP] --- GoogleProductId is empty, please check the product setting: {item.ProductName}");
 | ||
| #endif
 | ||
|                     }
 | ||
| 
 | ||
| 
 | ||
|                     if (!string.IsNullOrEmpty(item.AppStoreProductId))
 | ||
|                     {
 | ||
|                         ids.Add(item.AppStoreProductId, AppleAppStore.Name);
 | ||
|                     }
 | ||
|                     else
 | ||
|                     {
 | ||
| #if UNITY_IOS
 | ||
|                        emptyIDs = true;
 | ||
|                         LogE($"[IAP] --- AppleProductId is empty, please check the product setting: {item.ProductName}");
 | ||
| #endif
 | ||
|                     }
 | ||
| 
 | ||
|                     if (emptyIDs)
 | ||
|                     {
 | ||
|                         continue;
 | ||
|                     }
 | ||
|                     
 | ||
|                     _configBuilder.AddProduct(item.ProductId, item.Type, ids); // 添加商品
 | ||
| 
 | ||
|                     // 建立本地的商品信息列表
 | ||
|                     if (string.IsNullOrEmpty(item.Category)) item.Category = DefaultCategory;
 | ||
|                     _products[item.ProductId] = new ProductInfo() { Setting = item };
 | ||
|                 }
 | ||
|             }
 | ||
|             // 调用插件初始化
 | ||
|             UnityPurchasing.Initialize(this, _configBuilder);
 | ||
|         }
 | ||
|         
 | ||
|          /// <summary>
 | ||
|         /// 初始化成功
 | ||
|         /// </summary>
 | ||
|         /// <param name="controller"></param>
 | ||
|         /// <param name="extensions"></param>
 | ||
|         /// <exception cref="NotImplementedException"></exception>
 | ||
|         public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
 | ||
|         {
 | ||
|             LogI($"--- IAP Initialized Success");
 | ||
|             _storeController = controller;
 | ||
|             _storeExtensionProvider = extensions;
 | ||
|             
 | ||
| #if UNITY_IOS
 | ||
|             _appleExtensions = extensions.GetExtension<IAppleExtensions>();
 | ||
|             // On Apple platforms we need to handle deferred purchases caused by Apple's Ask to Buy feature.
 | ||
|             // On non-Apple platforms this will have no effect; OnDeferred will never be called.
 | ||
|             _appleExtensions.RegisterPurchaseDeferredListener(item =>
 | ||
|             {
 | ||
|                 LogI("Purchase deferred: " + item.definition.id);
 | ||
|                 OnAppStorePurchaseDeferred?.Invoke(item);
 | ||
|             });
 | ||
| #elif UNITY_ANDROID
 | ||
|             _configBuilder.Configure<IGooglePlayConfiguration>().SetObfuscatedAccountId(IPMConfig.IPM_UID);
 | ||
|             _googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
 | ||
|             // _googlePlayStoreExtensions.SetObfuscatedAccountId(IPMConfig.IPM_UID);
 | ||
|             //添加安装游戏后第一次初试化进行恢复购买的回调 只有安卓才有
 | ||
|             _googlePlayStoreExtensions.RestoreTransactions(OnRestoreHandle);
 | ||
| #endif
 | ||
|             
 | ||
|             foreach (var item in _storeController.products.all)
 | ||
|             {
 | ||
|                 if (!item.availableToPurchase)
 | ||
|                 {
 | ||
|                     continue;
 | ||
|                 }
 | ||
|                 
 | ||
|                 if (_products.ContainsKey(item.definition.id))
 | ||
|                 {
 | ||
|                     _products[item.definition.id].SetProduct(item);
 | ||
|                 }
 | ||
|             }
 | ||
| 
 | ||
|             InitValidator(); // 初始化订单验证器
 | ||
|             OnInitResult?.Invoke(true);
 | ||
|         }
 | ||
| 
 | ||
|          /// <summary>
 | ||
|          /// 初始化失败
 | ||
|          /// </summary>
 | ||
|          /// <param name="error"></param>
 | ||
|          /// <exception cref="NotImplementedException"></exception>
 | ||
|         public void OnInitializeFailed(InitializationFailureReason error)
 | ||
|         {
 | ||
|             LogE($"--- IAP Initialized Fail: {error}");
 | ||
|             OnInitResult?.Invoke(false);
 | ||
|         }
 | ||
| 
 | ||
|          /// <summary>
 | ||
|          /// 初始化失败
 | ||
|          /// </summary>
 | ||
|          /// <param name="error"></param>
 | ||
|          /// <param name="message"></param>
 | ||
|          /// <exception cref="NotImplementedException"></exception>
 | ||
|          public void OnInitializeFailed(InitializationFailureReason error, string message)
 | ||
|         {
 | ||
|             LogE($"--- IAP Initialized Fail: {error}   msg: {message}");
 | ||
|             OnInitResult?.Invoke(false);
 | ||
|         }
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 数据查询
 | ||
|         
 | ||
|         // <summary>
 | ||
|         /// 获取商品Info
 | ||
|         /// </summary>
 | ||
|         /// <param name="productName">商品名称</param>
 | ||
|         /// <returns></returns>
 | ||
|         public ProductInfo GetInfo(string productName)
 | ||
|         {
 | ||
|             if(null == Products || Products.Count == 0 ) return null;
 | ||
|             return Products.Values.FirstOrDefault(c => c.Name == productName);
 | ||
|         }
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 通过商品ID获取对应的信息
 | ||
|         /// </summary>
 | ||
|         /// <param name="productId">商品ID</param>
 | ||
|         /// <returns></returns>
 | ||
|         public ProductInfo GetInfoById(string productId)
 | ||
|         {
 | ||
|             if(null == Products || Products.Count == 0 ) return null;
 | ||
|             return Products.Values.FirstOrDefault(c => c.Id == productId);
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 获取道具价格
 | ||
|         /// </summary>
 | ||
|         /// <param name="id"></param>
 | ||
|         /// <returns></returns>
 | ||
|         public double GetProductPrice(string name)
 | ||
|         {
 | ||
|             if (_storeController == null || _storeController.products == null)
 | ||
|             {
 | ||
|                 return Fallback();
 | ||
|             }
 | ||
| 
 | ||
|             ProductInfo info = GetInfo(name);
 | ||
|             var product = _storeController.products.WithID(info.Id);
 | ||
|             if (product == null)
 | ||
|                 return Fallback();
 | ||
| 
 | ||
|             return (double)product.metadata.localizedPrice;
 | ||
| 
 | ||
|             double Fallback()
 | ||
|             {
 | ||
|                 ProductInfo info = GetInfo(name);
 | ||
|                 return info?.Price ?? 0.0;
 | ||
|             }
 | ||
|         }
 | ||
|         
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 获取道具价格(带单位 $0.01)
 | ||
|         /// </summary>
 | ||
|         /// <param name="name"></param>
 | ||
|         /// <returns></returns>
 | ||
|         public string GetProductPriceString(string name)
 | ||
|         {
 | ||
|             if (_storeController == null || _storeController.products == null)
 | ||
|             {
 | ||
|                 return Fallback();
 | ||
|             }
 | ||
| 
 | ||
|             ProductInfo info = GetInfo(name);
 | ||
|             var product = _storeController.products.WithID(info.Id);
 | ||
|             if (product == null)
 | ||
|                 return Fallback();
 | ||
| 
 | ||
|             return product.metadata.localizedPriceString;
 | ||
| 
 | ||
|             string Fallback()
 | ||
|             {
 | ||
|                 ProductInfo info = GetInfo(name);
 | ||
|                 var pr = info?.Price ?? 0.0;
 | ||
|                 return "$" + pr;
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 获取 IAP 内置商品
 | ||
|         /// </summary>
 | ||
|         /// <param name="productName"></param>
 | ||
|         /// <returns></returns>
 | ||
|         public Product GetProduct(string productName)
 | ||
|         {
 | ||
|             if (_storeController != null && _storeController.products != null)
 | ||
|             {
 | ||
|                 var info = GetInfo(productName);
 | ||
|                 if (info != null)
 | ||
|                 {
 | ||
|                     return _storeController.products.WithID(info.Id);
 | ||
|                 }
 | ||
|                 Debug.LogError($"[IAP] --- can't find <ProductInfo> with name {productName}");
 | ||
|             }
 | ||
|             
 | ||
|             // 商品不存在则返回 NULL
 | ||
|             Debug.LogError($"[IAP] --- _storeController is null or products is null or products.all.Length == 0");
 | ||
|             return null;
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 当前的商品是否已经持有收据了
 | ||
|         /// </summary>
 | ||
|         /// <param name="productName"></param>
 | ||
|         /// <returns></returns>
 | ||
|         public bool IsProductHasReceipt(string productName)
 | ||
|         {
 | ||
|             var product = GetProduct(productName);
 | ||
|             if (product != null) return product.hasReceipt;
 | ||
|             return false;
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 订单验证器
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 是否支持订单校验
 | ||
|         /// </summary>
 | ||
|         /// <returns></returns>
 | ||
|         private bool IsCurrentStoreSupportedByValidator() 
 | ||
|             => IsGooglePlayStoreSelected() || IsAppleAppStoreSelected();
 | ||
| 
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// Google 商店支持
 | ||
|         /// </summary>
 | ||
|         /// <returns></returns>
 | ||
|         private bool IsGooglePlayStoreSelected()
 | ||
|         {
 | ||
|             var currentAppStore = StandardPurchasingModule.Instance().appStore;
 | ||
|             return currentAppStore == AppStore.GooglePlay;
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// Apple 商店支持
 | ||
|         /// </summary>
 | ||
|         /// <returns></returns>
 | ||
|         private bool IsAppleAppStoreSelected()
 | ||
|         {
 | ||
|             var currentAppStore = StandardPurchasingModule.Instance().appStore;
 | ||
|             return currentAppStore == AppStore.AppleAppStore || currentAppStore == AppStore.MacAppStore;
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 初始化订单校验器
 | ||
|         /// </summary>
 | ||
|         protected virtual void InitValidator()
 | ||
|         {
 | ||
|             if (IsCurrentStoreSupportedByValidator())
 | ||
|             {
 | ||
|                 try
 | ||
|                 {
 | ||
|                     if (_googlePublicKey != null && _appleRootCert != null)
 | ||
|                     {
 | ||
|                         _validator = new CrossPlatformValidator(_googlePublicKey, _appleRootCert, Application.identifier);
 | ||
|                     }
 | ||
|                     else
 | ||
|                     {
 | ||
|                         Crashlytics.LogException(new Exception($"[IAP] Init Validator failed -> googlePublicKey: {_googlePublicKey}  appleRootCert: {_appleRootCert}"));
 | ||
|                     }
 | ||
|                 }
 | ||
|                 catch (NotImplementedException exception)
 | ||
|                 {
 | ||
|                     LogE("Cross Platform Validator Not Implemented: " + exception);
 | ||
|                 }
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 恢复购买
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 恢复购买
 | ||
|         /// </summary>
 | ||
|         /// <param name="success"></param>
 | ||
|         /// <param name="msg"></param>
 | ||
|         protected virtual void OnRestoreHandle(bool success, string msg)
 | ||
|         {
 | ||
|             LogI($"--- Restore complete: {success}: msg:{msg}" );
 | ||
| 
 | ||
|             
 | ||
|             if (success)
 | ||
|             {
 | ||
|                 bool isIAPUser = false;
 | ||
|                 // 扫描所有商品, 追加用户属性
 | ||
|                 for (int i = 0; i < _storeController.products.all.Length; i++)
 | ||
|                 {
 | ||
|                     var product = _storeController.products.all[i];
 | ||
|                     if (product.hasReceipt)
 | ||
|                     {
 | ||
|                         isIAPUser = true;
 | ||
|                     }
 | ||
|                 }
 | ||
|                 SetIsIAPUser(isIAPUser);
 | ||
|             }
 | ||
|             
 | ||
|             OnRestored?.Invoke(success, msg);
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 恢复购买道具
 | ||
|         /// </summary>
 | ||
|         public virtual void Restore()
 | ||
|         {
 | ||
|             if (!IsInitialized) return;
 | ||
| 
 | ||
| #if UNITY_IOS
 | ||
|             _appleExtensions.RestoreTransactions(OnRestoreHandle);
 | ||
| #elif UNITY_ANDROID
 | ||
|             _googlePlayStoreExtensions.RestoreTransactions(OnRestoreHandle);
 | ||
| #endif
 | ||
|         }
 | ||
|         
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 购买流程
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 购买商品
 | ||
|         /// </summary>
 | ||
|         /// <param name="productName"></param>
 | ||
|         public virtual T Buy(string productName)
 | ||
|         {
 | ||
|             if (!IsInitialized)
 | ||
|             {
 | ||
|                 LogE("Buy FAIL. Not initialized.");
 | ||
|                 OnBuyEnd?.Invoke(productName, false);
 | ||
|                 return (T)this;
 | ||
|             }
 | ||
|             ProductInfo info = GetInfo(productName);
 | ||
|             if (info == null)
 | ||
|             {
 | ||
|                 LogE($"Buy FAIL. No product with name: {productName}");
 | ||
|                 OnBuyEnd?.Invoke(productName, false);
 | ||
|                 return (T)this;
 | ||
|             }
 | ||
|             
 | ||
|             Product product = _storeController.products.WithID(info.Setting.ProductId);
 | ||
|             if (product != null && product.availableToPurchase)
 | ||
|             {
 | ||
| #if UNITY_ANDROID
 | ||
|                 _configBuilder
 | ||
|                     .Configure<IGooglePlayConfiguration>()
 | ||
|                     .SetObfuscatedAccountId(IPMConfig.IPM_UID);
 | ||
| #endif
 | ||
|                 _storeController.InitiatePurchase(product);
 | ||
| 
 | ||
|                 Analytics.IAPClick(info?.Category??"none", product.definition.id);
 | ||
|                 Analytics.IAPImp(DefaultCategory, product.definition.id);
 | ||
| 
 | ||
|                 _curPurchasingProduct = product;
 | ||
| 
 | ||
|                 OnBuyStart?.Invoke(productName);
 | ||
|                 return (T)this;
 | ||
|             }
 | ||
| 
 | ||
|             // 找不到商品
 | ||
|             LogE($"Can't find product by name: {productName}, pay canceled.");
 | ||
|             OnPurchaseOver(false, productName);
 | ||
|             OnBuyEnd?.Invoke(productName, false);
 | ||
| 
 | ||
|             return (T)this;
 | ||
|         }
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 处理支付流程
 | ||
|         /// </summary>
 | ||
|         /// <param name="purchaseEvent"></param>
 | ||
|         /// <returns></returns>
 | ||
|         /// <exception cref="NotImplementedException"></exception>
 | ||
|         public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
 | ||
|         {
 | ||
|             string productId = purchaseEvent.purchasedProduct.definition.id;
 | ||
|             ProductInfo info = GetInfoById(productId);
 | ||
|             bool success = false;
 | ||
|             
 | ||
|             if (null != info)
 | ||
|             {
 | ||
|                 if (IsFirstIAP) Analytics.FirstIAP(info.Id, info.Price, info.CurrencyCode); // 上报首次支付打点
 | ||
|                 Analytics.ProductIAP(info.Id,info.Id, info.Price, info.CurrencyCode);
 | ||
|                 Analytics.IAPRetTrue(info.Category, info.Id, info.Price, info.CurrencyCode, info.Type, info.IsFree);
 | ||
|                 success = true;
 | ||
|                 
 | ||
|                 SetIsIAPUser(success); // 设置用户属性标记
 | ||
|             }
 | ||
|             
 | ||
|             PurchaseCount++; // 记录支付次数
 | ||
|             Debug.Log($"############ ProcessPurchase: PurchaseCount:{PurchaseCount}");
 | ||
|             if (_curPurchasingProduct != null)
 | ||
|             {
 | ||
|                 _curPurchasingProduct = null; // 只有实际发生购买后才会有订单上报.  启动时的 Restore 操作自动调用支付成功. 这里做一个判定, 过滤掉订单的物品
 | ||
|                 ReportPurchaseResult(purchaseEvent); // 支付结果上报
 | ||
|             }
 | ||
|             
 | ||
|             var pp = purchaseEvent.purchasedProduct;
 | ||
|             if ( pp == null || string.IsNullOrEmpty(pp.receipt))
 | ||
|             {
 | ||
|                 string msg = $"{Tag} ---  Purchased product is null or has no receipt!!";
 | ||
|                 Debug.LogError(msg);
 | ||
|                 Crashlytics.LogException(new Exception(msg));
 | ||
|             }
 | ||
|             else
 | ||
|             {
 | ||
|                 OnGetProductReceipt?.Invoke(pp.definition.id, pp.receipt, pp.appleProductIsRestored);
 | ||
|             }
 | ||
|             
 | ||
|             string productName = info?.Name ?? "NULL";
 | ||
|             LogI($"{Tag} --- OnPurchaseSuccess :: purchase count: {PurchaseCount}  productName: {productName}");
 | ||
|             
 | ||
|             OnPurchaseOver(success, productName); // 支付成功处理逻辑
 | ||
|             OnBuyEnd?.Invoke(productName, success);
 | ||
|             
 | ||
|             return PurchaseProcessingResult.Complete; // 直接Consume 掉当前的商品
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 支付失败
 | ||
|         /// </summary>
 | ||
|         /// <param name="product"></param>
 | ||
|         /// <param name="failureReason"></param>
 | ||
|         /// <exception cref="NotImplementedException"></exception>
 | ||
|         public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
 | ||
|         {
 | ||
|             string productId = product.definition.id;
 | ||
|             ProductInfo info = GetInfoById(productId);
 | ||
|         
 | ||
|             //上报点位,用户购买失败的原因
 | ||
|             if (failureReason == PurchaseFailureReason.UserCancelled)
 | ||
|             {
 | ||
|                 Analytics.IAPClose(info.Category, product.definition.id);
 | ||
|             }
 | ||
|             else
 | ||
|             {
 | ||
|                 Analytics.IAPRetFalse(info.Category, product.definition.id, failureReason.ToString());
 | ||
|             }
 | ||
| 
 | ||
|             LogI($"{Tag} --- OnPurchaseFailed :: failureReason = {failureReason}");
 | ||
|             // 失败的处理逻辑
 | ||
|             OnPurchaseOver(false, info.Name);
 | ||
|             OnBuyEnd?.Invoke(info.Name, false);
 | ||
|             // 失败原因
 | ||
|             OnBuyFailed?.Invoke(info.Name, failureReason.ToString());
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 获取商品的本地化价格字符串
 | ||
|         /// 如果商品不存在或者 IAP 尚未初始化完成则显示 "Loading" 字样
 | ||
|         /// </summary>
 | ||
|         /// <param name="productName"></param>
 | ||
|         /// <returns></returns>
 | ||
|         public string GetLocalizedPriceString(string productName)
 | ||
|         {
 | ||
|             return GetInfo(productName)?.LocalizedPriceString ?? "Loading";
 | ||
|         }
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region Log 输出
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 日志输出
 | ||
|         /// </summary>
 | ||
|         /// <param name="msg"></param>
 | ||
|         private static void LogI(object msg)
 | ||
|         {
 | ||
|             if (_showLog) 
 | ||
|                 Debug.Log($"{Tag} {msg}");
 | ||
|         }
 | ||
| 
 | ||
|         private static void LogE(object msg)
 | ||
|         {
 | ||
|             if (_showLog) 
 | ||
|                 Debug.LogError($"{Tag} {msg}");
 | ||
|         }
 | ||
| 
 | ||
|         private static void LogW(object msg)
 | ||
|         {
 | ||
|             if (_showLog)
 | ||
|                 Debug.LogWarning($"{Tag} {msg}");
 | ||
|         }
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 实现接口
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 需要游戏侧继承并完成Blevel的取值上报
 | ||
|         /// </summary>
 | ||
|         /// <returns></returns>
 | ||
|         protected abstract int GetBLevel();
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 获取商品品配置列表
 | ||
|         /// </summary>
 | ||
|         /// <returns></returns>
 | ||
|         protected virtual ProductSetting[] GetProductSettings()
 | ||
|             => GuruSettings.Instance.Products;
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 支付回调
 | ||
|         /// </summary>
 | ||
|         /// <param name="success">是否成功</param>
 | ||
|         /// <param name="productName">商品名称</param>
 | ||
|         protected abstract void OnPurchaseOver(bool success, string productName);
 | ||
| 
 | ||
|         #endregion
 | ||
|         
 | ||
|         #region 支付上报逻辑
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 支付结果上报
 | ||
|         /// </summary>
 | ||
|         protected virtual void ReportPurchaseResult(PurchaseEventArgs args)
 | ||
|         {
 | ||
|             int blevel = GetBLevel();
 | ||
|             int orderType = args.purchasedProduct.definition.type == ProductType.Subscription ? 1 : 0;
 | ||
| 
 | ||
| 
 | ||
|             // Debug.Log($"############ ReportPurchaseResult: _validator:{_validator }");
 | ||
|             
 | ||
|             if (_validator == null)
 | ||
|             {
 | ||
|                 // Debug.Log($"############ --- Validator is null");
 | ||
|                 LogE($"{Tag} --- Validator is null. Report Order failed.");
 | ||
|                 Crashlytics.LogException(new Exception($"IAPService can not report order because Validator is null!"));
 | ||
|                 return;
 | ||
|             }
 | ||
|             
 | ||
|             try
 | ||
|             {
 | ||
|                 // ----- 支付后的b_level上报逻辑 
 | ||
|                 LogI($"--- Report b_level:[{blevel}] with product id:{args.purchasedProduct.definition.id} ");
 | ||
|                 
 | ||
|                 
 | ||
| #if UNITY_EDITOR
 | ||
|                 // Editor 不做上报逻辑
 | ||
| #elif UNITY_ANDROID
 | ||
|                 // Android 订单验证, 上报打点信息
 | ||
|                 var result = _validator.Validate(args.purchasedProduct.receipt);
 | ||
|                 string productID = orderType == 0 ? args.purchasedProduct.definition.id : "";
 | ||
|                 string subscriptionID = orderType == 1 ? args.purchasedProduct.definition.id : "";
 | ||
|                 
 | ||
|                 LogI($"{Tag} --- Report Android IAP Order -> orderType:{orderType}  productID:{productID}  blevel:{blevel}");
 | ||
|                 foreach (var productReceipt in result)
 | ||
|                 {
 | ||
|                     if (productReceipt is GooglePlayReceipt google)
 | ||
|                     {
 | ||
|                         Debug.Log($"{Tag} --- Report Android IAP Order -> orderType:{orderType}  productID:{productID}  blevel:{blevel}");
 | ||
|                         // new GoogleOrderRequest(orderType, productID, subscriptionID, google.purchaseToken, blevel).SetTimeOut(5).Send();
 | ||
|                         ReportGoogleOrder(orderType, productID, subscriptionID, google.purchaseToken, blevel);
 | ||
|                     }
 | ||
|                 }
 | ||
| #elif UNITY_IOS
 | ||
|                 // iOS 订单验证, 上报打点信息
 | ||
|                 var jsonData = JsonMapper.ToObject(args.purchasedProduct.receipt);
 | ||
|                 string receipt = jsonData["Payload"].ToString();
 | ||
|                 // if (HasReceipt(receipt))
 | ||
|                 // {
 | ||
|                 //     Debug.Log($"[IAP] Receipt has already reported: {receipt}");
 | ||
|                 //     return;
 | ||
|                 // }
 | ||
|                 // AddReceipt(receipt);
 | ||
|                 // new AppleOrderRequest(orderType, args.purchasedProduct.definition.id, receipt,blevel).Send();
 | ||
|                 ReportAppleOrder(orderType, args.purchasedProduct.definition.id, receipt,blevel);
 | ||
|                 Debug.Log($"{Tag} --- Report iOS IAP Order -> orderType:{orderType}  productID:{args.purchasedProduct.definition.id}  blevel:{blevel}");
 | ||
| #endif
 | ||
|             }
 | ||
|             catch (Exception e)
 | ||
|             {
 | ||
|                 LogE($" [IAPManager.RevenueUpload] got Exception: {e.Message}");
 | ||
|                 Crashlytics.LogException(new Exception($"[IAP] Unity report purchase data with b_level={blevel} got error: {e.Message}"));
 | ||
|             }
 | ||
|         }
 | ||
|         
 | ||
|         #endregion
 | ||
|         
 | ||
|         #region IOS Orders Collection
 | ||
|         
 | ||
|         private HashSet<string> iOSReceipts;
 | ||
|         public HashSet<string> IOSReceiptCollection
 | ||
|         {
 | ||
|             get
 | ||
|             {
 | ||
|                 // 读取订单信息
 | ||
|                 if (iOSReceipts == null)
 | ||
|                 {
 | ||
|                     iOSReceipts = new HashSet<string>();
 | ||
|                     string raw = PlayerPrefs.GetString(nameof(IOSReceiptCollection), "");
 | ||
|                     if (!string.IsNullOrEmpty(raw))
 | ||
|                     {
 | ||
|                         var arr = raw.Split(',');
 | ||
|                         for (int i = 0; i < arr.Length; i++)
 | ||
|                         {
 | ||
|                             iOSReceipts.Add(arr[i]);
 | ||
|                         }
 | ||
|                     }
 | ||
|                 }
 | ||
|                 return iOSReceipts;
 | ||
|             }
 | ||
| 
 | ||
|             set
 | ||
|             {
 | ||
|                 // 保存订单信息
 | ||
|                 iOSReceipts = value;
 | ||
|                 PlayerPrefs.SetString(nameof(IOSReceiptCollection), string.Join(",", iOSReceipts));
 | ||
|                 PlayerPrefs.Save();
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 添加订单信息
 | ||
|         /// </summary>
 | ||
|         /// <param name="receipt"></param>
 | ||
|         public void AddReceipt(string receipt)
 | ||
|         {
 | ||
|             if (!HasReceipt(receipt))
 | ||
|             {
 | ||
|                 IOSReceiptCollection.Add(receipt);
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 是否包含订单
 | ||
|         /// </summary>
 | ||
|         /// <param name="receipt"></param>
 | ||
|         /// <returns></returns>
 | ||
|         public bool HasReceipt(string receipt)
 | ||
|         {
 | ||
|             return IOSReceiptCollection.Contains(receipt);
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 用户标志位设置
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 标记是否为付费用户
 | ||
|         /// </summary>
 | ||
|         /// <param name="value"></param>
 | ||
|         public static void SetIsIAPUser(bool value = true)
 | ||
|         {
 | ||
|             Analytics.SetUserProperty(Analytics.PropertyIsIAPUser, value ? "true" : "false");
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 数据初始化
 | ||
| 
 | ||
| 
 | ||
|         private void InitModel()
 | ||
|         {
 | ||
|             _model = IAPModel.Load(); // 初始化 Model
 | ||
|             
 | ||
|             // 启动时查询
 | ||
|             if(_orderRequests == null) 
 | ||
|                 _orderRequests = new Queue<RequestBase>(20);
 | ||
| 
 | ||
| // #if UNITY_EDITOR
 | ||
| //             Debug.Log($"----- IAP Model init -----");         
 | ||
| // #elif UNITY_ANDROID
 | ||
| 
 | ||
| #if UNITY_ANDROID         
 | ||
|             if (_model.HasUnreportedGoogleOrder)
 | ||
|             {
 | ||
|                 foreach (var o in _model.googleOrders)
 | ||
|                 {
 | ||
|                     ReportGoogleOrder(o);
 | ||
|                 }
 | ||
|             }
 | ||
| #elif UNITY_IOS
 | ||
|             if (_model.HasUnreportedAppleOrder)
 | ||
|             {
 | ||
|                 foreach (var o in _model.appleOrders)
 | ||
|                 {
 | ||
|                     ReportAppleOrder(o);
 | ||
|                 }
 | ||
|             }
 | ||
| #endif
 | ||
|             
 | ||
| 
 | ||
|         }
 | ||
|         
 | ||
| 
 | ||
|         #endregion
 | ||
| 
 | ||
|         #region 订单上报队列
 | ||
|         
 | ||
|         private bool isOrderSending = false;
 | ||
|         private Queue<RequestBase> _orderRequests = new Queue<RequestBase>(20);
 | ||
|         
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 上报 Google Order Request
 | ||
|         /// </summary>
 | ||
|         /// <param name="orderType"></param>
 | ||
|         /// <param name="productId"></param>
 | ||
|         /// <param name="subscriptionId"></param>
 | ||
|         /// <param name="token"></param>
 | ||
|         /// <param name="level"></param>
 | ||
|         /// <param name="offerId"></param>
 | ||
|         /// <param name="basePlanId"></param>
 | ||
|         private void ReportGoogleOrder(int orderType, string productId, string subscriptionId, string token, int level,
 | ||
|             string offerId = "", string basePlanId = "")
 | ||
|         {
 | ||
|             var request = GoogleOrderRequest.Build(orderType, productId, subscriptionId, token, level, offerId, basePlanId);
 | ||
|             ReportNextOrder(request);
 | ||
|         }
 | ||
|         private void ReportGoogleOrder(GoogleOrderData data)
 | ||
|         {
 | ||
|             var request = GoogleOrderRequest.Build(data);
 | ||
|             ReportNextOrder(request);
 | ||
|         }
 | ||
|         
 | ||
|         private void ReportAppleOrder(int orderType, string productId, string receipt, int level)
 | ||
|         {
 | ||
|             var request = AppleOrderRequest.Build(orderType, productId, receipt, level);
 | ||
|             ReportNextOrder(request);
 | ||
|         }
 | ||
|         
 | ||
|         private void ReportAppleOrder(AppleOrderData data)
 | ||
|         {
 | ||
|             var request = AppleOrderRequest.Build(data);
 | ||
|             ReportNextOrder(request);
 | ||
|         }
 | ||
|         
 | ||
|         private void ReportNextOrder(RequestBase request)
 | ||
|         {
 | ||
|             _orderRequests.Enqueue(request);
 | ||
|             
 | ||
|             if(isOrderSending) return;
 | ||
|             isOrderSending = true;
 | ||
|             
 | ||
|             OnSendNextOrder();
 | ||
|         }
 | ||
|         
 | ||
|         
 | ||
|         /// <summary>
 | ||
|         /// 上报下一个 Google 订单
 | ||
|         /// </summary>
 | ||
|         private void OnSendNextOrder()
 | ||
|         {
 | ||
|             if (_orderRequests != null && _orderRequests.Count > 0)
 | ||
|             {
 | ||
|                 isOrderSending = true;
 | ||
|                 var request = _orderRequests.Dequeue();
 | ||
|                 GoogleOrderRequest go = request as GoogleOrderRequest;
 | ||
|                 AppleOrderRequest ao = request as AppleOrderRequest;
 | ||
|       
 | ||
|                 if (go != null  && _model.IsTokenExists(go.token))
 | ||
|                 {
 | ||
|                     OnSendNextOrder();
 | ||
|                     return;
 | ||
|                 }
 | ||
|                 
 | ||
|                 if( ao != null && _model.IsReceiptExist(ao.receipt))
 | ||
|                 {
 | ||
|                     OnSendNextOrder();
 | ||
|                     return;
 | ||
|                 }
 | ||
| 
 | ||
|                 request.SetTimeOut(OrderRequestTimeout)
 | ||
|                     .SetRetryTimes(OrderRequestRetryTimes)
 | ||
|                     .SetSuccessCallBack(() =>
 | ||
|                     {
 | ||
|                         if (go != null)
 | ||
|                         {
 | ||
|                             _model.AddToken(go.token);
 | ||
|                             _model.RemoveGoogleOrder(go.orderData);
 | ||
|                         }
 | ||
|                         else if (ao != null)
 | ||
|                         {
 | ||
|                             _model.AddReceipt(ao.receipt);
 | ||
|                             _model.RemoveAppleOrder(ao.orderData);
 | ||
|                         }
 | ||
|                         
 | ||
|                         OnSendNextOrder();
 | ||
|                     })
 | ||
|                     .SetFailCallBack(() =>
 | ||
|                     {
 | ||
|                         if (go != null)
 | ||
|                         {
 | ||
|                             _model.AddGoogleOrder(go.orderData);
 | ||
|                             ReportGoogleOrderLost(go.orderData);
 | ||
|                         }
 | ||
|                         else if (ao != null)
 | ||
|                         {
 | ||
|                             _model.AddAppleOrder(ao.orderData);
 | ||
|                             ReportAppleOrderLost(ao.orderData);
 | ||
|                         }
 | ||
|                         
 | ||
|                         OnSendNextOrder();
 | ||
|                     })
 | ||
|                     .Send();
 | ||
|             }
 | ||
|             else
 | ||
|             {
 | ||
|                 isOrderSending = false;
 | ||
|             }
 | ||
|         }
 | ||
| 
 | ||
|         private void ReportGoogleOrderLost(GoogleOrderData data)
 | ||
|         {
 | ||
|             Analytics.LogEvent("google_order_lost", new Dictionary<string, dynamic>()
 | ||
|             {
 | ||
|                 ["data"] = data.ToString(),
 | ||
|             }, new Analytics.EventSetting()
 | ||
|             {
 | ||
|                 EnableFirebaseAnalytics = true,
 | ||
|                 EnableFacebookAnalytics = true,
 | ||
|             });
 | ||
|         }
 | ||
|         
 | ||
|         private void ReportAppleOrderLost(AppleOrderData data)
 | ||
|         {
 | ||
|             Analytics.LogEvent("apple_order_lost", new Dictionary<string, dynamic>()
 | ||
|             {
 | ||
|                 ["data"] = data.ToString(),
 | ||
|             }, new Analytics.EventSetting()
 | ||
|             {
 | ||
|                 EnableFirebaseAnalytics = true,
 | ||
|                 EnableFacebookAnalytics = true,
 | ||
|             });
 | ||
|         }
 | ||
|         
 | ||
|         #endregion
 | ||
| 
 | ||
| 
 | ||
|     }
 | ||
| 
 | ||
| } |