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 initCallback; public Action buyCallback; public static IAPManager instance; private IStoreController _storeC; //存储商品信息 private IExtensionProvider _storeE; //IAP扩展工具 private List _infos; //所有产品信息 public static IAPManager Instance { get { if (instance == null) instance = new IAPManager(); return instance; } } // void Awake() { // instance = this; // DontDestroyOnLoad(gameObject); // } #region ================================================= 初始化 ================================================== /// /// 初始化(在Start中调用) /// public void Init(List 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 ================================================ 恢复购买 ================================================== /// /// 恢复购买 /// 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(); 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 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 da = new Dictionary { // { 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 ================================================== 订阅 ================================================== /// /// 解析订阅商品信息 /// /// 需要解析的商品集合 public bool AnalysisSubscriptionProduct(Product[] products) { IAppleExtensions _appleE = _storeE.GetExtension(); 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)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)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)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)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 /// /// 是否已经初始化 /// /// 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 ""; } //IOS专用 传入uuid类型的透传参数 用于校验订单 public void SetApplicationUsername(string applicationUsername) { #if UNITY_IOS if (!IsInitialized()) { IAPDebug("SetApplicationUsername FAIL. Not init."); return; } IAppleExtensions apple = _storeE.GetExtension(); apple.SetApplicationUsername(applicationUsername); #endif } #region ================================================== 原价 ================================================== /// /// 原价 /// /// /// /// 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"; } } /// /// 分割价格(把货币类型和数值分开) /// /// /// 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; } public string GetLocalizedIsoCurrencyCode(string productId) { if (!IsInitialized()) { IAPDebug($"ID:{productId}. Not init."); return "USD"; }; Product product = _storeC.products.WithID (productId); return product.metadata.isoCurrencyCode; } public double GetLocalizedPriceAmount(string productId) { if (!IsInitialized()) { IAPDebug($"ID:{productId}. Not init."); return 0.0; } ; Product product = _storeC.products.WithID(productId); double price = Decimal.ToDouble(product.metadata.localizedPrice); return price; } private void IAPDebug(string mes) { UnityEngine.Debug.Log($"IAPManager {mes}"); } }