diff --git a/Assets/Script/Common/FileParse/FileParse.cs b/Assets/Script/Common/FileParse/FileParse.cs index e818dd1..8b60b19 100644 --- a/Assets/Script/Common/FileParse/FileParse.cs +++ b/Assets/Script/Common/FileParse/FileParse.cs @@ -121,6 +121,14 @@ namespace WZ { StaticValue.AdmobCollapsibleBannerId = valueTemp; } + else if (_configs[i].Key.ToLower() == KEY_TF_IAP_CHECK_ORDER.ToLower()) + { + StaticValue.CheckOrderUrl = valueTemp; + } + else if (_configs[i].Key.ToLower() == KEY_TF_IAP_REPORT.ToLower()) + { + StaticValue.VerifyPurchaseUrl = valueTemp; + } else if (_configs[i].Key.ToLower() == KEY_Admob_NormalBannerId.ToLower()) { StaticValue.AdmobNormalBannerId = valueTemp; @@ -323,6 +331,8 @@ namespace WZ } + public const string KEY_TF_IAP_CHECK_ORDER = "TF_IAP_CHECK_ORDER"; + public const string KEY_TF_IAP_REPORT = "TF_IAP_REPORT"; public const string KEY_Admob_CollapsibleBannerId = "Admob_Banner_ID1"; public const string KEY_Admob_NormalBannerId = "Admob_Banner_ID2"; public const string KEY_Admob_SplashId = "Admob_APPOPEN_ID"; diff --git a/Assets/Script/Common/StaticValue.cs b/Assets/Script/Common/StaticValue.cs index 5d07ad7..a3a8ca7 100644 --- a/Assets/Script/Common/StaticValue.cs +++ b/Assets/Script/Common/StaticValue.cs @@ -7,6 +7,8 @@ namespace WZ public static class StaticValue { + public static string CheckOrderUrl = ""; + public static string VerifyPurchaseUrl = ""; public static string AdmobCollapsibleBannerId = ""; public static string AdmobNormalBannerId = ""; diff --git a/Assets/Script/SDKManager/AdjustManager/AdjustManager.cs b/Assets/Script/SDKManager/AdjustManager/AdjustManager.cs index 1c353c7..85ada3b 100644 --- a/Assets/Script/SDKManager/AdjustManager/AdjustManager.cs +++ b/Assets/Script/SDKManager/AdjustManager/AdjustManager.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using AdjustSdk; using ThinkingData.Analytics; using Unity.VisualScripting; +using Unity.VisualScripting.Antlr3.Runtime.Tree; +using UnityEditor.ShaderKeywordFilter; using UnityEngine; using WZ; @@ -18,6 +20,11 @@ public class AdjustManager : D_MonoSingleton private string callbackNetwork = ""; bool m_start = false; private int callGetTimes = 0; + private string _adjustNetwork = "_adjustNetwork"; + private string _adjustCampaign = "_adjustCampaign"; + private string _adjustAdgroup = "_adjustAdgroup"; + private string _adjustCreative = "_adjustCreative"; + private string _adjustClickLabel = "_adjustClickLabel"; public void Init() { @@ -27,7 +34,7 @@ public class AdjustManager : D_MonoSingleton Adjust.AddGlobalCallbackParameter("rush_version", RushSDKManager.GetSDKVersion()); Adjust.AddGlobalPartnerParameter("rush_version", RushSDKManager.GetSDKVersion()); - + AdjustConfig config = new AdjustConfig(StaticValue.AdjustToken, environment); // 设置归因变更回调函数 @@ -53,9 +60,9 @@ public class AdjustManager : D_MonoSingleton var network = AdjustNetwork.GetNetwork(); if (!string.IsNullOrEmpty(network)) - { + { RushSDKManager.Instance.OnUserSourceListener?.Invoke(IsOrganic(network), network); - } + } } @@ -172,8 +179,37 @@ public class AdjustManager : D_MonoSingleton }); RushSDKManager.Instance.OnUserSourceListener?.Invoke(IsOrganic(network), network); + PlayerPrefsUtils.SavePlayerPrefsString(_adjustNetwork, network); + PlayerPrefsUtils.SavePlayerPrefsString(_adjustCampaign, campaign); + PlayerPrefsUtils.SavePlayerPrefsString(_adjustAdgroup, adgroup); + PlayerPrefsUtils.SavePlayerPrefsString(_adjustCreative, creative); + PlayerPrefsUtils.SavePlayerPrefsString(_adjustClickLabel, attribution?.ClickLabel); } + public string GetAdjustNetwork() + { + return PlayerPrefsUtils.GetPlayerPrefsString(_adjustNetwork); + } + + public string GetAdjustCampaign() + { + return PlayerPrefsUtils.GetPlayerPrefsString(_adjustCampaign); + } + + public string GetAdjustAdgroup() + { + return PlayerPrefsUtils.GetPlayerPrefsString(_adjustAdgroup); + } + + public string GetAdjustCreative() + { + return PlayerPrefsUtils.GetPlayerPrefsString(_adjustCreative); + } + + public string GetAdjustClickLabel() + { + return PlayerPrefsUtils.GetPlayerPrefsString(_adjustClickLabel); + } private bool IsOrganic(string _network) { diff --git a/Assets/Script/SDKManager/Purchase.meta b/Assets/Script/SDKManager/Purchase.meta new file mode 100644 index 0000000..f9ac89a --- /dev/null +++ b/Assets/Script/SDKManager/Purchase.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2ebf62044324047e78f251d34185ff48 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/Constant.meta b/Assets/Script/SDKManager/Purchase/Constant.meta new file mode 100644 index 0000000..29ad424 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Constant.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ca2f9d75394fd4e859f89e488d422e80 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/Constant/IAPDataStateType.cs b/Assets/Script/SDKManager/Purchase/Constant/IAPDataStateType.cs new file mode 100644 index 0000000..ca00d4f --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Constant/IAPDataStateType.cs @@ -0,0 +1,16 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace WZ +{ + public enum IAPDataStateType + { + // 未处理 + def, + // 处理中 + deal + } + +} + diff --git a/Assets/Script/SDKManager/Purchase/Constant/IAPDataStateType.cs.meta b/Assets/Script/SDKManager/Purchase/Constant/IAPDataStateType.cs.meta new file mode 100644 index 0000000..a8d7ff6 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Constant/IAPDataStateType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0595ab8f5ac6241689d314aec6c7a3ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/Constant/IAPResultType.cs b/Assets/Script/SDKManager/Purchase/Constant/IAPResultType.cs new file mode 100644 index 0000000..48e87cf --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Constant/IAPResultType.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace WZ +{ + + public enum IAPResultType + { + PurchasingUnavailable, // 无法使用系统购买功能 + ExistingPurchasePending, // 请求新购买时正在进行前一项购买 + ProductUnavailable, // 无法在商店购买商品 + SignatureInvalid, // 购买收据的签名验证失败 + UserCancelled, // 用户选择取消而不是继续购买 + PaymentDeclined, // 付款出现问题 + DuplicateTransaction, // 当交易已经成功完成时出现的重复交易错误 + Unknown, // 未识别的购买问题的通用原因 + ServerRequestFailed, // 客户端购买成功请求服务端失败 + ServerAuthenticationFailed, // 客户端购买成功服务端验证失败 + PurchasingSuccess, + NULL + } + +} + diff --git a/Assets/Script/SDKManager/Purchase/Constant/IAPResultType.cs.meta b/Assets/Script/SDKManager/Purchase/Constant/IAPResultType.cs.meta new file mode 100644 index 0000000..dc195ff --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Constant/IAPResultType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2aa385284497442cebdff40e44bad707 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/IAPEvent.cs b/Assets/Script/SDKManager/Purchase/IAPEvent.cs new file mode 100644 index 0000000..56e41cc --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPEvent.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +#if UNITY_PURCHASE +namespace WZ +{ + public static class IAPEvent + { + public static void LogPurchaseInit() + { + var eventName = "Purchase_Init"; + FireBaseAnalyticsManager.Instance.LogEvent(eventName); + ShuShuEvent.Instance.Track(eventName); + } + + public static void LogPurchaseInit(bool success, string message = "") + { + var eventName = success ? "Purchase_Init_Success" : "Purchase_Init_Fail"; + if (success) + { + FireBaseAnalyticsManager.Instance.LogEvent(eventName); + ShuShuEvent.Instance.Track(eventName); + } + else + { + FireBaseAnalyticsManager.Instance.LogEvent(eventName, new Dictionary { { "fail_reason", message } }); + ShuShuEvent.Instance.Track(eventName, new Dictionary { { "fail_reason", message } }); + } + } + + public static void LogPurchaseSuccess(PurchaseInfo info,string environment) + { + // 总支付次数 + int payTimes = PlayerPrefsUtils.GetPlayerPrefsInt("PAY_TIME", 0); + if (!info.orderAlreadyExists) + { + payTimes += 1; + } + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + var args = new Dictionary + { + { "IAP", info.productName }, + { "product_id", info.productID }, + { "currency", info.currency }, + { "Price", fPrice }, + { "order_id", info.orderID }, + { "game_extra", info.gameExtra }, + { "is_first", PlayerPrefsUtils.GetPlayerPrefsInt(First_Purchase, 0) == 0}, + { "payment_type", "GooglePlay" }, + { "product_type", info.productType }, + { "orderAlreadyExists", info.orderAlreadyExists } + }; + if (string.IsNullOrEmpty(environment)) environment = ""; + var eventName = environment.Equals("sandbox") ? "IAP_Success_Sandbox" : "IAP_Success"; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, args); + ShuShuEvent.Instance.Track(eventName, args); + // 首次订单 设置支付次数/累计支付次数/累计支付金额 + if (!info.orderAlreadyExists) + { + fPrice = fPrice + PlayerPrefsUtils.GetPlayerPrefsFloat(PURCHASE_PRICE, 0.0f); + PlayerPrefsUtils.SavePlayerPrefsFloat(PURCHASE_PRICE, fPrice); + + ShuShuEvent.Instance.UserSet(new Dictionary() + { + { "total_pay_amount", fPrice }, + { "total_pay_times", payTimes } + }); + + ShuShuEvent.Instance.SetSuperProperties(new Dictionary() + { + { "pay_times", payTimes } + }); + PlayerPrefsUtils.SavePlayerPrefsInt(First_Purchase, 1); + ShuShuEvent.Instance.UserSet(new Dictionary() + { + { "first_pay_time", DateTime.Now } + }, true); + } + + PlayerPrefsUtils.SavePlayerPrefsInt(PAY_TIME, payTimes); + } + + public static void LogPurchaseFail(PurchaseInfo info) + { + var eventName = "IAP_Fail"; + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + var para = new Dictionary + { + { "IAP", info.productName }, + { "product_id", info.productID }, + { "currency", info.currency }, + { "pay_amount", fPrice }, + { "order_id", info.orderID }, + { "game_extra", info.gameExtra ?? ""}, + { "payment_type", "GooglePlay" }, + { "fail_reason", info.failReason } + }; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, para); + ShuShuEvent.Instance.Track(eventName, para); + } + + public static void LogIAPButtonClick(PurchaseInfo info) + { + var eventName = "IAP_Button_Click"; + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + var args = new Dictionary + { + { "IAP", info.productName}, + { "product_id", info.productID}, + { "currency", info.currency }, + { "Price", fPrice }, + { "game_extra", info.gameExtra}, + { "payment_type", "GooglePlay" }, + }; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, args); + ShuShuEvent.Instance.Track(eventName, args); + } + + public static void LogClientComplete(PurchaseInfo info) + { + var eventName = "Client_Finish_Channel"; + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + var para = new Dictionary + { + { "IAP", info.productName }, + { "product_id", info.productID }, + { "currency", info.currency }, + { "Price", fPrice }, + { "order_id", info.orderID }, + { "game_extra", info.gameExtra ?? ""}, + { "payment_type", "GooglePlay" }, + { "product_type",info.productType}, + { "is_first", PlayerPrefsUtils.GetPlayerPrefsInt(First_Purchase, 0) == 0 } + }; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, para); + ShuShuEvent.Instance.Track(eventName, para); + } + + public static void LogSaveOrderBySdk(PurchaseInfo info) + { + var eventName = "IAP_SDK_Save_Order"; + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + var para = new Dictionary + { + { "IAP", info.productName }, + { "product_id", info.productID }, + { "currency", info.currency }, + { "price", fPrice }, + { "order_id", info.orderID }, + { "game_extra", info.gameExtra ?? ""}, + { "payment_type", "GooglePlay" }, + { "product_type",info.productType}, + { "is_first", PlayerPrefsUtils.GetPlayerPrefsInt(First_Purchase, 0) == 0 } + }; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, para); + ShuShuEvent.Instance.Track(eventName, para); + } + + public static void LogStarVerifyOrderBySdk(PurchaseInfo info) + { + var eventName = "IAP_Cache_Order"; + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + var para = new Dictionary + { + { "IAP", info.productName }, + { "product_id", info.productID }, + { "currency", info.currency }, + { "price", fPrice }, + { "order_id", info.orderID }, + { "game_extra", info.gameExtra}, + { "payment_type", "GooglePlay" }, + { "product_type",info.productType}, + { "is_first", PlayerPrefsUtils.GetPlayerPrefsInt(First_Purchase, 0) == 0 } + }; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, para); + ShuShuEvent.Instance.Track(eventName, para); + } + + public static void LogRemoveOrderBySdk(PurchaseInfo info) + { + var eventName = "IAP_SDK_Remove_Order"; + float fPrice; + if (!float.TryParse(info.price, out fPrice)) + { + fPrice = 0.0f; + } + + var para = new Dictionary + { + { "price", fPrice }, + { "product_id", info.productID }, + { "IAP", info.productName }, + { "order_id", info.orderID }, + { "currency", info.currency }, + { "payment_method", "googleplay" }, + { "game_extra", info.gameExtra}, + { "product_type", info.productType} + }; + FireBaseAnalyticsManager.Instance.LogEvent(eventName, para); + ShuShuEvent.Instance.Track(eventName, para); + } + + public static string First_Purchase = "First_purchase"; + public static string PAY_TIME = "PAY_TIME"; + public static string PURCHASE_PRICE = "PURCHASE_PRICE"; + + } + +} +#endif diff --git a/Assets/Script/SDKManager/Purchase/IAPEvent.cs.meta b/Assets/Script/SDKManager/Purchase/IAPEvent.cs.meta new file mode 100644 index 0000000..982759d --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 273e000ea028247038e5e7f8f756fbc0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/IAPInfo.cs b/Assets/Script/SDKManager/Purchase/IAPInfo.cs new file mode 100644 index 0000000..528db05 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPInfo.cs @@ -0,0 +1,39 @@ +#if UNITY_PURCHASE +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +namespace WZ +{ + public struct PurchaseInfo + { + public string productName; + public string productID; + public string orderID; + public string currency; + public string price; + public string gameExtra; + public string productType; + public string failReason; + public bool orderAlreadyExists; + public bool purchaseResult; + public IAPResultType resultType; + public PurchaseInfo(string productName, string productID, string orderID, + string currency, string price, string gameExtra, + string productType, string failReason, bool orderAlreadyExists, bool purchaseResult, + IAPResultType resultType) + { + this.productName = productName; + this.productID = productID; + this.orderID = orderID; + this.currency = currency; + this.price = price; + this.gameExtra = gameExtra; + this.productType = productType; + this.failReason = failReason; + this.orderAlreadyExists = orderAlreadyExists; + this.purchaseResult = purchaseResult; + this.resultType = resultType; + } + } +} +#endif \ No newline at end of file diff --git a/Assets/Script/SDKManager/Purchase/IAPInfo.cs.meta b/Assets/Script/SDKManager/Purchase/IAPInfo.cs.meta new file mode 100644 index 0000000..7079b92 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d2378dd12f2d41c190fc63f6016b4d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/IAPOrderManager.cs b/Assets/Script/SDKManager/Purchase/IAPOrderManager.cs new file mode 100644 index 0000000..e932413 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPOrderManager.cs @@ -0,0 +1,395 @@ +#if UNITY_PURCHASE +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using UnityEngine; + +namespace WZ +{ + + public class IAPOrderManager : D_MonoSingleton + { + private static string _purchaseOrderMap = "PurchaseOrderMap"; + private List NotRetryCode = new List { 0, 430, 900, 901, 902, 903, 904, 905 }; + private Dictionary mRepeatCountDic; + + #region 订单验证 + public void VerifyPurchase(IAPDataConfig args) + { + LoggerUtils.Debug("[iap] Start VerifyPurchase orderid:" + args.info["order_id"] + "status:" + args.state); + if (args.state == IAPDataStateType.def) + { + // 记录订单处理次数,处理5次还是失败就不处理了 + if (mRepeatCountDic.ContainsKey(args.info["order_id"])) + { + mRepeatCountDic[args.info["order_id"]] = mRepeatCountDic[args.info["order_id"]] + 1; + } + else + { + mRepeatCountDic.Add(args.info["order_id"], 1); + } + + // 开始处理,设置正在处理的状态 + RefreshOrderStatue(args, IAPDataStateType.deal); + args.info.TryGetValue("mGameExtraParam", out var gameExtraParam); + + if (string.IsNullOrEmpty(gameExtraParam)) + { + gameExtraParam = IAPPurchaseManager.Instance._gameExtraParam; + } + + LoggerUtils.Debug("[iap] IAP VerifyPurchase start"); + var requestArgs = args.info.ToDictionary(kv => kv.Key, kv => (object)kv.Value); + ServerMgr.Instance.VerifyPurchase(requestArgs, (code, msg, data) => + { + RefreshOrderStatue(args, IAPDataStateType.def); + LoggerUtils.Debug("IAP VerifyPurchase CODE:" + code + " illegal:" + data.illegal_order + " env:" + data.environment); + LoggerUtils.Debug("IAP VerifyPurchase msg:" + msg); + + if ((code == 0 || code == 430) && data.illegal_order == 0) + { + // 将订阅商品信息存储到本地 + if (!IAPPurchaseManager.Instance._productArgs.ContainsKey(args.info["product_id"].ToString()) && args.info["product_type"].ToString().Equals("Subscription")) + { + IAPPurchaseManager.Instance._productArgs.Add(args.info["product_id"].ToString(), requestArgs); + + } + else if (IAPPurchaseManager.Instance._productArgs.ContainsKey(args.info["product_id"].ToString()) && args.info["product_type"].ToString().Equals("Subscription")) + { + IAPPurchaseManager.Instance._productArgs[args.info["product_id"].ToString()] = requestArgs; + } + RushSDKManager.Instance.OnPurchaseComplete?.Invoke(new PurchaseInfo( + productName: args.info["product_name"], + productID: args.info["product_id"], + orderID: args.info["order_id"], + currency: args.info["currency"], + price: args.info["price"], + gameExtra: gameExtraParam, + failReason: "", + orderAlreadyExists: code == 430, + purchaseResult: code == 0, + resultType: IAPResultType.PurchasingSuccess, + productType: args.info["product_type"] + )); + LogVerifySuccessOrder(args, data.environment, gameExtraParam, code == 430); + } + else + { + // 不需要重试的错误码 + if (!NotRetryCode.Contains(code)) + { + SaveVerifyFailOrderId(args); + } + + // 主动删除过期订单 + if (!string.IsNullOrEmpty(data.illegal_msg) && args.info["product_type"].ToString().Equals("Subscription")) + { + if (data.illegal_msg.Contains("product expired")) + { + RemoveVerifySuccessOrder(args.info["product_id"]); + } + } + if (code == -1) + { + RushSDKManager.Instance.OnPurchaseComplete?.Invoke(new PurchaseInfo( + productName: args.info["product_name"], + productID: args.info["product_id"], + orderID: args.info["order_id"], + currency: args.info["currency"], + price: args.info["price"], + gameExtra: gameExtraParam, + failReason: "", + orderAlreadyExists: code == 430, + purchaseResult: false, + resultType: IAPResultType.NULL, + productType: args.info["product_type"] + )); + } + else + { + RushSDKManager.Instance.OnPurchaseComplete?.Invoke(new PurchaseInfo( + productName: args.info["product_name"], + productID: args.info["product_id"], + orderID: args.info["order_id"], + currency: args.info["currency"], + price: args.info["price"], + gameExtra: gameExtraParam, + failReason: "", + orderAlreadyExists: code == 430, + purchaseResult: false, + resultType: code == 430 ? IAPResultType.PurchasingSuccess : IAPResultType.ServerAuthenticationFailed, + productType: args.info["product_type"] + )); + } + + // msg 包含 illegal_order_0 说明是正常的缓存订单上报 + // 卸载重装后,一次性商品 + if (msg.Contains("illegal_order_0")) + { + LogVerifySuccessOrder(args, msg.Contains("sandbox") ? "sandbox" : "production", gameExtraParam, code == 430); + } + else + { + LogVerifyFailOrder(args, gameExtraParam, $"Purchase service code : {code} msg : {msg} dataMsg:{data.illegal_msg} illegal_order:{data.illegal_order} environment:{(msg.Contains("sandbox") ? "sandbox" : "production")}"); + } + } + }); + } + } + + private void LogVerifySuccessOrder(IAPDataConfig _productInfo, string _enviroment, string _gameExtraParam, bool _orderAlreadyExists) + { + IAPEvent.LogPurchaseSuccess(new PurchaseInfo( + productName: _productInfo.info["product_name"], + productID: _productInfo.info["product_id"], + orderID: _productInfo.info["order_id"], + currency: _productInfo.info["currency"], + price: _productInfo.info["price"], + gameExtra: _gameExtraParam, + failReason: "", + orderAlreadyExists: _orderAlreadyExists, + purchaseResult: !_orderAlreadyExists, + resultType: IAPResultType.NULL, + productType: _productInfo.info["product_type"] + ), _enviroment); + } + + private void LogVerifyFailOrder(IAPDataConfig _productInfo, string _gameExtraParam, string _errInfo) + { + + IAPEvent.LogPurchaseFail(new PurchaseInfo( + productName: _productInfo.info["product_name"], + productID: _productInfo.info["product_id"], + orderID: _productInfo.info["order_id"], + currency: _productInfo.info["currency"], + price: _productInfo.info["price"], + gameExtra: _gameExtraParam, + failReason: _errInfo, + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL, + productType: _productInfo.info["product_type"] + )); + } + + #endregion + + #region 重置订单状态 + private void RefreshOrderStatue(IAPDataConfig _productInfo, IAPDataStateType state) + { + if (ES3.KeyExists("FailOrderCacheData")) + { + List list = ES3.Load(_purchaseOrderMap) as List; + var value = _productInfo.info["order_id"]; + var want = list.Find(e => e.info.ContainsValue(value)); + if (want != null) + { + want.state = state; + ES3.Save(_purchaseOrderMap, list); + } + } + } + #endregion + + #region 存储订单 + public void SaveOrder(IAPDataConfig _productInfo) + { + if (ES3.KeyExists(_purchaseOrderMap)) + { + List list = ES3.Load(_purchaseOrderMap) as List; + var want = list.Find(e => e.info.ContainsValue(_productInfo.info["order_id"])); + if (want == null) + { + list.Add(_productInfo); + ES3.Save(_purchaseOrderMap, list); + IAPEvent.LogSaveOrderBySdk(new PurchaseInfo( + productName: _productInfo.info["product_name"], + productID: _productInfo.info["product_id"], + orderID: _productInfo.info["order_id"], + currency: _productInfo.info["currency"], + price: _productInfo.info["price"], + gameExtra: _productInfo.info["mGameExtraParam"], + productType: _productInfo.info["product_type"], + failReason: null, + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL + )); + + LoggerUtils.Debug("[iap] IAP SaveOrder 已经有表了,添加订单,订单ID:" + _productInfo.info["order_id"] + " 未验证订单个数:" + list.Count); + } + } + else + { + List tem = new List + { + _productInfo + }; + ES3.Save(_purchaseOrderMap, tem); + IAPEvent.LogSaveOrderBySdk(new PurchaseInfo( + productName: _productInfo.info["product_name"], + productID: _productInfo.info["product_id"], + orderID: _productInfo.info["order_id"], + currency: _productInfo.info["currency"], + price: _productInfo.info["price"], + gameExtra: _productInfo.info["mGameExtraParam"], + productType: _productInfo.info["product_type"], + failReason: null, + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL + )); + LoggerUtils.Debug("[iap] IAP SaveOrder 第一次存储表 添加订单ID:" + _productInfo.info["order_id"] + " 商品类型:" + _productInfo.info["product_type"] + " 订单个数:" + tem.Count); + } + } + #endregion + + #region 保存验证失败订单 + private bool inDealFailOrder; + private void SaveVerifyFailOrderId(IAPDataConfig _productInfo) + { + LoggerUtils.Debug("[iap] iap save verify fail order,orderId:" + _productInfo.info["order_id"] + " 商品类型:" + _productInfo.info["product_type"]); + if (ES3.KeyExists(_purchaseOrderMap)) + { + List list = ES3.Load(_purchaseOrderMap) as List; + + var value = _productInfo.info["order_id"]; + var want = list.Find(e => e.info.ContainsValue(value)); + if (want == null) + { + list.Add(_productInfo); + ES3.Save(_purchaseOrderMap, list); + ReadFailOrderId(); + LoggerUtils.Debug("[iap] IAP SaveVerifyFailOrderId 已经有表了,添加失败订单,订单ID:" + _productInfo.info["order_id"] + " 商品类型:" + _productInfo.info["product_type"]); + } + else + { + if (!inDealFailOrder) + { + inDealFailOrder = true; + InvokeRepeating(nameof(ReadFailOrderId), 0, 30); + } + } + } + else + { + List tem = new List + { + _productInfo + }; + ES3.Save(_purchaseOrderMap, tem); + ReadFailOrderId(); + LoggerUtils.Debug("[iap] IAP SaveVerifyFailOrderId 第一次存储表 添加失败订单 订单个数:" + tem.Count); + } + } + #endregion + + #region 再次验证订单 + public void ReadFailOrderId() + { + if (ES3.KeyExists(_purchaseOrderMap)) + { + List list = ES3.Load(_purchaseOrderMap) as List; + LoggerUtils.Debug("[iap] IAP ReadFailOrderId 读取失败订单列表,count:" + list.Count + " mRepeatCountDic" + mRepeatCountDic.Count); + if (list.Count > 0) + { + // 当前进程每条订单校验5次,如果失败则不再处理 + // 重复校验时判断次数是否大于5,大于5则不再处理 + IAPDataConfig tempData = null; + if (mRepeatCountDic.Count > 0) + { + foreach (var data in list) + { + LoggerUtils.Debug("[iap] iap start verify fail order with repeat dic" + data.info["order_id"] + " count:" + mRepeatCountDic[data.info["order_id"]]); + if (mRepeatCountDic.ContainsKey(data.info["order_id"]) && mRepeatCountDic[data.info["order_id"]] <= 5) + { + tempData = data; + break; + } + } + } + // 如果还有次数不超过5次的订单,则继续校验,否则就不再处理 + if (tempData != null) + { + LoggerUtils.Debug("[iap] iap start verify fail order with repeat temp dic" + tempData.info["order_id"] + " count:" + mRepeatCountDic[tempData.info["order_id"]]); + VerifyPurchase(tempData); + IAPEvent.LogStarVerifyOrderBySdk(new PurchaseInfo( + productName: tempData.info["product_name"], + productID: tempData.info["product_id"], + orderID: tempData.info["order_id"], + currency: tempData.info["currency"], + price: tempData.info["price"], + gameExtra: tempData.info["mGameExtraParam"], + productType: tempData.info["product_type"], + failReason: null, + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL)); + } + else + { + LoggerUtils.Debug("[iap] iap cancel verify fail order"); + CancelInvoke(nameof(ReadFailOrderId)); + inDealFailOrder = false; + } + } + else + { + CancelInvoke(nameof(ReadFailOrderId)); + inDealFailOrder = false; + } + } + } + + #endregion + + #region 移除订单 + // 校验成功后移除订单 + public void RemoveVerifySuccessOrder(string orderId) + { + if (ES3.KeyExists(_purchaseOrderMap)) + { + List list = ES3.Load(_purchaseOrderMap) as List; + var want = list.Find(e => e.info.ContainsValue(orderId)); + if (want != null) + { + LoggerUtils.Debug("[iap] IAP RemoveVerifySuccessOrder 找到并移除订单,订单ID:" + orderId); + list.Remove(want); + IAPEvent.LogRemoveOrderBySdk(new PurchaseInfo( + productName: want.info["product_name"], + productID: want.info["product_id"], + orderID: want.info["order_id"], + currency: want.info["currency"], + price: want.info["price"], + gameExtra: want.info["mGameExtraParam"], + productType: want.info["product_type"], + failReason: null, + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL)); + ES3.Save(_purchaseOrderMap, list); + } + + LoggerUtils.Debug("[iap] IAP 订单还剩余:" + list.Count + " 条未校验"); + if (list.Count > 0) + { + ReadFailOrderId(); + } + else + { + inDealFailOrder = false; + CancelInvoke(nameof(ReadFailOrderId)); + } + } + } + + #endregion + public class IAPDataConfig + { + public Dictionary info; + public IAPDataStateType state; + } + } +} +#endif diff --git a/Assets/Script/SDKManager/Purchase/IAPOrderManager.cs.meta b/Assets/Script/SDKManager/Purchase/IAPOrderManager.cs.meta new file mode 100644 index 0000000..574c1f7 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPOrderManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c0364ee309a94459af3113c481b3206 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/IAPPurchaseManager.cs b/Assets/Script/SDKManager/Purchase/IAPPurchaseManager.cs new file mode 100644 index 0000000..45ba708 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPPurchaseManager.cs @@ -0,0 +1,581 @@ +#if UNITY_PURCHASE +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework.Constraints; +using Unity.Services.Core; +using Unity.Services.Core.Environments; +using UnityEngine; +using UnityEngine.Purchasing; +using UnityEngine.Purchasing.Extension; +using static WZ.IAPOrderManager; + + +namespace WZ +{ + public class IAPPurchaseManager : D_MonoSingleton, IDetailedStoreListener + { + #region API + public void BuyProductByID(string productId, string productName, string gameExtraParam) + { +#if !UNITY_EDITOR + RushSDKManager.Instance.OnPurchaseComplete(new PurchaseInfo( + productName: productName, + productID: productId, + orderID: "", + currency: "USD", + price: "", + gameExtra: gameExtraParam, + failReason: "", + orderAlreadyExists: false, + purchaseResult: true, + resultType: IAPResultType.PurchasingSuccess, + productType: "" + )); +#else + _productName = productName; + _gameExtraParam = gameExtraParam; + Product m_p = GetProductInfoByID(productId); + var currencyCode = ""; + var localizedPrice = ""; + if (m_p != null) + { + currencyCode = m_p.metadata.isoCurrencyCode; + localizedPrice = m_p.metadata.localizedPrice.ToString(); + } + + IAPEvent.LogIAPButtonClick(new PurchaseInfo( + productName: _productName, + productID: productId, + orderID: "", + currency: currencyCode, + price: localizedPrice, + gameExtra: _gameExtraParam, + failReason: "", + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL, + productType: "")); + + if (IsInitialized()) + { + if (_inPurchaseProgress) + { + LoggerUtils.Debug("[iap] The payment is in progress, please do not initiate the payment repeatedly."); + return; + } + + Product product = _storeController.products.WithID(productId); + if (product != null && product.availableToPurchase) + { + _inPurchaseProgress = true; + LoggerUtils.Debug( + string.Format("[iap] Purchasing product asychronously: '{0}'", product.definition.id)); + + if (_googlePlayConfiguration != null) + { + + if (!string.IsNullOrEmpty(productName)) + { + _googlePlayConfiguration.SetObfuscatedAccountId(productName); + } + _googlePlayConfiguration.SetObfuscatedProfileId(gameExtraParam); + LoggerUtils.Debug($"[iap] [BuyProductByID] 设置成功 userId = {productName} profileId = {gameExtraParam}"); + } + _storeController.InitiatePurchase(product); + } + else + { + IAPEvent.LogPurchaseFail(new PurchaseInfo( + productName: _productName, + productID: productId, + orderID: "", + currency: currencyCode, + price: localizedPrice, + gameExtra: _gameExtraParam, + failReason: "BuyProductID FAIL. Not purchasing product, either is not found or is not available for purchase", + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL, + productType: product.definition.type.ToString())); + LoggerUtils.Debug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase"); + + } + } + else + { + RushSDKManager.Instance.OnPurchaseComplete(new PurchaseInfo( + productName: productName, + productID: productId, + orderID: "", + currency: "", + price: "", + gameExtra: _gameExtraParam, + failReason: "BuyProductID FAIL. IAP Not initialized or Not add product.", + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.PurchasingUnavailable, + productType: "")); + + IAPEvent.LogPurchaseFail(new PurchaseInfo( + productName: _productName, + productID: productId, + orderID: "", + currency: currencyCode, + price: localizedPrice, + gameExtra: _gameExtraParam, + failReason: "BuyProductID FAIL. Not purchasing product, either is not found or is not available for purchase", + orderAlreadyExists: false, + purchaseResult: false, + resultType: IAPResultType.NULL, + productType: "")); + LoggerUtils.Debug("[iap] BuyProductID FAIL. IAP Not initialized or Not add product."); + LoggerUtils.Debug("[iap] OnPurchaseFailed -> productId : " + productId + " , transactionID : " + "" + " , localizedPrice : " + "" + " , isoCurrencyCode : " + ""); + } +#endif + } + + public Product GetProductInfoByID(string pID) + { + if (_storeController == null && _storeExtensionProvider == null) + return null; + for (int i = 0; i < _storeController.products.all.Length; i++) + { + Product tItem = _storeController.products.all[i]; + if (tItem.definition.id.Equals(pID)) + { + return tItem; + } + } + return null; + } + + public void AddProducts(Dictionary products, Action onProductsResult = null) + { + _initProductDic = products; + + FetchAdditionalProducts(products, onProductsResult); + } + + + + #endregion + + + #region 购买成功 + public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent) + { + _inPurchaseProgress = false; + + LoggerUtils.Debug("[iap] Purchase OK: " + purchaseEvent.purchasedProduct.definition.id); + + var wrapper = (Dictionary)MiniJson.JsonDecode(purchaseEvent.purchasedProduct.receipt); + var payload = (string)wrapper["Payload"]; + var profileId = _gameExtraParam; + var _productName = ""; + var payloadObj = (Dictionary)MiniJson.JsonDecode(payload); + var o = (string)payloadObj["json"]; + var payloadData = (Dictionary)MiniJson.JsonDecode(o); + + if (payloadData.TryGetValue("obfuscatedAccountId", out var obfuscatedAccountIdValue)) + { + var obfuscatedAccountId = (string)obfuscatedAccountIdValue; + if (!string.IsNullOrEmpty(obfuscatedAccountId)) + { + _productName = obfuscatedAccountId; + } + } + + if (payloadData.TryGetValue("obfuscatedProfileId", out var value)) + { + var obfuscatedProfileId = (string)value; + if (!string.IsNullOrEmpty(obfuscatedProfileId)) + { + profileId = obfuscatedProfileId; + } + } + LoggerUtils.Debug("[iap] productName" + _productName + " profileId:" + profileId); + + string token = ""; + string orderId = ""; + if (Application.platform == RuntimePlatform.Android) + { + 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"]; + + LoggerUtils.Debug("[iap] ClientIAPSuccess productId : " + purchaseEvent.purchasedProduct.definition.id + + " , transactionID : " + orderId + + " , token : " + token + + " , localizedPrice : " + purchaseEvent.purchasedProduct.metadata.localizedPriceString + + " , isoCurrencyCode : " + purchaseEvent.purchasedProduct.metadata.isoCurrencyCode); + } + + IAPDataConfig newData = new IAPDataConfig(); + newData.info = new Dictionary + { + { "price", purchaseEvent.purchasedProduct.metadata.localizedPrice.ToString() }, + { "product_id", purchaseEvent.purchasedProduct.definition.id }, + { "product_name", (string)_productName }, + { "purchase_token", token }, + { "order_id", orderId }, + { "currency", purchaseEvent.purchasedProduct.metadata.isoCurrencyCode }, + { "payment_method", "googleplay" }, + { "product_type", purchaseEvent.purchasedProduct.definition.type.ToString() }, + { "mGameExtraParam", (string)profileId } + }; + newData.state = IAPDataStateType.def; + + IAPEvent.LogClientComplete(new PurchaseInfo( + productName: (string)_productName, + productID: purchaseEvent.purchasedProduct.definition.id, + orderID: orderId, + currency: purchaseEvent.purchasedProduct.metadata.isoCurrencyCode, + price: purchaseEvent.purchasedProduct.metadata.localizedPrice.ToString(), + gameExtra: _gameExtraParam, + failReason: "", + orderAlreadyExists: false, + purchaseResult: true, + resultType: IAPResultType.PurchasingSuccess, + productType: purchaseEvent.purchasedProduct.definition.type.ToString() + )); + + IAPOrderManager.Instance.SaveOrder(newData); + IAPOrderManager.Instance.VerifyPurchase(newData); + return PurchaseProcessingResult.Complete; + } + + #endregion + + #region 动态添加商品 + private void FetchAdditionalProducts(Dictionary ProductDic, + Action onProductsResult = null) + { + if (!IsInitialized()) + { + _addProductsDic = ProductDic; + LoggerUtils.Debug("[iap] IAP not init.Now InitUnityPurchase"); + InitUnityPurchase(); + return; + } + + if (_isFetchingAdditionalProducts) + { + LoggerUtils.Debug("[iap] 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; + + LoggerUtils.Debug("[iap] Fetched successfully!"); + RushSDKManager.Instance.OnGetProductsInfo?.Invoke(_storeController.products.all); + + foreach (var product in _storeController.products.all) + { + LoggerUtils.Debug("[iap]" + 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); + } + + LoggerUtils.Debug("[iap] Fetching failed for the specified reason: " + i + " msg: " + msg); + }; + + _storeController.FetchAdditionalProducts(additional, onSuccess, onFailure); + } + } + #endregion + + #region 初始化 + public void PreInitialize() + { + LoggerUtils.Debug("[iap] PreInitialize() mServiceInit: " + _serviceInit); + if (!_serviceInit) + { + InitializeUnityServices(OnSuccess, OnError); + } + } + + private void InitializeUnityServices(Action onSuccess, Action onError) + { + try + { + var options = new InitializationOptions().SetEnvironmentName("production"); + + UnityServices.InitializeAsync(options).ContinueWith(task => onSuccess()); + } + catch (Exception exception) + { + onError(exception.Message); + } + } + private void OnSuccess() + { + LoggerUtils.Debug("[iap] Congratulations!\nUnity Gaming Services has been successfully initialized."); + _serviceInit = true; + IAPEvent.LogPurchaseInit(); + } + + private void OnError(string message) + { + LoggerUtils.Debug($"[iap] Unity Gaming Services failed to initialize with error: {message}."); + } + + /// + /// 初始化IAP + /// + public void Initialize() + { + LoggerUtils.Debug("[iap] IAP Initialize() _storeController.Debug:" + _storeController + " m_StoreExtensionProvider: " + _storeExtensionProvider); + if (_storeController == null && _storeExtensionProvider == null) + InitUnityPurchase(); + } + + private void InitUnityPurchase() + { + LoggerUtils.Debug("[iap] IAP InitUnityPurchase() IsInitialized: " + IsInitialized()); + if (IsInitialized()) return; + + _repeatCountDic = new Dictionary(); + + _module ??= StandardPurchasingModule.Instance(); + + // 配置模式; + _builder ??= ConfigurationBuilder.Instance(_module); + + int productsNum = 0; + + if (_initProductDic != null && _initProductDic.Count > 0) + { + foreach (string tID in _initProductDic.Keys) + { + productsNum++; + if (!string.IsNullOrEmpty(tID)) + { + LoggerUtils.Debug($"[iap] Add InitProductDic APProducts: {tID}"); + _builder.AddProduct(tID, _initProductDic[tID]); + } + } + } + + if (_addProductsDic != null && _addProductsDic.Count > 0) + { + foreach (string tID in _addProductsDic.Keys) + { + productsNum++; + if (!string.IsNullOrEmpty(tID)) + { + LoggerUtils.Debug($"[iap] Add AddProductsDic IAPProducts: {tID}"); + _builder.AddProduct(tID, _addProductsDic[tID]); + } + } + } + + _googlePlayConfiguration = _builder.Configure(); + if (productsNum > 0) + { + UnityPurchasing.Initialize(this, _builder); + } + else + { + LoggerUtils.Debug( + "[iap] UnityPurchasing will not initialize.Products is empty,please add product."); + } + } + + private bool IsInitialized() + { + return _storeController != null && _storeExtensionProvider != null; + } + #endregion + + #region 初始化成功 + public void OnInitialized(IStoreController controller, IExtensionProvider extensions) + { + LoggerUtils.Debug("[iap] IAP initialize Succeed!"); + + _storeController = controller; + _storeExtensionProvider = extensions; + + RushSDKManager.Instance.OnGetProductsInfo?.Invoke(_storeController.products.all); + foreach (var product in _storeController.products.all) + { + LoggerUtils.Debug("[iap] " + product.metadata.localizedTitle + + "|" + product.metadata.localizedPriceString + + "|" + product.metadata.localizedDescription + + "|" + product.metadata.isoCurrencyCode + + "|" + product.definition.id + + "|" + product.definition.type); + } + + // CheckSubscribeReceipt(); + RushSDKManager.Instance.OnGetProductsInfo?.Invoke(_storeController.products.all); + RushSDKManager.Instance.OnPurchaseInitComplete?.Invoke(true, ""); + IAPEvent.LogPurchaseInit(true); + + } + #endregion + + #region 初始化失败 + public void OnInitializeFailed(InitializationFailureReason error) + { + OnInitializeFailed(error, ""); + RushSDKManager.Instance.OnPurchaseInitComplete?.Invoke(false, error.ToString()); + } + + public void OnInitializeFailed(InitializationFailureReason error, string message) + { + LoggerUtils.Debug("[iap] IAP OnInitializeFailed error:" + error.ToString() + " msg:" + message); + switch (error) + { + case InitializationFailureReason.AppNotKnown: + LoggerUtils.Debug("[iap] Is your App correctly uploaded on the relevant publisher console?"); + break; + case InitializationFailureReason.PurchasingUnavailable: + LoggerUtils.Debug("[iap] Billing disabled! Ask the user if billing is disabled in device settings."); + break; + case InitializationFailureReason.NoProductsAvailable: + LoggerUtils.Debug("[iap] No products available for purchase! Developer configuration error; check product metadata!"); + break; + } + IAPEvent.LogPurchaseInit(false, error.ToString()); + } + #endregion + + #region 购买失败 + public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription) + { + LoggerUtils.Debug("[iap] OnPurchaseFailed productId : " + failureDescription.productId + + " , transactionID : " + product.transactionID + + " , localizedPrice : " + product.metadata.localizedPriceString + + " , isoCurrencyCode : " + product.metadata.isoCurrencyCode + + "failureDescription" + failureDescription.message); + + + // 失败打点 + IAPEvent.LogPurchaseFail(new PurchaseInfo( + productName: product.metadata.localizedTitle, + productID: product.definition.id, + orderID: "", + currency: product.metadata.isoCurrencyCode, + price: product.metadata.localizedPriceString, + gameExtra: _gameExtraParam, + failReason: failureDescription.message, + orderAlreadyExists: false, + purchaseResult: false, + resultType: (IAPResultType)failureDescription.reason, + productType: product.definition.type.ToString() + )); + + // 购买失败回调 + RushSDKManager.Instance.OnPurchaseComplete(new PurchaseInfo( + productName: product.metadata.localizedTitle, + productID: product.definition.id, + orderID: "", + currency: product.metadata.isoCurrencyCode, + price: product.metadata.localizedPriceString, + gameExtra: _gameExtraParam, + failReason: failureDescription.message, + orderAlreadyExists: false, + purchaseResult: false, + resultType: (IAPResultType)failureDescription.reason, + productType: product.definition.type.ToString() + )); + } + + public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason) + { + // m_PurchaseInProgress = false; + LoggerUtils.Debug("[iap] OnPurchaseFailed productId -> : " + product.definition.id + + " , transactionID : " + product.transactionID + + " , localizedPrice : " + product.metadata.localizedPriceString + + " , isoCurrencyCode : " + product.metadata.isoCurrencyCode + + " , failureReason : " + failureReason.ToString()); + + + // 失败打点 + IAPEvent.LogPurchaseFail(new PurchaseInfo( + productName: product.metadata.localizedTitle, + productID: product.definition.id, + orderID: "", + currency: product.metadata.isoCurrencyCode, + price: product.metadata.localizedPriceString, + gameExtra: _gameExtraParam, + productType: "", + failReason: failureReason.ToString(), + orderAlreadyExists: false, + purchaseResult: false, + resultType: (IAPResultType)failureReason) + ); + + // 购买失败回调 + RushSDKManager.Instance.OnPurchaseComplete(new PurchaseInfo( + productName: product.metadata.localizedTitle, + productID: product.definition.id, + orderID: "", + currency: product.metadata.isoCurrencyCode, + price: product.metadata.localizedPriceString, + gameExtra: _gameExtraParam, + failReason: failureReason.ToString(), + orderAlreadyExists: false, + purchaseResult: false, + resultType: (IAPResultType)failureReason, + productType: product.definition.type.ToString() + + )); + // ReadFailOrderId(); + } + #endregion + + #region Properties + private StandardPurchasingModule _module; + private ConfigurationBuilder _builder; + private static IStoreController _storeController; + private static IExtensionProvider _storeExtensionProvider; + public string _gameExtraParam = ""; + private static string _productName = ""; + private bool _inPurchaseProgress = false; + public Dictionary> _productArgs = new Dictionary>(); + private bool _serviceInit = false; + private Dictionary _addProductsDic; + private Dictionary _initProductDic; + private Dictionary _repeatCountDic; + private IGooglePlayConfiguration _googlePlayConfiguration; + private bool _isFetchingAdditionalProducts = false; + #endregion + } +} + +#endif \ No newline at end of file diff --git a/Assets/Script/SDKManager/Purchase/IAPPurchaseManager.cs.meta b/Assets/Script/SDKManager/Purchase/IAPPurchaseManager.cs.meta new file mode 100644 index 0000000..e540c7d --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPPurchaseManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffce1a3398ade48a9907b1f3ebb82f8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/IAPSubscribeInfo.cs b/Assets/Script/SDKManager/Purchase/IAPSubscribeInfo.cs new file mode 100644 index 0000000..7c070c4 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPSubscribeInfo.cs @@ -0,0 +1,44 @@ +#if UNITY_PURCHASE +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace WZ +{ + + public struct SubscribeInfo + { + + public int Illegal_order; // 0:代表合法订单,1:代表非法订单 + public string Illegal_msg; // 非法订单信息 + public string Environment; // production & sandbox + public string Purchase_time; //订阅时间,单位毫秒 + public int Is_subscribed; //是否订阅过 0:未订阅过,1:订阅过 + public int Is_expired; //是否过期 0未过期,1:已过期 + public int Is_cancelled; // 0:未取消,1:已取消 + public int Is_free_trial; // 0:不是免费试用,1:是免费试用 + public int Is_auto_renewing; //是否自动续订 0:非自动,1:自动 + public string Remaining_time; //订阅到期剩余时间,单位毫秒 + public string Expiry_time; //过期时间,单位毫秒 + public string Latest_order_id; //当前订阅的最新订单号 + public string Product_id; //产品ID + + public SubscribeInfo(int illegal_order, string illegal_msg, string environment, string purchase_time, int is_subscribed, int is_expired, int is_cancelled, int is_free_trial, int is_auto_renewing, string remaining_time, string expiry_time, string latest_order_id,string product_id) + { + Illegal_order = illegal_order; + Illegal_msg = illegal_msg; + Environment = environment; + Purchase_time = purchase_time; + Is_subscribed = is_subscribed; + Is_expired = is_expired; + Is_cancelled = is_cancelled; + Is_free_trial = is_free_trial; + Is_auto_renewing = is_auto_renewing; + Remaining_time = remaining_time; + Expiry_time = expiry_time; + Latest_order_id = latest_order_id; + Product_id = product_id; + } + } +} +#endif \ No newline at end of file diff --git a/Assets/Script/SDKManager/Purchase/IAPSubscribeInfo.cs.meta b/Assets/Script/SDKManager/Purchase/IAPSubscribeInfo.cs.meta new file mode 100644 index 0000000..9d8f9a7 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/IAPSubscribeInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33709546096a84c849505413000f3a28 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/Server.meta b/Assets/Script/SDKManager/Purchase/Server.meta new file mode 100644 index 0000000..ccd37cc --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Server.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1edbb658f8a39411aa0d6508c05142b3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/Server/RequestHandler.cs b/Assets/Script/SDKManager/Purchase/Server/RequestHandler.cs new file mode 100644 index 0000000..1e3ca73 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Server/RequestHandler.cs @@ -0,0 +1,113 @@ +#if UNITY_PURCHASE +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace WZ +{ + + public class RequestHandler : MonoBehaviour + { + private static RequestHandler _instance; + + public static RequestHandler Instance + { + get + { + if (_instance != null) return _instance; + _instance = FindObjectOfType(); + if (_instance != null) return _instance; + var obj = new GameObject(); + _instance = obj.AddComponent(); + return _instance; + } + } + + private void Awake() + { + if (_instance == null) + { + _instance = this; + } + else if (_instance != this) + { + Destroy(gameObject); + } + + DontDestroyOnLoad(gameObject); + } + + public void SendPostRequest(string url, string jsonRequestBody, Dictionary headers = null, Action callback = null) + { + StartCoroutine(PostRequestCoroutine(url, jsonRequestBody, headers, callback)); + } + + private static IEnumerator PostRequestCoroutine(string url, string jsonRequestBody, Dictionary headers, Action callback) + { + using var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST); + request.timeout = 15; + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Content-Type", "application/json"); + + if (!string.IsNullOrEmpty(jsonRequestBody)) + { + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonRequestBody)); + request.uploadHandler.contentType = "application/json"; + } + + if (headers != null) + { + foreach (var header in headers) + { + request.SetRequestHeader(header.Key, header.Value); + } + } +#if UNITY_EDITOR || DEVELOPMENT_BUILD + var startTime = Time.realtimeSinceStartup; +#endif + yield return request.SendWebRequest(); + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + var endTime = Time.realtimeSinceStartup; + var sb = new StringBuilder(); + sb.AppendLine($"POST Request URL: {url}"); + sb.AppendLine($"POST Request Headers: {GetHeadersAsString(headers)}"); + sb.AppendLine($"POST Request Body: {jsonRequestBody}"); + sb.AppendLine($"Response Code: {request.responseCode}"); + sb.AppendLine($"Response Time: {endTime - startTime:F2} seconds"); + sb.AppendLine($"Response Headers: \n {GetHeadersAsString(request.GetResponseHeaders())}"); + var downloadHandlerText = request.downloadHandler != null ? request.downloadHandler.text : ""; + sb.AppendLine($"Response: {downloadHandlerText}"); +#endif + + + if (request.result == UnityWebRequest.Result.Success) + { + callback?.Invoke(0, request.downloadHandler?.text); + } + else + { + callback?.Invoke(-1, request.error ?? $"request fail, result = {request.result}"); + } + + request.disposeDownloadHandlerOnDispose = true; + request.disposeUploadHandlerOnDispose = true; + } + + private static string GetHeadersAsString(Dictionary headers) + { + if (headers == null || headers.Count == 0) + { + return "\tN/A"; + } + + return headers.Aggregate("", (current, header) => current + "\t" + header.Key + ": " + header.Value + "\n"); + } + } +} + +#endif \ No newline at end of file diff --git a/Assets/Script/SDKManager/Purchase/Server/RequestHandler.cs.meta b/Assets/Script/SDKManager/Purchase/Server/RequestHandler.cs.meta new file mode 100644 index 0000000..bef7f49 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Server/RequestHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d0a1895b264d84fb888ce190870a1fc7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/Purchase/Server/ServerMgr.cs b/Assets/Script/SDKManager/Purchase/Server/ServerMgr.cs new file mode 100644 index 0000000..9e180e3 --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Server/ServerMgr.cs @@ -0,0 +1,272 @@ +#if UNITY_PURCHASE +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Google.MiniJSON; +using Newtonsoft.Json; +using ThinkingData.Analytics; +using UnityEngine; + +namespace WZ +{ + public class ServerMgr : D_MonoSingleton + { + private const string XXTEA_KEY = "tkbff&(gBUjX#$s0710"; + private const string secretKey = "tk~!@#$%^&*()_+0708"; + + public void CheckOrder(Dictionary args, Action callback) + { + Post(StaticValue.CheckOrderUrl, args,callback); + } + + public void VerifyPurchase(Dictionary args, Action callback) + { + float fPrice; + if (!float.TryParse(args["price"].ToString(), out fPrice)) + { + fPrice = 0.0f; + } + var ssProperties = new Dictionary + { + { "is_first", PlayerPrefsUtils.GetPlayerPrefsInt("First_Purchase", 0) == 0 }, + { "IAP", args["iap_name"].ToString() }, + { "product_id", args["product_id"].ToString() }, + { "payment_type", "GooglePlay" }, + { "Price", fPrice } + }; + try + { + if (TDAnalytics.GetSuperProperties().Count > 0) + { + ssProperties = ssProperties.Concat(TDAnalytics.GetSuperProperties()).ToDictionary(postParK => postParK.Key, PostParV => PostParV.Value); + } + + if (TDAnalytics.GetPresetProperties().ToDictionary().Count > 0) + { + ssProperties = ssProperties.Concat(TDAnalytics.GetPresetProperties().ToDictionary()).ToDictionary(postParK => postParK.Key, PostParV => PostParV.Value); + } + } + catch + { + + } + args.Add("ss_super_properties", JsonConvert.SerializeObject(ssProperties)); + Post(StaticValue.VerifyPurchaseUrl, args, callback); + } + + public void Post(string url, Dictionary args, Action callback, + Dictionary headers = null) where T : new() + { + args ??= new Dictionary(); + + headers ??= new Dictionary(); + + AddBaseParameters(args); + + EncryptionParameters(args); + var requestBody = ConvertDictionaryToJson(args); + LoggerUtils.Debug("[server] url=> " + url +" requestBody=>"+requestBody); + var encryptBody = EncryptRequestBody(requestBody); + if (!string.IsNullOrEmpty(encryptBody)) + { + headers["params"] = encryptBody; + headers["encrypt"] = true.ToString().ToLower(); + requestBody = ""; + } + headers.AddIfNotExists("Is-Dev", "0"); + + try + { + headers.AddIfNotExists("unity-platform", Application.platform.ToString()); + } + catch (Exception e) + { + // ignored + } + + RequestHandler.Instance.SendPostRequest(url, requestBody, headers, + (code, res) => + { + LoggerUtils.Debug($"[server] res ====> code : {code} res : {res}"); + if (code == 0) + { + Response resp = null; + var errorMsg = ""; + + try + { + res = DecryptResponseBody(res); + resp = JsonUtility.FromJson>(res); + } + catch (Exception e) + { + errorMsg = $"[server] Data conversion exception。{e.Message} {res}"; + LoggerUtils.Debug(errorMsg); + } + + LoggerUtils.Debug($"[server] resp ====> code : {resp.code} res : {resp.data}"); + if (resp != null) + { + callback(resp.code, resp.msg, resp.data); + } + else + { + callback.Invoke(-1, errorMsg, new T()); + } + } + else + { + callback.Invoke(code, res, new T()); + } + + LoggerUtils.Debug("[server]" + "[res] " + res); + }); + } + + private static string EncryptRequestBody(string requestBody) + { + var encryptRequestBodyBytes = XXTEA.Encrypt(Encoding.UTF8.GetBytes(requestBody), Encoding.UTF8.GetBytes(XXTEA_KEY)); + var encryptRequestBody = EncryptionUtils.BytesToHexString(encryptRequestBodyBytes, false); + LoggerUtils.Debug($"[[server]] [EncryptRequestBody] requestBody = {requestBody} encryptRequestBody = {encryptRequestBody}"); + return encryptRequestBody; + } + + private static string DecryptResponseBody(string responseBody) + { + var decryptResponseBodyBytes = XXTEA.Decrypt(EncryptionUtils.HexStringToBytes(responseBody), Encoding.UTF8.GetBytes(XXTEA_KEY)); + var decryptResponseBody = Encoding.UTF8.GetString(decryptResponseBodyBytes); + LoggerUtils.Debug($"[[server]] [DecryptResponseBody] decryptResponseBody = {decryptResponseBody}"); + return decryptResponseBody; + } + + public static string GetMD5Hash(string input) + { + using (var md5 = MD5.Create()) + { + var inputBytes = Encoding.ASCII.GetBytes(input); + var hashBytes = md5.ComputeHash(inputBytes); + + var builder = new StringBuilder(); + foreach (var t in hashBytes) + { + builder.Append(t.ToString("x2")); // Convert byte to hexadecimal string + } + + return builder.ToString(); + } + } + + private static void EncryptionParameters(Dictionary args) + { + var signString = + $"{secretKey}platform={args["platform"]}packagename={args["package_name"]}channel={args["channel"]}appversion={args["app_version"]}appversioncode={args["app_version_code"]}language={args["language"]}ip={args["ip"]}ts={args["ts"]}"; + var sign = GetMD5Hash(signString); + args["sign"] = sign; + } + + private static void AddBaseParameters(IDictionary args) + { + + args.AddIfNotExists("unity_sdk_version", RushSDKManager.GetSDKVersion()); + + args.AddIfNotExists("package_name", Application.identifier); + args.AddIfNotExists("platform", "android"); + args.AddIfNotExists("platform_os", "android"); + args.AddIfNotExists("channel", "googleplay"); + args.AddIfNotExists("device_type", "Android"); + args.AddIfNotExists("platform_channel", "gp"); + args.AddIfNotExists("app_version", Application.version); + args.AddIfNotExists("app_version_code", DataUtils.AndroidVersionCode()); + args.AddIfNotExists("language", "ZH"); + args.AddIfNotExists("ip", ""); + args.AddIfNotExists("device_id", AdjustManager.Instance.GetGdid()); + args.AddIfNotExists("adjust_adid", AdjustManager.Instance.GetAdid()); + args.AddIfNotExists("app_u8id", ""); + args.AddIfNotExists("app_name", Application.productName); + args.AddIfNotExists("model", ""); + args.AddIfNotExists("screen_size", ""); + args.AddIfNotExists("network_type", ""); + args.AddIfNotExists("ua", ""); + args.AddIfNotExists("idfa", ""); + args.AddIfNotExists("idfv", ""); + args.AddIfNotExists("gaid", AdjustManager.Instance.GetGdid()); + args.AddIfNotExists("oaid", ""); + args.AddIfNotExists("android_id", ""); + args.AddIfNotExists("adid", AdjustManager.Instance.GetAdid()); + args.AddIfNotExists("fire_adid", ""); + args.AddIfNotExists("ad_network", AdjustManager.Instance.GetAdjustNetwork()); + args.AddIfNotExists("campaign", AdjustManager.Instance.GetAdjustCampaign()); + args.AddIfNotExists("adgroup", AdjustManager.Instance.GetAdjustAdgroup()); + args.AddIfNotExists("creative", AdjustManager.Instance.GetAdjustCreative()); + args.AddIfNotExists("clickLabel", AdjustManager.Instance.GetAdjustClickLabel()); + args.AddIfNotExists("referrer", ""); + args.AddIfNotExists("memory", ""); + args.AddIfNotExists("memory_usage", ""); + args.AddIfNotExists("country", ""); + args.AddIfNotExists("user_id", ""); + args.AddIfNotExists("user_type", ""); + args.AddIfNotExists("ss_distinct_id", TDAnalytics.GetDistinctId() ?? ""); + args.AddIfNotExists("ss_account_id", ""); + args.AddIfNotExists("ts", "" + TimeUtils.CurrentTimestamp()); + } + + + public static string ConvertDictionaryToJson(Dictionary dictionary) + { + return Json.Serialize(dictionary); + } + + [Serializable] + public class Response + { + public int code = -1; + public string msg; + public T data; + public int ts; + } + + [Serializable] + public class SensitiveDataInfo + { + public bool has_sensitive; + public string content; + } + + [Serializable] + public class TranslateDataInfo + { + public string content; + } + + [Serializable] + public class DataInfo + { + public string environment; + public int illegal_order = -1; + public string illegal_msg; + } + + [Serializable] + public class SubscriptionInfo + { + public int illegal_order; + public string illegal_msg; + public string environment; + public string purchase_time; + public int is_subscribed; + public int is_expired; + public int is_cancelled; + public int is_free_trial; + public int is_auto_renewing; + public string remaining_time; + public string expiry_time; + public string latest_order_id; + } + } + +} + +#endif \ No newline at end of file diff --git a/Assets/Script/SDKManager/Purchase/Server/ServerMgr.cs.meta b/Assets/Script/SDKManager/Purchase/Server/ServerMgr.cs.meta new file mode 100644 index 0000000..5a67fde --- /dev/null +++ b/Assets/Script/SDKManager/Purchase/Server/ServerMgr.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af510513690fd4631b3b57b1d427f4c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SDKManager/RushSDKManager.cs b/Assets/Script/SDKManager/RushSDKManager.cs index 37f6227..ad468b5 100644 --- a/Assets/Script/SDKManager/RushSDKManager.cs +++ b/Assets/Script/SDKManager/RushSDKManager.cs @@ -67,6 +67,14 @@ public class RushSDKManager : D_MonoSingleton OnGetProductsInfo += _action; } + /// + /// 支付回调 + /// + public Action OnPurchaseComplete; + public void RegisterPurchaseCompletionHandler(Action _action) + { + OnPurchaseComplete = _action; + } #endregion #endif diff --git a/Assets/Script/Test.cs b/Assets/Script/Test.cs index 2499730..c610a7d 100644 --- a/Assets/Script/Test.cs +++ b/Assets/Script/Test.cs @@ -27,16 +27,19 @@ public class Test : MonoBehaviour public void OnShowAd() { + // RushSDKManager.Instance.ShowBanner(); // RushSDKManager.Instance.InitializeSdk(null, true); // KwaiAdsManager.Instance.ShowRewardAd(); - AdsSDKManager.Instance.ShowRewardAd("getcoin",(state,revenue)=> - { - LoggerUtils.Debug("[kwai] OnRewardedVideoAdShowed revenue:"+revenue+"+ state:"+state); - }); + // AdsSDKManager.Instance.ShowRewardAd("getcoin",(state,revenue)=> + // { + // LoggerUtils.Debug("[kwai] OnRewardedVideoAdShowed revenue:"+revenue+"+ state:"+state); + // }); + RushSDKManager.Instance.ShowBanner(BannerType.Standard,BannerAlignType.CenterTop); } public void OnShowInterstitial() { + RushSDKManager.Instance.ShowBanner(BannerType.Collapsible,BannerAlignType.CenterBottom); // KwaiAdsManager.Instance.OnInterstitialCallback(); // var small = gameObject.transform.Find("NativeAd-small").GetComponent(); // var medium = gameObject.transform.Find("NativeAd-medium").GetComponent(); @@ -63,11 +66,11 @@ public class Test : MonoBehaviour // { // Debug.LogWarning("luojian admob native ad start show fail,not ready."); // } - AdsSDKManager.Instance.ShowInterstitialAd("endgame",IvType.IV1, (revenue) => - { - LoggerUtils.Debug("oninter show call revenue:"+revenue); + // AdsSDKManager.Instance.ShowInterstitialAd("endgame",IvType.IV1, (revenue) => + // { + // LoggerUtils.Debug("oninter show call revenue:"+revenue); - }); + // }); } public void OnInterShow() @@ -80,13 +83,15 @@ public class Test : MonoBehaviour } public void ToponDebug() - { - ATSDKAPI.showDebuggerUI(); + { + // ATSDKAPI.showDebuggerUI(); + RushSDKManager.Instance.HideBanner(BannerType.Standard); } public void MaxDebug() { - MaxSdk.ShowMediationDebugger(); + // MaxSdk.ShowMediationDebugger(); + RushSDKManager.Instance.HideBanner(BannerType.Collapsible); } public void Native1Show() diff --git a/Assets/Script/Utils/DataUtils.cs b/Assets/Script/Utils/DataUtils.cs index c45068c..a3dc04e 100644 --- a/Assets/Script/Utils/DataUtils.cs +++ b/Assets/Script/Utils/DataUtils.cs @@ -13,13 +13,23 @@ namespace WZ public T[] items; } + public static int AndroidVersionCode() + { + AndroidJavaClass contextCls = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); + AndroidJavaObject context = contextCls.GetStatic("currentActivity"); + AndroidJavaObject packageMngr = context.Call("getPackageManager"); + string packageName = context.Call("getPackageName"); + AndroidJavaObject packageInfo = + packageMngr.Call("getPackageInfo", packageName, 0); + return packageInfo.Get("versionCode"); + } public static T[] FromJsonArray(string json) { string wrappedJson = $"{{\"items\":{json}}}"; Wrapper wrapper = JsonUtility.FromJson>(wrappedJson); return wrapper.items; } - + public static double StringToDouble(string str) { double result = 0; @@ -33,6 +43,12 @@ namespace WZ } return result; } + + public static void AddIfNotExists(this IDictionary dictionary, TKey key, TValue value) + { + if (!dictionary.ContainsKey(key)) + dictionary.Add(key, value); + } } } diff --git a/Assets/Script/Utils/EncryptionUtils.cs b/Assets/Script/Utils/EncryptionUtils.cs index b435a40..b7ce651 100644 --- a/Assets/Script/Utils/EncryptionUtils.cs +++ b/Assets/Script/Utils/EncryptionUtils.cs @@ -9,6 +9,86 @@ namespace WZ { public static class EncryptionUtils { + public static string BytesToHexString(byte[] bytes, bool isUpperCase) + { + if (bytes == null) + return ""; + + char[] hexDigits = isUpperCase ? HEX_DIGITS_UPPER : HEX_DIGITS_LOWER; + int len = bytes.Length; + if (len == 0) + return ""; + + char[] ret = new char[len << 1]; + for (int i = 0, j = 0; i < len; i++) + { + ret[j++] = hexDigits[bytes[i] >> 4 & 0x0f]; + ret[j++] = hexDigits[bytes[i] & 0x0f]; + } + + return new string(ret); + } + + public static byte[] HexStringToBytes(string hexString) + { + if (IsSpace(hexString)) + return new byte[0]; + + int len = hexString.Length; + if (len % 2 != 0) + { + hexString = "0" + hexString; + len = len + 1; + } + + char[] hexBytes = hexString.ToUpper().ToCharArray(); + byte[] ret = new byte[len >> 1]; + for (int i = 0; i < len; i += 2) + { + ret[i >> 1] = (byte)(HexToDec(hexBytes[i]) << 4 | HexToDec(hexBytes[i + 1])); + } + + return ret; + } + + public static bool IsSpace(string s) + { + if (s == null) + return true; + + for (int i = 0, len = s.Length; i < len; ++i) + { + if (!char.IsWhiteSpace(s[i])) + { + return false; + } + } + + return true; + } + + private static int HexToDec(char hexChar) + { + if (hexChar >= '0' && hexChar <= '9') + { + return hexChar - '0'; + } + else if (hexChar >= 'A' && hexChar <= 'F') + { + return hexChar - 'A' + 10; + } + else + { + throw new ArgumentException(); + } + } + + private static readonly char[] HEX_DIGITS_UPPER = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + private static readonly char[] HEX_DIGITS_LOWER = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static string GetKey(string packageName) { var keyBytes = Encoding.UTF8.GetBytes(packageName); diff --git a/Assets/Script/Utils/TimeUtils.cs b/Assets/Script/Utils/TimeUtils.cs index da2b641..27f0541 100644 --- a/Assets/Script/Utils/TimeUtils.cs +++ b/Assets/Script/Utils/TimeUtils.cs @@ -5,7 +5,10 @@ namespace WZ public class TimeUtils { #region 本地时间 - + public static long CurrentTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } // 获取当前本地时间的毫秒级时间戳 public static long GetLocalTimestamp() {