新增内购

This commit is contained in:
juncong lee 2025-09-18 18:30:57 +08:00
parent c89fe09c1a
commit 5201363037
29 changed files with 2020 additions and 16 deletions

View File

@ -121,6 +121,14 @@ namespace WZ
{ {
StaticValue.AdmobCollapsibleBannerId = valueTemp; 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()) else if (_configs[i].Key.ToLower() == KEY_Admob_NormalBannerId.ToLower())
{ {
StaticValue.AdmobNormalBannerId = valueTemp; 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_CollapsibleBannerId = "Admob_Banner_ID1";
public const string KEY_Admob_NormalBannerId = "Admob_Banner_ID2"; public const string KEY_Admob_NormalBannerId = "Admob_Banner_ID2";
public const string KEY_Admob_SplashId = "Admob_APPOPEN_ID"; public const string KEY_Admob_SplashId = "Admob_APPOPEN_ID";

View File

@ -7,6 +7,8 @@ namespace WZ
public static class StaticValue public static class StaticValue
{ {
public static string CheckOrderUrl = "";
public static string VerifyPurchaseUrl = "";
public static string AdmobCollapsibleBannerId = ""; public static string AdmobCollapsibleBannerId = "";
public static string AdmobNormalBannerId = ""; public static string AdmobNormalBannerId = "";

View File

@ -5,6 +5,8 @@ using System.Threading.Tasks;
using AdjustSdk; using AdjustSdk;
using ThinkingData.Analytics; using ThinkingData.Analytics;
using Unity.VisualScripting; using Unity.VisualScripting;
using Unity.VisualScripting.Antlr3.Runtime.Tree;
using UnityEditor.ShaderKeywordFilter;
using UnityEngine; using UnityEngine;
using WZ; using WZ;
@ -18,6 +20,11 @@ public class AdjustManager : D_MonoSingleton<AdjustManager>
private string callbackNetwork = ""; private string callbackNetwork = "";
bool m_start = false; bool m_start = false;
private int callGetTimes = 0; 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() public void Init()
{ {
@ -172,8 +179,37 @@ public class AdjustManager : D_MonoSingleton<AdjustManager>
}); });
RushSDKManager.Instance.OnUserSourceListener?.Invoke(IsOrganic(network), network); 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) private bool IsOrganic(string _network)
{ {

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ebf62044324047e78f251d34185ff48
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ca2f9d75394fd4e859f89e488d422e80
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
public enum IAPDataStateType
{
// 未处理
def,
// 处理中
deal
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0595ab8f5ac6241689d314aec6c7a3ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2aa385284497442cebdff40e44bad707
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<string, object> { { "fail_reason", message } });
ShuShuEvent.Instance.Track(eventName, new Dictionary<string, object> { { "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<string, object>
{
{ "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<string, object>()
{
{ "total_pay_amount", fPrice },
{ "total_pay_times", payTimes }
});
ShuShuEvent.Instance.SetSuperProperties(new Dictionary<string, object>()
{
{ "pay_times", payTimes }
});
PlayerPrefsUtils.SavePlayerPrefsInt(First_Purchase, 1);
ShuShuEvent.Instance.UserSet(new Dictionary<string, object>()
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 273e000ea028247038e5e7f8f756fbc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d2378dd12f2d41c190fc63f6016b4d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<IAPOrderManager>
{
private static string _purchaseOrderMap = "PurchaseOrderMap";
private List<int> NotRetryCode = new List<int> { 0, 430, 900, 901, 902, 903, 904, 905 };
private Dictionary<string, int> 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<IAPDataConfig> list = ES3.Load(_purchaseOrderMap) as List<IAPDataConfig>;
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<IAPDataConfig> list = ES3.Load(_purchaseOrderMap) as List<IAPDataConfig>;
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<IAPDataConfig> tem = new List<IAPDataConfig>
{
_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<IAPDataConfig> list = ES3.Load(_purchaseOrderMap) as List<IAPDataConfig>;
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<IAPDataConfig> tem = new List<IAPDataConfig>
{
_productInfo
};
ES3.Save(_purchaseOrderMap, tem);
ReadFailOrderId();
LoggerUtils.Debug("[iap] IAP SaveVerifyFailOrderId 第一次存储表 添加失败订单 订单个数:" + tem.Count);
}
}
#endregion
#region
public void ReadFailOrderId()
{
if (ES3.KeyExists(_purchaseOrderMap))
{
List<IAPDataConfig> list = ES3.Load(_purchaseOrderMap) as List<IAPDataConfig>;
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<IAPDataConfig> list = ES3.Load(_purchaseOrderMap) as List<IAPDataConfig>;
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<string, string> info;
public IAPDataStateType state;
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c0364ee309a94459af3113c481b3206
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<IAPPurchaseManager>, 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<string, ProductType> products, Action<bool, string> 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<string, object>)MiniJson.JsonDecode(purchaseEvent.purchasedProduct.receipt);
var payload = (string)wrapper["Payload"];
var profileId = _gameExtraParam;
var _productName = "";
var payloadObj = (Dictionary<string, object>)MiniJson.JsonDecode(payload);
var o = (string)payloadObj["json"];
var payloadData = (Dictionary<string, object>)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<string, object>)MiniJson.JsonDecode(payload);
var gpJson = (string)gpDetails["json"];
var tokenJson = (Dictionary<string, object>)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<string, string>
{
{ "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<string, ProductType> ProductDic,
Action<bool, string> onProductsResult = null)
{
if (!IsInitialized())
{
_addProductsDic = ProductDic;
LoggerUtils.Debug("[iap] IAP not init.Now InitUnityPurchase");
InitUnityPurchase();
return;
}
if (_isFetchingAdditionalProducts)
{
LoggerUtils.Debug("[iap] Now fetching additional productsdon't call repeatedly");
if (onProductsResult != null)
{
onProductsResult(false, "Now fetching additional productsdon't call repeatedly");
}
return;
}
_isFetchingAdditionalProducts = true;
if (ProductDic != null)
{
var additional = new HashSet<ProductDefinition>();
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<InitializationFailureReason, string> 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<string> 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}.");
}
/// <summary>
/// 初始化IAP
/// </summary>
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<string, int>();
_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<IGooglePlayConfiguration>();
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<string, Dictionary<string, object>> _productArgs = new Dictionary<string, Dictionary<string, object>>();
private bool _serviceInit = false;
private Dictionary<string, ProductType> _addProductsDic;
private Dictionary<string, ProductType> _initProductDic;
private Dictionary<string, int> _repeatCountDic;
private IGooglePlayConfiguration _googlePlayConfiguration;
private bool _isFetchingAdditionalProducts = false;
#endregion
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ffce1a3398ade48a9907b1f3ebb82f8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 33709546096a84c849505413000f3a28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1edbb658f8a39411aa0d6508c05142b3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<RequestHandler>();
if (_instance != null) return _instance;
var obj = new GameObject();
_instance = obj.AddComponent<RequestHandler>();
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<string, string> headers = null, Action<int, string> callback = null)
{
StartCoroutine(PostRequestCoroutine(url, jsonRequestBody, headers, callback));
}
private static IEnumerator PostRequestCoroutine(string url, string jsonRequestBody, Dictionary<string, string> headers, Action<int, string> 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<string, string> headers)
{
if (headers == null || headers.Count == 0)
{
return "\tN/A";
}
return headers.Aggregate("", (current, header) => current + "\t" + header.Key + ": " + header.Value + "\n");
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0a1895b264d84fb888ce190870a1fc7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<ServerMgr>
{
private const string XXTEA_KEY = "tkbff&(gBUjX#$s0710";
private const string secretKey = "tk~!@#$%^&*()_+0708";
public void CheckOrder(Dictionary<string, object> args, Action<int,string, SubscriptionInfo> callback)
{
Post(StaticValue.CheckOrderUrl, args,callback);
}
public void VerifyPurchase(Dictionary<string, object> args, Action<int, string, DataInfo> callback)
{
float fPrice;
if (!float.TryParse(args["price"].ToString(), out fPrice))
{
fPrice = 0.0f;
}
var ssProperties = new Dictionary<string, object>
{
{ "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<T>(string url, Dictionary<string, object> args, Action<int, string, T> callback,
Dictionary<string, string> headers = null) where T : new()
{
args ??= new Dictionary<string, object>();
headers ??= new Dictionary<string, string>();
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<T> resp = null;
var errorMsg = "";
try
{
res = DecryptResponseBody(res);
resp = JsonUtility.FromJson<Response<T>>(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<string, object> 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<string, object> 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<string, object> dictionary)
{
return Json.Serialize(dictionary);
}
[Serializable]
public class Response<T>
{
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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af510513690fd4631b3b57b1d427f4c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -67,6 +67,14 @@ public class RushSDKManager : D_MonoSingleton<RushSDKManager>
OnGetProductsInfo += _action; OnGetProductsInfo += _action;
} }
/// <summary>
/// 支付回调
/// </summary>
public Action<PurchaseInfo> OnPurchaseComplete;
public void RegisterPurchaseCompletionHandler(Action<PurchaseInfo> _action)
{
OnPurchaseComplete = _action;
}
#endregion #endregion
#endif #endif

View File

@ -27,16 +27,19 @@ public class Test : MonoBehaviour
public void OnShowAd() public void OnShowAd()
{ {
// RushSDKManager.Instance.ShowBanner();
// RushSDKManager.Instance.InitializeSdk(null, true); // RushSDKManager.Instance.InitializeSdk(null, true);
// KwaiAdsManager.Instance.ShowRewardAd(); // KwaiAdsManager.Instance.ShowRewardAd();
AdsSDKManager.Instance.ShowRewardAd("getcoin",(state,revenue)=> // AdsSDKManager.Instance.ShowRewardAd("getcoin",(state,revenue)=>
{ // {
LoggerUtils.Debug("[kwai] OnRewardedVideoAdShowed revenue:"+revenue+"+ state:"+state); // LoggerUtils.Debug("[kwai] OnRewardedVideoAdShowed revenue:"+revenue+"+ state:"+state);
}); // });
RushSDKManager.Instance.ShowBanner(BannerType.Standard,BannerAlignType.CenterTop);
} }
public void OnShowInterstitial() public void OnShowInterstitial()
{ {
RushSDKManager.Instance.ShowBanner(BannerType.Collapsible,BannerAlignType.CenterBottom);
// KwaiAdsManager.Instance.OnInterstitialCallback(); // KwaiAdsManager.Instance.OnInterstitialCallback();
// var small = gameObject.transform.Find("NativeAd-small").GetComponent<RectTransform>(); // var small = gameObject.transform.Find("NativeAd-small").GetComponent<RectTransform>();
// var medium = gameObject.transform.Find("NativeAd-medium").GetComponent<RectTransform>(); // var medium = gameObject.transform.Find("NativeAd-medium").GetComponent<RectTransform>();
@ -63,11 +66,11 @@ public class Test : MonoBehaviour
// { // {
// Debug.LogWarning("luojian admob native ad start show fail,not ready."); // Debug.LogWarning("luojian admob native ad start show fail,not ready.");
// } // }
AdsSDKManager.Instance.ShowInterstitialAd("endgame",IvType.IV1, (revenue) => // AdsSDKManager.Instance.ShowInterstitialAd("endgame",IvType.IV1, (revenue) =>
{ // {
LoggerUtils.Debug("oninter show call revenue:"+revenue); // LoggerUtils.Debug("oninter show call revenue:"+revenue);
}); // });
} }
public void OnInterShow() public void OnInterShow()
@ -81,12 +84,14 @@ public class Test : MonoBehaviour
public void ToponDebug() public void ToponDebug()
{ {
ATSDKAPI.showDebuggerUI(); // ATSDKAPI.showDebuggerUI();
RushSDKManager.Instance.HideBanner(BannerType.Standard);
} }
public void MaxDebug() public void MaxDebug()
{ {
MaxSdk.ShowMediationDebugger(); // MaxSdk.ShowMediationDebugger();
RushSDKManager.Instance.HideBanner(BannerType.Collapsible);
} }
public void Native1Show() public void Native1Show()

View File

@ -13,6 +13,16 @@ namespace WZ
public T[] items; public T[] items;
} }
public static int AndroidVersionCode()
{
AndroidJavaClass contextCls = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject context = contextCls.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject packageMngr = context.Call<AndroidJavaObject>("getPackageManager");
string packageName = context.Call<string>("getPackageName");
AndroidJavaObject packageInfo =
packageMngr.Call<AndroidJavaObject>("getPackageInfo", packageName, 0);
return packageInfo.Get<int>("versionCode");
}
public static T[] FromJsonArray<T>(string json) public static T[] FromJsonArray<T>(string json)
{ {
string wrappedJson = $"{{\"items\":{json}}}"; string wrappedJson = $"{{\"items\":{json}}}";
@ -33,6 +43,12 @@ namespace WZ
} }
return result; return result;
} }
public static void AddIfNotExists<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
{
if (!dictionary.ContainsKey(key))
dictionary.Add(key, value);
}
} }
} }

View File

@ -9,6 +9,86 @@ namespace WZ
{ {
public static class EncryptionUtils 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) private static string GetKey(string packageName)
{ {
var keyBytes = Encoding.UTF8.GetBytes(packageName); var keyBytes = Encoding.UTF8.GetBytes(packageName);

View File

@ -5,7 +5,10 @@ namespace WZ
public class TimeUtils public class TimeUtils
{ {
#region #region
public static long CurrentTimestamp()
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
// 获取当前本地时间的毫秒级时间戳 // 获取当前本地时间的毫秒级时间戳
public static long GetLocalTimestamp() public static long GetLocalTimestamp()
{ {