com.guru.unity.sdk.core/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs

705 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

namespace Guru
{
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
using System.Collections.Generic;
using Firebase.Crashlytics;
using Guru.LitJson;
public abstract class IAPServiceBase<T>: IStoreListener where T: IAPServiceBase<T> , new()
{
#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;
/// <summary>
/// 是否是首次购买
/// </summary>
public int PurchaseCount
{
get => PlayerPrefs.GetInt(nameof(PurchaseCount), 0);
set => PlayerPrefs.SetInt(nameof(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> OnRestored;
public event Action<string> OnBuyStart;
public event Action<string, bool> OnBuyEnd;
public event Action<string, string> OnBuyFailed;
#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;
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;
for (int i = 0; i < len; i++)
{
item = settings[i];
ids = new IDs();
if (!string.IsNullOrEmpty(item.GooglePlayProductId))
{
ids.Add(item.GooglePlayProductId, GooglePlay.Name);
}
if (!string.IsNullOrEmpty(item.AppStoreProductId))
{
ids.Add(item.AppStoreProductId, AppleAppStore.Name);
}
_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].Product = 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;
}
}
#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>
protected virtual void OnRestoreHandle(bool success)
{
LogI($"--- Restore complete: {success}" );
OnRestored?.Invoke(success);
}
/// <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);
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;
}
PurchaseCount++; // 记录支付次数
Debug.Log($"############ ProcessPurchase: PurchaseCount:{PurchaseCount}");
ReportPurchaseResult(purchaseEvent); // 支付结果上报
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());
}
#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}");
Debug.Log($"############ --- result.Count: {result.Length}");
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).Send();
}
}
#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();
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
}
}