382 lines
15 KiB
C#
382 lines
15 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using AppsFlyerSDK;
|
||
using UnityEngine;
|
||
using UnityEngine.Purchasing;
|
||
using UnityEngine.Purchasing.Security;
|
||
|
||
|
||
public struct ProductInfo {
|
||
public string productId;
|
||
public ProductType type;
|
||
}
|
||
|
||
public class IAPManager : /* MonoBehaviour, */ IStoreListener {
|
||
public Action<bool, Product[], string> initCallback;
|
||
public Action<bool, Product, string> buyCallback;
|
||
public static IAPManager instance;
|
||
|
||
private IStoreController _storeC; //存储商品信息
|
||
private IExtensionProvider _storeE; //IAP扩展工具
|
||
private List<ProductInfo> _infos; //所有产品信息
|
||
|
||
public static IAPManager Instance {
|
||
get {
|
||
if (instance == null)
|
||
instance = new IAPManager();
|
||
|
||
return instance;
|
||
}
|
||
}
|
||
// void Awake() {
|
||
// instance = this;
|
||
// DontDestroyOnLoad(gameObject);
|
||
// }
|
||
|
||
#region ================================================= 初始化 ==================================================
|
||
|
||
/// <summary>
|
||
/// 初始化(在Start中调用)
|
||
/// </summary>
|
||
public void Init(List<ProductInfo> infos) {
|
||
if (IsInitialized()) return;
|
||
|
||
_infos = infos;
|
||
//标准采购模块
|
||
StandardPurchasingModule module = StandardPurchasingModule.Instance();
|
||
//配置模式
|
||
ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
|
||
#if UNITY_ANDROID || UNITY_EDITOR
|
||
string name = GooglePlay.Name;
|
||
#elif UNITY_IOS
|
||
string name = AppleAppStore.Name;
|
||
#else
|
||
string name = GooglePlay.Name;
|
||
#endif
|
||
string id;
|
||
foreach (var item in infos) {
|
||
id = GetProductIdById(item.productId);
|
||
builder.AddProduct(id, item.type, new IDs() { { id, name } });
|
||
}
|
||
UnityPurchasing.Initialize(instance, builder);
|
||
}
|
||
|
||
//初始化成功
|
||
public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {
|
||
_storeC = controller;
|
||
_storeE = extensions;
|
||
ProductCollection products = _storeC.products;
|
||
IAPDebug("init success");
|
||
initCallback?.Invoke(true, products.all, string.Empty);
|
||
}
|
||
|
||
//初始化失败(没有网络的情况下并不会调起,而是一直等到有网络连接再尝试初始化)
|
||
public void OnInitializeFailed(InitializationFailureReason error) {
|
||
string er = error.ToString("G");
|
||
IAPDebug($"init fail: {er}");
|
||
initCallback?.Invoke(false, null, er);
|
||
}
|
||
|
||
public void OnInitializeFailed(InitializationFailureReason error, string message) {
|
||
string er = error.ToString("G");
|
||
IAPDebug($"init fail2: {er}");
|
||
initCallback?.Invoke(false, null, er);
|
||
}
|
||
#endregion
|
||
|
||
#region ================================================== 购买 ==================================================
|
||
|
||
public bool Buy(string productId, string payload) {
|
||
if (!IsInitialized()) {
|
||
IAPDebug($"ID:{productId}. Not init.");
|
||
return false;
|
||
};
|
||
// string productId = GetProductIdById(cfg.productIds);
|
||
Product product = _storeC.products.WithID(productId);
|
||
if (product == null || !product.availableToPurchase) {
|
||
IAPDebug($"ID:{productId}.Not found or is not available for purchase");
|
||
return false;
|
||
}
|
||
|
||
_storeC.InitiatePurchase(productId, payload);
|
||
return true;
|
||
}
|
||
|
||
//购买失败
|
||
public void OnPurchaseFailed(Product pro, PurchaseFailureReason p) {
|
||
string er = p.ToString("G");
|
||
IAPDebug($"ID:{pro.definition.id}. purchase fail: {er}");
|
||
buyCallback?.Invoke(false, pro, er);
|
||
}
|
||
|
||
//购买成功
|
||
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e) {
|
||
#if UNITY_EDITOR
|
||
bool isValid = true;
|
||
#else
|
||
_localChecking (e.purchasedProduct, out bool isValid);
|
||
#endif
|
||
if (!isValid) buyCallback?.Invoke(false, e.purchasedProduct, "本地验证失败");
|
||
else {
|
||
_appsFlyerChecking(e.purchasedProduct);
|
||
IAPDebug($"ID:{e.purchasedProduct.definition.id}. purchase success");
|
||
buyCallback?.Invoke(true, e.purchasedProduct, string.Empty);
|
||
}
|
||
return PurchaseProcessingResult.Pending;
|
||
}
|
||
|
||
// 确认购买
|
||
public void ConsumePurchase(string productId)
|
||
{
|
||
if (!IsInitialized())
|
||
{
|
||
return;
|
||
}
|
||
var product = _storeC.products.WithID(productId);
|
||
if (product == null)
|
||
{
|
||
return;
|
||
}
|
||
_storeC.ConfirmPendingPurchase(product);
|
||
}
|
||
#endregion
|
||
|
||
#region ================================================ 恢复购买 ==================================================
|
||
|
||
/// <summary>
|
||
/// 恢复购买
|
||
/// </summary>
|
||
public void RestorePurchases(Action callBack) {
|
||
if (!IsInitialized()) {
|
||
IAPDebug("RestorePurchases FAIL. Not init.");
|
||
callBack?.Invoke();
|
||
return;
|
||
}
|
||
|
||
if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer) {
|
||
IAPDebug("RestorePurchases started ...");
|
||
IAppleExtensions apple = _storeE.GetExtension<IAppleExtensions>();
|
||
apple.RestoreTransactions((result) => {
|
||
// 返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase);
|
||
IAPDebug($"RestorePurchases result: {result}");
|
||
if (!result) callBack?.Invoke();
|
||
});
|
||
} else {
|
||
IAPDebug($"RestorePurchases FAIL. Not supported on this platform. Current = {Application.platform}");
|
||
callBack?.Invoke();
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region ================================================== 验证 ==================================================
|
||
|
||
//本地验证
|
||
private void _localChecking(Product product, out bool isValid) {
|
||
isValid = true;
|
||
//Unity IAP 的验证逻辑仅包含在这些平台上。
|
||
#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
|
||
// 用我们在 Editor 混淆处理窗口中准备的密钥来,准备验证器
|
||
var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
|
||
AppleTangle.Data(), Application.identifier);
|
||
try {
|
||
//在 Google Play 上,结果中仅有一个商品 ID。
|
||
//在 Apple 商店中,收据包含多个商品。
|
||
IAPDebug($"receipt: {product.receipt}");
|
||
var result = validator.Validate(product.receipt);
|
||
isValid = false;
|
||
foreach (IPurchaseReceipt re in result) {
|
||
IAPDebug($"Local validation:{re.productID}");
|
||
List<ProductInfo> info = _infos.Where(s => GetProductIdById(s.productId).Equals(re.productID)).ToList();
|
||
if (info.Count > 0) isValid = true;
|
||
}
|
||
} catch (IAPSecurityException) {
|
||
isValid = false;
|
||
IAPDebug("Invalid receipt, not unlocking content");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
//AppsFlyer验证
|
||
private void _appsFlyerChecking(Product product) {
|
||
|
||
IAPDebug($"CURRENCY:{product.metadata.isoCurrencyCode} REVENUE:{product.metadata.localizedPrice.ToString()} CONTENT_TYPE:{product.transactionID} CONTENT_ID:{product.definition.id}");
|
||
|
||
// Dictionary<string, string> da = new Dictionary<string, string> {
|
||
// { AFInAppEventParameterName.CURRENCY, product.metadata.isoCurrencyCode },
|
||
// { AFInAppEventParameterName.REVENUE, product.metadata.localizedPrice.ToString() },
|
||
// { AFInAppEventParameterName.QUANTITY, "1" },
|
||
// { AFInAppEventParameterName.CONTENT_TYPE, product.transactionID },
|
||
// { AFInAppEventParameterName.CONTENT_ID, product.definition.id }
|
||
// };
|
||
// #if !ENABLE_GM
|
||
// AppsFlyer.sendEvent(AFInAppEventType.PURCHASE, da);
|
||
// #endif
|
||
}
|
||
#endregion
|
||
|
||
|
||
#region ================================================== 订阅 ==================================================
|
||
|
||
/// <summary>
|
||
/// 解析订阅商品信息
|
||
/// </summary>
|
||
/// <param name="products">需要解析的商品集合</param>
|
||
public bool AnalysisSubscriptionProduct(Product[] products) {
|
||
IAppleExtensions _appleE = _storeE.GetExtension<IAppleExtensions>();
|
||
var introductory_info_dict = _appleE.GetIntroductoryPriceDictionary();
|
||
|
||
int length = products.Length;
|
||
string intro_json;
|
||
SubscriptionInfo info;
|
||
Product pro;
|
||
bool isSubscribed = false;
|
||
for (var i = 0; i < length; i++) {
|
||
pro = products[i];
|
||
//购买状态
|
||
if (!pro.availableToPurchase) { IAPDebug($"ID:{pro.definition.id}. not available for purchase"); continue; }
|
||
//有收据
|
||
if (!pro.hasReceipt) { IAPDebug($"ID:{pro.definition.id}. no receipt"); continue; }
|
||
//订阅类型
|
||
if (pro.definition.type != ProductType.Subscription) { IAPDebug($"ID:{pro.definition.id}. no Subscription Product"); continue; }
|
||
|
||
if (_checkIfProductIsAvailableForSubscriptionManager(pro.receipt)) {
|
||
intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(products[i].definition.storeSpecificId)) ? null : introductory_info_dict[products[i].definition.storeSpecificId];
|
||
info = new SubscriptionManager(products[i], intro_json).getSubscriptionInfo();
|
||
if (!isSubscribed) isSubscribed = info.isSubscribed() == Result.True;
|
||
IAPDebug("product id is: " + info.getProductId());
|
||
IAPDebug("purchase date is: " + info.getPurchaseDate());
|
||
IAPDebug("is subscribed? " + info.isSubscribed().ToString());
|
||
IAPDebug("is expired? " + info.isExpired().ToString());
|
||
IAPDebug("is cancelled? " + info.isCancelled());
|
||
IAPDebug("product is in free trial peroid? " + info.isFreeTrial());
|
||
IAPDebug("product is auto renewing? " + info.isAutoRenewing());
|
||
IAPDebug("subscription remaining valid time until next billing date is: " + info.getRemainingTime());
|
||
IAPDebug("is this product in introductory price period? " + info.isIntroductoryPricePeriod());
|
||
IAPDebug("the product introductory price period is: " + info.getIntroductoryPricePeriod());
|
||
IAPDebug("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles());
|
||
IAPDebug("the product introductory localized price is: " + info.getIntroductoryPrice());
|
||
IAPDebug("subscription next billing date is: " + info.getExpireDate());
|
||
}
|
||
}
|
||
return isSubscribed;
|
||
}
|
||
|
||
//检查产品是否可用于SubscriptionManager(此类支持 Apple 商店和 Google Play 应用商店。对于 Google Play,此类仅支持使用 IAP SDK 1.19+ 购买的商品。)
|
||
private bool _checkIfProductIsAvailableForSubscriptionManager(string receipt) {
|
||
var receipt_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(receipt);
|
||
if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload")) {
|
||
IAPDebug("The product receipt does not contain enough information");
|
||
return false;
|
||
}
|
||
|
||
var store = (string)receipt_wrapper["Store"];
|
||
var payload = (string)receipt_wrapper["Payload"];
|
||
|
||
if (payload == null) return false;
|
||
if (store == GooglePlay.Name) {
|
||
var payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
|
||
if (!payload_wrapper.ContainsKey("json")) {
|
||
IAPDebug("The product receipt does not contain enough information, the 'json' field is missing");
|
||
return false;
|
||
}
|
||
var original_json_payload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode((string)payload_wrapper["json"]);
|
||
if (original_json_payload_wrapper == null || !original_json_payload_wrapper.ContainsKey("developerPayload")) {
|
||
IAPDebug("The product receipt does not contain enough information, the 'developerPayload' field is missing");
|
||
return false;
|
||
}
|
||
var developerPayloadJSON = (string)original_json_payload_wrapper["developerPayload"];
|
||
var developerPayload_wrapper = (Dictionary<string, object>)MiniJson.JsonDecode(developerPayloadJSON);
|
||
if (developerPayload_wrapper == null || !developerPayload_wrapper.ContainsKey("is_free_trial") || !developerPayload_wrapper.ContainsKey("has_introductory_price_trial")) {
|
||
IAPDebug("The product receipt does not contain enough information, the product is not purchased using 1.19 or later");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
if (store == AppleAppStore.Name || store == AmazonApps.Name || store == MacAppStore.Name) return true;
|
||
return false;
|
||
}
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 是否已经初始化
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public bool IsInitialized() {
|
||
return _storeC != null;
|
||
}
|
||
|
||
//产品Id TODO...
|
||
public static string GetProductIdById(string id) {
|
||
return id;
|
||
// string[] ids = id.Split('|');
|
||
// #if UNITY_ANDROID
|
||
// return ids[0];
|
||
// #elif UNITY_IOS || UNITY_EDITOR
|
||
// return ids[1];
|
||
// #endif
|
||
// return "";
|
||
}
|
||
|
||
#region ================================================== 原价 ==================================================
|
||
|
||
/// <summary>
|
||
/// 原价
|
||
/// </summary>
|
||
/// <param name="str"></param>
|
||
/// <param name="sale"></param>
|
||
/// <returns></returns>
|
||
public string MoneyStrSale(string str, float sale) {
|
||
var arr = MoneySplit(str);
|
||
var symbol = arr[0];
|
||
var value = arr[1];
|
||
|
||
IAPDebug("MoneyStrSale symbol:" + symbol + ",value:" + value + ",sale:" + sale);
|
||
try {
|
||
float result = float.Parse(value) * sale;
|
||
var rs = symbol + String.Format("{0:F2}", result);
|
||
IAPDebug("MoneyStrSale rs:" + rs);
|
||
return rs;
|
||
} catch (Exception e) {
|
||
IAPDebug("MoneyStrSale Exception:" + e.Message);
|
||
return symbol + "0";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分割价格(把货币类型和数值分开)
|
||
/// </summary>
|
||
/// <param name="str"></param>
|
||
/// <returns></returns>
|
||
public string[] MoneySplit(string str) {
|
||
IAPDebug("MoneyStrSale str.Length:" + str.Length);
|
||
var segIndex = 0;
|
||
for (int i = 0; i < str.Length; i++) {
|
||
var c = str[i];
|
||
var ic = Convert.ToInt32(c);
|
||
if (ic >= 48 && ic <= 57) {
|
||
segIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
var symbol = str.Substring(0, segIndex);
|
||
var value = str.Substring(segIndex, str.Length - segIndex);
|
||
return new string[] { symbol, value };
|
||
}
|
||
#endregion
|
||
|
||
public string GetLocalizedPrice(string productId)
|
||
{
|
||
if (!IsInitialized()) {
|
||
IAPDebug($"ID:{productId}. Not init.");
|
||
return "";
|
||
};
|
||
Product product = _storeC.products.WithID (productId);
|
||
return product.metadata.localizedPriceString;
|
||
}
|
||
|
||
private void IAPDebug(string mes) {
|
||
UnityEngine.Debug.Log($"IAPManager {mes}");
|
||
}
|
||
} |