2023-06-15 19:13:44 +08:00

385 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using AppsFlyerSDK;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
using UnityEngine.Purchasing.Extension;
public struct ProductInfo {
public string productId;
public ProductType type;
}
public class IAPManager : /* MonoBehaviour, */ IDetailedStoreListener {
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 void OnPurchaseFailed(Product pro, PurchaseFailureDescription p) {
IAPDebug($"ID:{pro.definition.id}. purchase fail: {p.message}");
buyCallback?.Invoke(false, pro, p.message);
}
//购买成功
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}");
}
}