705 lines
23 KiB
C#
705 lines
23 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;
|
||
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
|
||
|
||
}
|
||
|
||
} |