#if USE_IAP using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Purchasing; using Touka; using UnityEngine.Purchasing.Extension; using Unity.Services.Core; using Unity.Services.Core.Environments; namespace Touka { public class IAPTool : NormalSingleton, IDetailedStoreListener { /// /// 获取到在AppStore和Google Play 上配置的商品时回调 /// 游戏可根据返回的商品列表进行商品UI的展示或隐藏 /// public event Action OnGetProductsInfo; /// /// 拉起支付窗口,开始支付流程时回调 /// public event Action OnPurchaseBegin; private static string mProductName = ""; private static string mGameExtraParam = ""; /// /// 恢复购买回调 /// public event Action OnRestoreDone; public bool IsRestoring => mIsRestoring; private bool mIsRestoring = false; private static IStoreController m_StoreController; // 存储商品信息; private static IExtensionProvider m_StoreExtensionProvider; // IAP扩展工具; private bool m_PurchaseInProgress = false; // 是否处于付费中; private Dictionary mInitProductDic; private SubscriptionInfo mSubsInfo = null; private const string Environment = "production"; private bool mServiceInit = false; private bool IsFetchingAdditionalProducts = false; private Dictionary mAddProductsDic; /// /// 初始化UnityServices /// public void PreInitialize() { TKGDebugger.LogDebug("PreInitialize() mServiceInit: " + mServiceInit); if (!mServiceInit) { InitializeUnityServices(OnSuccess, OnError); } } /// /// 初始化IAP /// public void Initialize() { TKGDebugger.LogDebug("IAP Initialize() m_StoreController:" + m_StoreController + " m_StoreExtensionProvider: " + m_StoreExtensionProvider); if (m_StoreController == null && m_StoreExtensionProvider == null) InitUnityPurchase(); } private bool IsInitialized() { return m_StoreController != null && m_StoreExtensionProvider != null; } /// /// IAP初始化后,可使用此接口,添加新增商品id /// /// 商品IDs /// bool:添加新增商品id结果 string:附加信息 [Obsolete( "AddProducts(Dictionary products, Action onProductsResult = null) is deprecated, please use TKGSDKManager.Instance.BuyProductByID(string productId) instead.")] public void AddProducts(Dictionary products, Action onProductsResult = null) { mInitProductDic = products; FetchAdditionalProducts(products, onProductsResult); } /// /// 初始化UnityServices /// /// /// private void InitializeUnityServices(Action onSuccess, Action onError) { try { var options = new InitializationOptions().SetEnvironmentName(Environment); UnityServices.InitializeAsync(options).ContinueWith(task => onSuccess()); } catch (Exception exception) { onError(exception.Message); } } void OnSuccess() { var text = "Congratulations!\nUnity Gaming Services has been successfully initialized."; TKGDebugger.LogDebug(text); mServiceInit = true; } void OnError(string message) { var text = $"Unity Gaming Services failed to initialize with error: {message}."; TKGDebugger.LogError(text); } // 初始化IAP; private void InitUnityPurchase() { TKGDebugger.LogDebug("IAP InitUnityPurchase() IsInitialized: " + IsInitialized()); if (IsInitialized()) return; // 标准采购模块; StandardPurchasingModule module = StandardPurchasingModule.Instance(); // 配置模式; ConfigurationBuilder builder = ConfigurationBuilder.Instance(module); // 注意ProductType的类型,Consumable是可以无限购买(比如水晶),NonConsumable是只能购买一次(比如关卡),Subscription是每月订阅(比如VIP); // 这里初始化没有添加平台信息,因为平台信息有的时候还存在bug,如果必须添加,也可以添加,没有问题,确保平台信息添加正确就行了。 int productsNum = 0; foreach (string tID in IAPProducts.ProductDic.Keys) { productsNum++; if (!string.IsNullOrEmpty(tID)) { TKGDebugger.LogDebug($"Add IAPProducts IAPProducts: {tID}"); builder.AddProduct(tID, IAPProducts.ProductDic[tID]); } } if (mInitProductDic != null && mInitProductDic.Count > 0) { foreach (string tID in mInitProductDic.Keys) { productsNum++; if (!string.IsNullOrEmpty(tID)) { TKGDebugger.LogDebug($"Add InitProductDic APProducts: {tID}"); builder.AddProduct(tID, mInitProductDic[tID]); } } } if (mAddProductsDic != null && mAddProductsDic.Count > 0) { foreach (string tID in mAddProductsDic.Keys) { productsNum++; if (!string.IsNullOrEmpty(tID)) { TKGDebugger.LogDebug($"Add AddProductsDic IAPProducts: {tID}"); builder.AddProduct(tID, mAddProductsDic[tID]); } } } if (productsNum > 0) { //初始化; UnityPurchasing.Initialize(this, builder); } else { TKGDebugger.LogDebug( "UnityPurchasing will not initialize.Products is empty,please invoke TKGSDKManager.Instance.BuyProductByID(string productId) add product."); } } /// /// 批量获取其他产品 /// private void FetchAdditionalProducts(Dictionary ProductDic, Action onProductsResult = null) { if (!IsInitialized()) { mAddProductsDic = ProductDic; TKGDebugger.LogDebug("IAP not init.Now InitUnityPurchase"); InitUnityPurchase(); return; } if (IsFetchingAdditionalProducts) { TKGDebugger.LogDebug("Now fetching additional products,don't call repeatedly"); if (onProductsResult != null) { onProductsResult(false, "Now fetching additional products,don't call repeatedly"); } return; } IsFetchingAdditionalProducts = true; if (ProductDic != null) { var additional = new HashSet(); foreach (string tID in ProductDic.Keys) { additional.Add(new ProductDefinition(tID, ProductDic[tID])); } Action onSuccess = () => { IsFetchingAdditionalProducts = false; TKGDebugger.LogDebug("Fetched successfully!"); if (OnGetProductsInfo != null) { OnGetProductsInfo(m_StoreController.products.all); } foreach (var product in m_StoreController.products.all) { TKGDebugger.LogDebug(product.metadata.localizedTitle + "|" + product.metadata.localizedPriceString + "|" + product.metadata.localizedDescription + "|" + product.metadata.isoCurrencyCode); } if (onProductsResult != null) { onProductsResult(true, "Fetched successfully!"); } }; Action onFailure = (InitializationFailureReason i, string msg) => { IsFetchingAdditionalProducts = false; if (onProductsResult != null) { onProductsResult(true, "Fetching failed for the specified reason: " + i + " msg: " + msg); } Debug.Log("Fetching failed for the specified reason: " + i + " msg: " + msg); }; m_StoreController.FetchAdditionalProducts(additional, onSuccess, onFailure); } } #region Public Func /// /// 根据商品ID 购买商品; /// /// 商品ID [Obsolete( "BuyProductByID(string productId) is deprecated, please use TKGSDKManager.Instance.BuyProductByID(string productId) instead.")] public void BuyProductByID(string productId, string productName, string gameExtraParam) { mProductName = productName; mGameExtraParam = gameExtraParam; TKGNativeInterface.Instance.LogIAPBtnClick(productName, productId); // 设置监听购买二次验证的回调 TKGSDKCallback.SetSecondPurchaseCallback(TKGSDKManager.Instance.OnPurchaseDone); if (IsInitialized()) { if (m_PurchaseInProgress) { TKGDebugger.LogDebug("The payment is in progress, please do not initiate the payment repeatedly."); return; } Product product = m_StoreController.products.WithID(productId); if (product != null && product.availableToPurchase) { OnPurchaseBegin?.Invoke(); m_PurchaseInProgress = true; TKGDebugger.LogDebug( string.Format("Purchasing product asychronously: '{0}'", product.definition.id)); m_StoreController.InitiatePurchase(product); } else { TKGDebugger.LogDebug( "BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase"); TKGNativeInterface.Instance.ClientIAPFailed(mProductName, productId, "", "", "", "", mGameExtraParam, IAPClientFailReasonType.NotInit); } } else { TKGDebugger.LogDebug("BuyProductID FAIL. IAP Not initialized or Not add product."); //OnPurchaseDone?.Invoke(productId, false, "", ""); TKGDebugger.LogDebug("OnPurchaseFailed -> productId : " + productId + " , transactionID : " + "" + " , localizedPrice : " + "" + " , isoCurrencyCode : " + ""); TKGNativeInterface.Instance.ClientIAPFailed(mProductName, productId, "", "", "", "", mGameExtraParam, IAPClientFailReasonType.NotInit); } } // 确认购买产品成功; private void DoConfirmPendingPurchaseByID(string productId) { Product product = m_StoreController.products.WithID(productId); if (product != null && product.availableToPurchase) { if (m_PurchaseInProgress) { m_StoreController.ConfirmPendingPurchase(product); m_PurchaseInProgress = false; } } } /// /// 恢复购买 /// [Obsolete("RestorePurchases() is deprecated, please use TKGSDKManager.Instance.RestorePurchases() instead.")] public void RestorePurchases() { if (!IsInitialized()) { OnRestoreDone?.Invoke(false); TKGDebugger.LogDebug("RestorePurchases FAIL. Not initialized."); return; } if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer) { TKGDebugger.LogDebug("RestorePurchases started ..."); mIsRestoring = true; var apple = m_StoreExtensionProvider.GetExtension(); apple.RestoreTransactions((result, msg) => { mIsRestoring = false; OnRestoreDone?.Invoke(result); // 返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase); TKGDebugger.LogDebug("RestorePurchases continuing: " + result + "msg:" + msg + " If no further messages, no purchases available to restore."); }); } else { TKGDebugger.LogDebug("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform); } } #endregion #region IStoreListener Callback // IAP初始化成功回调函数; public void OnInitialized(IStoreController controller, IExtensionProvider extensions) { TKGDebugger.LogDebug("IAP initialize Succeed!"); m_StoreController = controller; m_StoreExtensionProvider = extensions; // 这里可以获取您在AppStore和Google Play 上配置的商品; if (OnGetProductsInfo != null) { OnGetProductsInfo(m_StoreController.products.all); } foreach (var product in m_StoreController.products.all) { TKGDebugger.LogDebug(product.metadata.localizedTitle + "|" + product.metadata.localizedPriceString + "|" + product.metadata.localizedDescription + "|" + product.metadata.isoCurrencyCode); } #if UNITY_IOS m_StoreExtensionProvider.GetExtension().RegisterPurchaseDeferredListener(OnDeferred); #endif /* Dictionary introductory_info_dict = null; #if UNITY_IOS introductory_info_dict = m_StoreExtensionProvider.GetExtension().GetIntroductoryPriceDictionary(); #endif TKGDebugger.LogDebug("IAP - Available items:"); foreach (var item in controller.products.all) { if (item.availableToPurchase) { TKGDebugger.LogDebug("IAP - " + string.Join(" - ", new[] { item.metadata.localizedTitle, item.metadata.localizedDescription, item.metadata.isoCurrencyCode, item.metadata.localizedPrice.ToString(), item.metadata.localizedPriceString, item.transactionID, item.receipt })); // this is the usage of SubscriptionManager class if (item.receipt != null) { if (item.definition.type == ProductType.Subscription) { if (CheckIfProductIsAvailableForSubscriptionManagerC(item.receipt)) { string intro_json = (introductory_info_dict == null || !introductory_info_dict.ContainsKey(item.definition.storeSpecificId)) ? null : introductory_info_dict[item.definition.storeSpecificId]; SubscriptionManager p = new SubscriptionManager(item, intro_json); SubscriptionInfo info = p.getSubscriptionInfo(); mSubsInfo = info; TKGDebugger.LogDebug("product id is: " + info.getProductId()); TKGDebugger.LogDebug("purchase date is: " + info.getPurchaseDate()); TKGDebugger.LogDebug("subscription next billing date is: " + info.getExpireDate()); TKGDebugger.LogDebug("is subscribed? " + info.isSubscribed().ToString()); TKGDebugger.LogDebug("is expired? " + info.isExpired().ToString()); TKGDebugger.LogDebug("is cancelled? " + info.isCancelled()); TKGDebugger.LogDebug("product is in free trial peroid? " + info.isFreeTrial()); TKGDebugger.LogDebug("product is auto renewing? " + info.isAutoRenewing()); TKGDebugger.LogDebug("subscription remaining valid time until next billing date is: " + info.getRemainingTime()); TKGDebugger.LogDebug("is this product in introductory price period? " + info.isIntroductoryPricePeriod()); TKGDebugger.LogDebug("the product introductory localized price is: " + info.getIntroductoryPrice()); TKGDebugger.LogDebug("the product introductory price period is: " + info.getIntroductoryPricePeriod()); TKGDebugger.LogDebug("the number of product introductory price period cycles is: " + info.getIntroductoryPricePeriodCycles()); } else { TKGDebugger.LogDebug("This product is not available for SubscriptionManager class, only products that are purchase by 1.19+ SDK can use this class."); } } else { TKGDebugger.LogDebug("the product is not a subscription product"); } } else { TKGDebugger.LogDebug("the product should have a valid receipt"); } } } */ } public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription) { TKGDebugger.LogError(product.transactionID + "," + failureDescription.productId + "," + failureDescription.message); m_PurchaseInProgress = false; //OnPurchaseDone?.Invoke(product.definition.id, false, "", ""); TKGDebugger.LogDebug("productId : " + failureDescription.productId + " , transactionID : " + product.transactionID + " , localizedPrice : " + product.metadata.localizedPriceString + " , isoCurrencyCode : " + product.metadata.isoCurrencyCode); TKGNativeInterface.Instance.ClientIAPFailed(mProductName, failureDescription.productId, product.transactionID, "", product.metadata.isoCurrencyCode,product.metadata.localizedPrice.ToString(), mGameExtraParam, IAPClientFailReasonType.PurchaseFailed); } public void OnInitializeFailed(InitializationFailureReason error) { OnInitializeFailed(error, ""); } // IAP初始化失败回掉函数(没有网络的情况下并不会调起,而是一直等到有网络连接再尝试初始化); #pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。 public void OnInitializeFailed(InitializationFailureReason error, string? message) #pragma warning restore CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。 { TKGDebugger.LogDebug("IAP OnInitializeFailed error:" + error.ToString()); TKGDebugger.LogDebug("IAP OnInitializeFailed message:" + message); switch (error) { case InitializationFailureReason.AppNotKnown: TKGDebugger.LogError("Is your App correctly uploaded on the relevant publisher console?"); break; case InitializationFailureReason.PurchasingUnavailable: TKGDebugger.LogDebug("Billing disabled! Ask the user if billing is disabled in device settings."); break; case InitializationFailureReason.NoProductsAvailable: TKGDebugger.LogDebug( "No products available for purchase! Developer configuration error; check product metadata!"); break; } } private bool CheckIfProductIsAvailableForSubscriptionManagerC(string receipt) { var receipt_wrapper = (Dictionary)MiniJson.JsonDecode(receipt); if (!receipt_wrapper.ContainsKey("Store") || !receipt_wrapper.ContainsKey("Payload")) { TKGDebugger.LogDebug("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) { switch (store) { case GooglePlay.Name: { var payload_wrapper = (Dictionary)MiniJson.JsonDecode(payload); if (!payload_wrapper.ContainsKey("json")) { TKGDebugger.LogDebug( "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")) { TKGDebugger.LogDebug( "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")) { TKGDebugger.LogDebug( "The product receipt does not contain enough information, the product is not purchased using 1.19 or later"); return false; } return true; } case AppleAppStore.Name: case AmazonApps.Name: case MacAppStore.Name: { return true; } default: { return false; } } } return false; } // /// /// 支付成功处理函数; /// 在支持交易恢复功能的平台上(例如 Google Play 和通用 Windows 应用程序), /// Unity IAP 会在重新安装后的第一次初始化期间自动恢复用户拥有的任何商品; /// 系统将为每项拥有的商品调用 IStoreListener 的 ProcessPurchase 方法。 /// /// /// public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e) { m_PurchaseInProgress = false; TKGDebugger.LogDebug("Purchase OK: " + e.purchasedProduct.definition.id); // 消息结构 : Receipt: {"Store":"fake","TransactionID":"9c5c16a5-1ae4-468f-806d-bc709440448a","Payload":"{ \"this\" : \"is a fake receipt\" }"}; TKGDebugger.LogDebug("Receipt: " + e.purchasedProduct.receipt); TKGSDKManager.Instance.SetSegment(SegmentType.Purchase); //获取并解析你需要上传的数据。解析成string类型 var wrapper = (Dictionary)MiniJson.JsonDecode(e.purchasedProduct.receipt); // Corresponds to http://docs.unity3d.com/Manual/UnityIAPPurchaseReceipts.html // 正在使用的商店的名称,例如 GooglePlay 或 AppleAppStore var store = (string)wrapper["Store"]; TKGDebugger.LogDebug("Purchase OK: store" + store); //下面的payload 验证商品信息的数据。即我们需要上传的部分。 // For Apple this will be the base64 encoded ASN.1 receipt // 对于苹果来说,这将是base64编码的ASN.1收据 var payload = (string)wrapper["Payload"]; TKGDebugger.LogDebug("Purchase OK: payload" + payload); string token; string orderId; if (Application.platform == RuntimePlatform.Android) { // For GooglePlay payload contains more JSON // 对于GooglePlay有效负载包含更多JSON var gpDetails = (Dictionary)MiniJson.JsonDecode(payload); var gpJson = (string)gpDetails["json"]; var tokenJson = (Dictionary)MiniJson.JsonDecode(gpJson); token = (string)tokenJson["purchaseToken"]; orderId = (string)tokenJson["orderId"]; TKGDebugger.LogDebug("ClientIAPSuccess productId : " + e.purchasedProduct.definition.id + " , transactionID : " + orderId + " , token : " + token + " , localizedPrice : " + e.purchasedProduct.metadata.localizedPriceString + " , isoCurrencyCode : " + e.purchasedProduct.metadata.isoCurrencyCode); TKGNativeInterface.Instance.ClientIAPSuccess(mProductName, e.purchasedProduct.definition.id, orderId, token, e.purchasedProduct.metadata.isoCurrencyCode, e.purchasedProduct.metadata.localizedPrice.ToString(), mGameExtraParam); //if (orderId.StartsWith("GPA.")) //{ // TKGSDKManager.Instance.LogPurchasePrice(e.purchasedProduct.metadata.localizedPriceString, // e.purchasedProduct.metadata.isoCurrencyCode); // todo delete android upload event // OnPurchaseDone?.Invoke(e.purchasedProduct.definition.id, true, orderId, token); //} //else //{ // //OnPurchaseDone?.Invoke(e.purchasedProduct.definition.id, false, "", ""); // TKGDebugger.LogDebug("productId : " + e.purchasedProduct.definition.id + " , transactionID : " + orderId + " , localizedPrice : " + e.purchasedProduct.metadata.localizedPriceString + " , isoCurrencyCode : " + e.purchasedProduct.metadata.isoCurrencyCode); // TKGNativeInterface.Instance.ClientIAPFailed("", e.purchasedProduct.definition.id, orderId, token, e.purchasedProduct.metadata.localizedPriceString, e.purchasedProduct.metadata.isoCurrencyCode); //} } else { //TKGSDKManager.Instance.LogPurchasePrice(e.purchasedProduct.metadata.localizedPriceString, // e.purchasedProduct.metadata.isoCurrencyCode); // todo delete iOS upload event //苹果验单直接传入 payload token = ""; orderId = (string)wrapper["TransactionID"]; //OnPurchaseDone?.Invoke(e.purchasedProduct.definition.id, true, orderId, token); TKGDebugger.LogDebug("ClientIAPSuccess productId : " + e.purchasedProduct.definition.id + " , transactionID : " + orderId + " , token : " + token + " , localizedPrice : " + e.purchasedProduct.metadata.localizedPriceString + " , isoCurrencyCode : " + e.purchasedProduct.metadata.isoCurrencyCode); TKGNativeInterface.Instance.ClientIAPSuccess(mProductName, e.purchasedProduct.definition.id, orderId, token, e.purchasedProduct.metadata.isoCurrencyCode, e.purchasedProduct.metadata.localizedPrice.ToString(),mGameExtraParam); } return PurchaseProcessingResult.Complete; } // 支付失败回掉函数; public void OnPurchaseFailed(Product product, PurchaseFailureReason failureDescription) { TKGDebugger.LogDebug("Purchase OK: " + product.definition.id); m_PurchaseInProgress = false; //OnPurchaseDone?.Invoke(item.definition.id, false, "", ""); TKGDebugger.LogDebug("OnPurchaseFailed -> productId : " + product.definition.id + " , transactionID : " + product.transactionID + " , localizedPrice : " + product.metadata.localizedPriceString + " , isoCurrencyCode : " + product.metadata.isoCurrencyCode); TKGNativeInterface.Instance.ClientIAPFailed(mProductName, product.definition.id, product.transactionID, "", product.metadata.isoCurrencyCode,product.metadata.localizedPrice.ToString(),mGameExtraParam, IAPClientFailReasonType.PurchaseFailed); } // 购买延迟提示(这个看自己项目情况是否处理); // 在 Apple 平台上,我们需要处理由 Apple 的"购买前先询问"功能导致的延期购买。 // 在非 Apple 平台上,这将不起作用;永远不会调用 OnDeferred。 public void OnDeferred(Product product) { TKGDebugger.LogDebug("Purchase deferred: " + product.definition.id + " , maybe slow internet."); //OnPurchaseDone?.Invoke(item.definition.id, false, "", ""); TKGNativeInterface.Instance.ClientIAPFailed(mProductName, product.definition.id, product.transactionID, "", product.metadata.isoCurrencyCode, product.metadata.localizedPrice.ToString(), mGameExtraParam, IAPClientFailReasonType.Deferred); } // 恢复购买功能执行回掉函数; public void OnTransactionsRestored(bool success) { TKGDebugger.LogDebug("Transactions restored : " + success); } #endregion #region custom functions public string GetPriceByID(string pID) { if (m_StoreController == null && m_StoreExtensionProvider == null) return ""; Product[] tProducts = m_StoreController.products.all; for (int i = 0; i < tProducts.Length; i++) { Product tItem = tProducts[i]; if (tItem.definition.id.Equals(pID)) { #if UNITY_ANDROID return tItem.metadata.GetGoogleProductMetadata().localizedPriceString; #else return tItem.metadata.localizedPriceString; #endif } } return ""; } public Product GetProductInfoByID(string pID) { if (m_StoreController == null && m_StoreExtensionProvider == null) return null; for (int i = 0; i < m_StoreController.products.all.Length; i++) { Product tItem = m_StoreController.products.all[i]; if (tItem.definition.id.Equals(pID)) { return tItem; } } return null; } public SubscriptionInfo GetSubscriptionInfo() { return mSubsInfo; } #endregion } } #endif