1377 lines
46 KiB
C#
1377 lines
46 KiB
C#
|
||
namespace Guru
|
||
{
|
||
using System;
|
||
using System.Linq;
|
||
using UnityEngine;
|
||
using UnityEngine.Purchasing;
|
||
using UnityEngine.Purchasing.Security;
|
||
using System.Collections.Generic;
|
||
using Newtonsoft.Json;
|
||
using Newtonsoft.Json.Linq;
|
||
|
||
public abstract class IAPServiceBase<T>: IStoreListener where T: IAPServiceBase<T> , new()
|
||
{
|
||
private const int OrderRequestTimeout = 10;
|
||
private const 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 && _model != null;
|
||
|
||
private ProductInfo _curProductInfo = null;
|
||
private string _curProductCategory = "";
|
||
|
||
public string CurrentBuyingProductId
|
||
{
|
||
get
|
||
{
|
||
if (_curProductInfo != null)
|
||
{
|
||
return _curProductInfo.Id;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
}
|
||
|
||
protected 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;
|
||
private string _uid;
|
||
private string _uuid;
|
||
/// <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(string uid, bool showLog = false)
|
||
{
|
||
|
||
if (string.IsNullOrEmpty(uid)) uid = IPMConfig.IPM_UID;
|
||
_uid = uid;
|
||
_showLog = showLog;
|
||
InitPurchasing();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 带有校验器的初始化
|
||
/// </summary>
|
||
/// <param name="uuid"></param>
|
||
/// <param name="googlePublicKey"></param>
|
||
/// <param name="appleRootCert"></param>
|
||
/// <param name="showLog"></param>
|
||
public virtual void InitWithKeys(string uid, byte[] googlePublicKey, byte[] appleRootCert, bool showLog = false)
|
||
{
|
||
_googlePublicKey = googlePublicKey;
|
||
_appleRootCert = appleRootCert;
|
||
InitModel();
|
||
Initialize(uid, showLog);
|
||
}
|
||
|
||
|
||
public void SetUID(string uid)
|
||
{
|
||
if (_configBuilder != null && !string.IsNullOrEmpty(uid))
|
||
{
|
||
_uid = uid;
|
||
_configBuilder.Configure<IGooglePlayConfiguration>().SetObfuscatedAccountId(uid);
|
||
Debug.Log($"[IAP] --- Set UID: {uid}");
|
||
}
|
||
}
|
||
|
||
|
||
public void SetUUID(string uuid)
|
||
{
|
||
if (_appleExtensions != null && !string.IsNullOrEmpty(uuid))
|
||
{
|
||
_uuid = uuid;
|
||
_appleExtensions.SetApplicationUsername(uuid);
|
||
Debug.Log($"[IAP] --- Set UUID: {uuid}");
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/// <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(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)
|
||
{
|
||
_storeController = controller;
|
||
_storeExtensionProvider = extensions;
|
||
var uuid = IPMConfig.IPM_UUID;
|
||
if(string.IsNullOrEmpty(_uid)) _uid = IPMConfig.IPM_UID;
|
||
|
||
if (!string.IsNullOrEmpty(_uid) && string.IsNullOrEmpty(uuid))
|
||
{
|
||
uuid = IDHelper.GenUUID(_uid);
|
||
}
|
||
LogI($"--- IAP Initialized Success. With UID: {_uid} UUID: {uuid} DeviceId: {IPMConfig.IPM_DEVICE_ID}");
|
||
|
||
#if UNITY_IOS
|
||
_appleExtensions = extensions.GetExtension<IAppleExtensions>();
|
||
_appleExtensions.SetApplicationUsername(uuid); // SetUp UUID (8)-(4)-(4)-(12): xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||
// 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);
|
||
});
|
||
|
||
var appReceipt = _configBuilder.Configure<IAppleConfiguration>().appReceipt;
|
||
if (!string.IsNullOrEmpty(appReceipt))
|
||
{
|
||
LogI($"[IAP] --- AppReceipt: {appReceipt}");
|
||
}
|
||
|
||
#elif UNITY_ANDROID
|
||
_configBuilder.Configure<IGooglePlayConfiguration>().SetObfuscatedAccountId(_uid); // SetUp UID
|
||
_googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
|
||
//添加安装游戏后第一次初试化进行恢复购买的回调 只有安卓才有
|
||
_googlePlayStoreExtensions.RestoreTransactions(OnRestoreHandle);
|
||
#endif
|
||
|
||
foreach (var product in _storeController.products.all)
|
||
{
|
||
if (!product.availableToPurchase)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (_products.ContainsKey(product.definition.id))
|
||
{
|
||
_products[product.definition.id].SetProduct(product);
|
||
}
|
||
}
|
||
|
||
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
|
||
{
|
||
Analytics.LogCrashlytics(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;
|
||
}
|
||
}
|
||
if(isIAPUser) SetIsIAPUser(true);
|
||
}
|
||
|
||
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, string category = "")
|
||
{
|
||
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 (string.IsNullOrEmpty(category)) category = info.Category;
|
||
_storeController.InitiatePurchase(product);
|
||
_curProductInfo = info;
|
||
_curProductCategory = category;
|
||
|
||
// Analytics.IAPClick(_curProductCategory, product.definition.id);
|
||
// Analytics.IAPImp(_curProductCategory, product.definition.id); // <--- Client should report this Event
|
||
|
||
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;
|
||
string productName = "";
|
||
if (null != info)
|
||
{
|
||
success = true;
|
||
productName = info.Name;
|
||
SetIsIAPUser(true); // 设置用户属性标记
|
||
|
||
LogI($"{Tag} --- OnPurchaseSuccess :: purchase count: {PurchaseCount} productName: {productName}");
|
||
|
||
// 只有实际发生购买后才会有订单上报. 启动时的 Restore 操作自动调用支付成功. 这里做一个判定, 过滤掉订单的物品
|
||
if (_curProductInfo != null)
|
||
{
|
||
ReportPurchaseResult(purchaseEvent); // 订单上报
|
||
|
||
// 真实购买后上报对应的事件
|
||
if (IsFirstIAP) {
|
||
// 上报首次支付打点
|
||
Analytics.FirstIAP(info.Id, info.Price, info.CurrencyCode);
|
||
}
|
||
Analytics.ProductIAP(info.Id,info.Id, info.Price, info.CurrencyCode);
|
||
}
|
||
|
||
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);
|
||
Analytics.LogCrashlytics(new Exception(msg));
|
||
}
|
||
else
|
||
{
|
||
OnGetProductReceipt?.Invoke(pp.definition.id, pp.receipt, pp.appleProductIsRestored);
|
||
}
|
||
|
||
PurchaseCount++; // 记录支付次数
|
||
}
|
||
else
|
||
{
|
||
string msg = $"{Tag} --- Purchased end, but can't find ProductInfo with ID: {productId}";
|
||
Debug.LogError(msg);
|
||
Analytics.LogCrashlytics(new Exception(msg));
|
||
}
|
||
|
||
LogI($"{Tag} --- Call OnBuyEnd [{productName}] :: {OnBuyEnd}");
|
||
OnBuyEnd?.Invoke(productName, success);
|
||
OnPurchaseOver(success, productName); // 支付成功处理逻辑
|
||
ClearCurPurchasingProduct(); // 清除购买缓存
|
||
|
||
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);
|
||
|
||
//上报点位,用户购买失败的原因
|
||
Analytics.IAPRetFalse(_curProductCategory, 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());
|
||
ClearCurPurchasingProduct(); // 清除购买缓存
|
||
}
|
||
|
||
private void ClearCurPurchasingProduct()
|
||
{
|
||
_curProductInfo = null;
|
||
_curProductCategory = "";
|
||
}
|
||
|
||
|
||
/// <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 bool ReportPurchaseResult(PurchaseEventArgs args)
|
||
{
|
||
// 验证器判定
|
||
if (_validator == null)
|
||
{
|
||
// Debug.Log($"############ --- Validator is null");
|
||
LogE($"{Tag} --- Validator is null. Report Order failed.");
|
||
Analytics.LogCrashlytics(new Exception($"IAPService can not report order because Validator is null!"));
|
||
return false;
|
||
}
|
||
|
||
//---------------- All Report Information --------------------
|
||
int level = GetBLevel();
|
||
int orderType = args.purchasedProduct.definition.type == ProductType.Subscription ? 1 : 0;
|
||
string productId = args.purchasedProduct.definition.id;
|
||
// string appleReceiptString = "";
|
||
string userCurrency = args.purchasedProduct.metadata.isoCurrencyCode;
|
||
double payPrice = decimal.ToDouble(args.purchasedProduct.metadata.localizedPrice);
|
||
string scene = _curProductCategory ?? "Store";
|
||
bool isFree = false;
|
||
if (orderType == 1) isFree = IsSubscriptionFreeTrailById(productId);
|
||
string appleReceiptString = "";
|
||
//---------------- All Report Information --------------------
|
||
|
||
LogI($"--- Report b_level:[{level}] with product id:{args.purchasedProduct.definition.id} ");
|
||
|
||
#if UNITY_EDITOR
|
||
// // Editor 不做上报逻辑
|
||
LogI($"--- Editor Validate Success. But Editor can't report order.");
|
||
return true;
|
||
#endif
|
||
IPurchaseReceipt[] allReceipts = null;
|
||
|
||
try
|
||
{
|
||
|
||
#if UNITY_IOS
|
||
// ---- iOS 订单验证, 上报打点信息 ----
|
||
var jsonData = JsonConvert.DeserializeObject<JObject>(args.purchasedProduct.receipt);
|
||
if (jsonData != null && jsonData.TryGetValue("Payload", out var recp))
|
||
{
|
||
appleReceiptString = recp.ToString();
|
||
LogI($"--- [{productId}] iOS receipt: {appleReceiptString}");
|
||
}
|
||
|
||
Debug.Log($"[IAP] --- Full receipt: \n{args.purchasedProduct.receipt}");
|
||
#endif
|
||
|
||
allReceipts = _validator.Validate(args.purchasedProduct.receipt);
|
||
|
||
// ---- Android 订单验证, 上报打点信息 ----
|
||
string offerId = "";
|
||
string basePlanId = "";
|
||
|
||
foreach (var receipt in allReceipts)
|
||
{
|
||
if (receipt == null) continue;
|
||
|
||
if (receipt is GooglePlayReceipt googleReceipt)
|
||
{
|
||
ReportGoogleOrder(orderType,
|
||
googleReceipt.productID,
|
||
googleReceipt.purchaseToken,
|
||
googleReceipt.orderID,
|
||
googleReceipt.purchaseDate,
|
||
level,
|
||
userCurrency, payPrice, scene, isFree,
|
||
offerId, basePlanId);
|
||
}
|
||
else if (receipt is AppleInAppPurchaseReceipt appleReceipt)
|
||
{
|
||
ReportAppleOrder(orderType,
|
||
appleReceipt.productID,
|
||
appleReceiptString,
|
||
appleReceipt.transactionID,
|
||
appleReceipt.purchaseDate,
|
||
level,
|
||
userCurrency, payPrice, scene, isFree);
|
||
}
|
||
else
|
||
{
|
||
string exmsg = $"--- [{productId}] :: Unknown Receipt Type: {receipt.GetType()} can't report order.";
|
||
Analytics.LogCrashlytics(exmsg);
|
||
LogE(exmsg);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogE($" [IAPManager.RevenueUpload] got Exception: {ex.Message}");
|
||
Analytics.LogCrashlytics(new Exception($"[IAP] Unity report purchase data with b_level={level} got error: {ex.Message}"));
|
||
return false;
|
||
}
|
||
|
||
LogI("--- All Receipt is valid ---");
|
||
return true;
|
||
}
|
||
|
||
#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>
|
||
private static void SetIsIAPUser(bool value = true)
|
||
{
|
||
if (Instance != null && Instance._model != null && value)
|
||
{
|
||
Instance._model.SetIsIapUser(true); // 用户属性
|
||
}
|
||
Analytics.SetIsIapUser(value);
|
||
}
|
||
|
||
|
||
#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)
|
||
{
|
||
int i = 0;
|
||
while (_model.googleOrders.Count > 0
|
||
&& i < _model.googleOrders.Count)
|
||
{
|
||
var o = _model.googleOrders[i];
|
||
ReportGoogleOrder(o);
|
||
i++;
|
||
}
|
||
}
|
||
#elif UNITY_IOS
|
||
if (_model.HasUnreportedAppleOrder)
|
||
{
|
||
int i = 0;
|
||
while (_model.appleOrders.Count > 0
|
||
&& i < _model.appleOrders.Count)
|
||
{
|
||
var o = _model.appleOrders[i];
|
||
ReportAppleOrder(o);
|
||
i++;
|
||
}
|
||
}
|
||
#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="date"></param>
|
||
/// <param name="level"></param>
|
||
/// <param name="offerId"></param>
|
||
/// <param name="basePlanId"></param>
|
||
/// <param name="orderId"></param>
|
||
private void ReportGoogleOrder(int orderType, string productId, string token,
|
||
string orderId, DateTime date, int level,
|
||
string userCurrency, double payPrice, string scene, bool isFree = false,
|
||
string offerId = "", string basePlanId = "")
|
||
{
|
||
var payedDate = TimeUtil.GetTimeStampString(date);
|
||
var request = GoogleOrderRequest.Build(orderType, productId, token, orderId, payedDate, level,
|
||
userCurrency, payPrice, scene, isFree, offerId, basePlanId);
|
||
ReportNextOrder(request);
|
||
}
|
||
private void ReportGoogleOrder(GoogleOrderData data)
|
||
{
|
||
var request = GoogleOrderRequest.Build(data);
|
||
ReportNextOrder(request);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 上报 Apple Order Request
|
||
/// </summary>
|
||
/// <param name="orderType"></param>
|
||
/// <param name="productId"></param>
|
||
/// <param name="receipt"></param>
|
||
/// <param name="orderId"></param>
|
||
/// <param name="date"></param>
|
||
/// <param name="level"></param>
|
||
/// <param name="userCurrency"></param>
|
||
/// <param name="payPrice"></param>
|
||
/// <param name="scene"></param>
|
||
/// <param name="isFree"></param>
|
||
/// <param name="offerId"></param>
|
||
/// <param name="basePlanId"></param>
|
||
private void ReportAppleOrder(int orderType, string productId, string receipt,
|
||
string orderId, DateTime date,int level, string userCurrency, double payPrice, string scene, bool isFree = false,
|
||
string offerId = "", string basePlanId = "")
|
||
{
|
||
var payedDate = TimeUtil.GetTimeStampString(date);
|
||
var request = AppleOrderRequest.Build(orderType, productId, receipt, orderId, payedDate, level,
|
||
userCurrency, payPrice, scene, isFree, offerId, basePlanId);
|
||
ReportNextOrder(request);
|
||
}
|
||
|
||
private void ReportAppleOrder(AppleOrderData data)
|
||
{
|
||
var request = AppleOrderRequest.Build(data);
|
||
ReportNextOrder(request);
|
||
}
|
||
|
||
private void ReportNextOrder(RequestBase request)
|
||
{
|
||
if(_orderRequests == null) _orderRequests = new Queue<RequestBase>(20);
|
||
_orderRequests.Enqueue(request);
|
||
|
||
if(isOrderSending) return;
|
||
isOrderSending = true;
|
||
|
||
OnSendNextOrder();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 上报下一个订单数据
|
||
/// </summary>
|
||
private void OnSendNextOrder()
|
||
{
|
||
if (_orderRequests != null && _orderRequests.Count > 0)
|
||
{
|
||
// 如果上报队列不为空, 则尝试上报
|
||
isOrderSending = true;
|
||
var request = _orderRequests.Dequeue();
|
||
if (request == null)
|
||
{
|
||
// 跳过空请求
|
||
OnSendNextOrder();
|
||
return;
|
||
}
|
||
|
||
GoogleOrderRequest googleReq = request as GoogleOrderRequest;
|
||
AppleOrderRequest appReq = request as AppleOrderRequest;
|
||
|
||
if (googleReq != null)
|
||
{
|
||
if (_model.IsTokenExists(googleReq.token))
|
||
{
|
||
OnSendNextOrder(); // 跳过上报过的 Google 订单
|
||
return;
|
||
}
|
||
_model.AddGoogleOrder(googleReq.orderData); // 缓存当前 orderData 等待上报后再消除
|
||
}
|
||
else if( appReq != null)
|
||
{
|
||
if (_model.IsReceiptExist(appReq.receipt))
|
||
{
|
||
OnSendNextOrder(); // 跳过上报过的 Apple 订单
|
||
return;
|
||
}
|
||
_model.AddAppleOrder(appReq.orderData); // 缓存当前 orderData 等待上报后再消除
|
||
}
|
||
|
||
request.SetTimeOut(OrderRequestTimeout)
|
||
.SetRetryTimes(OrderRequestRetryTimes)
|
||
.SetSuccessCallBack(() =>
|
||
{
|
||
//---------------- Success ------------------------
|
||
if (googleReq != null)
|
||
{
|
||
_model.AddToken(googleReq.token); // 记录当前的 Google 订单
|
||
_model.RemoveGoogleOrder(googleReq.orderData); // 成功后清除缓存 orderData
|
||
}
|
||
else if (appReq != null)
|
||
{
|
||
_model.AddReceipt(appReq.receipt); // 记录当前的 Apple 订单
|
||
_model.RemoveAppleOrder(appReq.orderData); // 成功后清除缓存 orderData
|
||
}
|
||
OnSendNextOrder(); // NEXT Order
|
||
})
|
||
.SetFailCallBack(() =>
|
||
{
|
||
//---------------- Fail ------------------------
|
||
if (googleReq != null)
|
||
{
|
||
ReportGoogleOrderLost(googleReq.orderData); // 上报 Google 订单缺失打点
|
||
}
|
||
else if (appReq != null)
|
||
{
|
||
ReportAppleOrderLost(appReq.orderData); // 上报 Apple 订单缺失打点
|
||
}
|
||
OnSendNextOrder(); // NEXT Order
|
||
})
|
||
.Send();
|
||
}
|
||
else
|
||
{
|
||
isOrderSending = false;
|
||
}
|
||
}
|
||
|
||
private void ReportGoogleOrderLost(GoogleOrderData data)
|
||
{
|
||
Analytics.TrackEvent("google_order_lost", new Dictionary<string, dynamic>()
|
||
{
|
||
["data"] = data.ToString(),
|
||
}, new Analytics.EventSetting()
|
||
{
|
||
EnableFirebaseAnalytics = true,
|
||
EnableFacebookAnalytics = true,
|
||
});
|
||
}
|
||
|
||
private void ReportAppleOrderLost(AppleOrderData data)
|
||
{
|
||
Analytics.TrackEvent("apple_order_lost", new Dictionary<string, dynamic>()
|
||
{
|
||
["data"] = data.ToString(),
|
||
}, new Analytics.EventSetting()
|
||
{
|
||
EnableFirebaseAnalytics = true,
|
||
EnableFacebookAnalytics = true,
|
||
});
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Subscription
|
||
|
||
|
||
public static DateTime DefaultSubscriptionDate = new DateTime(1970, 1,1,0,0,0);
|
||
|
||
|
||
private SubscriptionManager GetSubManager(string productName)
|
||
{
|
||
var product = GetProduct(productName);
|
||
if (product != null && product.definition.type == ProductType.Subscription)
|
||
{
|
||
return new SubscriptionManager(product, null);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private SubscriptionManager GetSubManagerById(string productId)
|
||
{
|
||
var product = _storeController.products.WithID(productId);
|
||
if (product != null && product.definition.type == ProductType.Subscription)
|
||
{
|
||
return new SubscriptionManager(product, null);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
|
||
public bool IsSubscriptionFreeTrail(string productName)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isFreeTrial() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public bool IsSubscriptionFreeTrailById(string productId)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManagerById(productId);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isFreeTrial() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
public bool IsSubscriptionCancelled(string productName)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isCancelled() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public bool IsSubscriptionAvailable(string productName)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isSubscribed() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
public bool IsSubscriptionExpired(string productName)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isExpired() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
public bool IsSubscriptionAutoRenewing(string productName)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isAutoRenewing() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// IntroductioryPrice Period
|
||
/// </summary>
|
||
/// <param name="productName"></param>
|
||
/// <returns></returns>
|
||
public bool IsSubscriptionIntroductoryPricePeriod(string productName)
|
||
{
|
||
if(!IsInitialized) return false;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().isIntroductoryPricePeriod() == Result.True;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
public DateTime GetSubscriptionExpireDate(string productName)
|
||
{
|
||
if(!IsInitialized) return DefaultSubscriptionDate;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo()?.getExpireDate() ?? DateTime.Now;
|
||
}
|
||
return DefaultSubscriptionDate;
|
||
}
|
||
|
||
|
||
public DateTime GetSubscriptionPurchaseDate(string productName)
|
||
{
|
||
if(!IsInitialized) return DefaultSubscriptionDate;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().getPurchaseDate();
|
||
}
|
||
return DefaultSubscriptionDate;
|
||
}
|
||
|
||
|
||
public DateTime GetSubscriptionCancelDate(string productName)
|
||
{
|
||
if(!IsInitialized) return DefaultSubscriptionDate;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().getCancelDate();
|
||
}
|
||
return DefaultSubscriptionDate;
|
||
}
|
||
|
||
|
||
public TimeSpan GetSubscriptionRemainingTime(string productName)
|
||
{
|
||
if(!IsInitialized) return TimeSpan.Zero;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().getRemainingTime();
|
||
}
|
||
return TimeSpan.Zero;
|
||
}
|
||
|
||
public TimeSpan GetSubscriptionIntroductoryPricePeriod(string productName)
|
||
{
|
||
if(!IsInitialized) return TimeSpan.Zero;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().getIntroductoryPricePeriod();
|
||
}
|
||
return TimeSpan.Zero;
|
||
}
|
||
|
||
|
||
public TimeSpan GetSubscriptionFreeTrialPeriod(string productName)
|
||
{
|
||
if(!IsInitialized) return TimeSpan.Zero;
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().getFreeTrialPeriod();
|
||
}
|
||
return TimeSpan.Zero;
|
||
}
|
||
|
||
public string GetSubscriptionInfoJsonString(string productName)
|
||
{
|
||
if(!IsInitialized) return "";
|
||
|
||
var smgr = GetSubManager(productName);
|
||
if (smgr != null)
|
||
{
|
||
return smgr.getSubscriptionInfo().getSubscriptionInfoJsonString();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
#endregion
|
||
|
||
}
|
||
|
||
} |