+ Opt IAPService OrderData, add orderId, payedDate, productId to tch001 EVENT.
parent
6ef4d0cdec
commit
5a52fc0987
|
|
@ -308,25 +308,34 @@ namespace Guru
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static bool EnableTch02Event { get; set; } = false; // 是否使用太极02事件(请手动开启)
|
public static bool EnableTch02Event { get; set; } = false; // 是否使用太极02事件(请手动开启)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 太极001 IAP收入
|
/// 太极001 IAP收入
|
||||||
/// 每发生一次iap收入,触发一次该事件,value值为本次iap的收入值;
|
/// 每发生一次iap收入,触发一次该事件,value值为本次iap的收入值;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">中台返回地收入值</param>
|
/// <param name="value">中台返回地收入值</param>
|
||||||
public static void Tch001IAPRev(double value)
|
/// <param name="productId"></param>
|
||||||
|
/// <param name="orderId"></param>
|
||||||
|
/// <param name="orderType"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
public static void Tch001IAPRev(double value, string productId, string orderId, string orderType, string timestamp)
|
||||||
{
|
{
|
||||||
TchRevEvent(EventTchAdRev001Impression, IAPPlatform, value);
|
TchRevEvent(EventTchAdRev001Impression, IAPPlatform, value, orderType, productId, orderId, timestamp);
|
||||||
|
|
||||||
if(EnableTch02Event) return; // 如果使用了太极02 则不做FB上报
|
if(EnableTch02Event) return; // 如果使用了太极02 则不做FB上报
|
||||||
FBPurchase(value, USD, "iap", IAPPlatform);
|
FBPurchase(value, USD, "iap", IAPPlatform);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 太极02 IAP 收入打点
|
/// 太极02 IAP 收入打点
|
||||||
/// 发生一次iap收入,触发一次该事件,value值为本次iap的收入值;
|
/// 发生一次iap收入,触发一次该事件,value值为本次iap的收入值;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
|
/// <param name="productId"></param>
|
||||||
|
/// <param name="orderId"></param>
|
||||||
|
/// <param name="orderType"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
// public static void Tch02IAPRev(double value, string productId, string orderId, string orderType, string timestamp)
|
||||||
public static void Tch02IAPRev(double value)
|
public static void Tch02IAPRev(double value)
|
||||||
{
|
{
|
||||||
if (!EnableTch02Event) return;
|
if (!EnableTch02Event) return;
|
||||||
|
|
@ -371,23 +380,38 @@ namespace Guru
|
||||||
//FB标准购买事件
|
//FB标准购买事件
|
||||||
FBPurchase(value, USD, "ads", AdMAX);
|
FBPurchase(value, USD, "ads", AdMAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 太极事件点位上报
|
/// 太极事件点位上报
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="evtName"></param>
|
/// <param name="evtName"></param>
|
||||||
/// <param name="platform"></param>
|
/// <param name="platform"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
private static void TchRevEvent(string evtName, string platform, double value)
|
/// <param name="orderType"></param>
|
||||||
|
/// <param name="productId"></param>
|
||||||
|
/// <param name="orderId"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
private static void TchRevEvent(string evtName, string platform, double value,
|
||||||
|
string orderType = "", string productId = "", string orderId = "", string timestamp = "")
|
||||||
{
|
{
|
||||||
LogEvent(evtName, new Dictionary<string, dynamic>()
|
var data = new Dictionary<string, dynamic>()
|
||||||
{
|
{
|
||||||
{ ParameterAdPlatform, platform },
|
{ ParameterAdPlatform, platform },
|
||||||
{ ParameterCurrency, USD },
|
{ ParameterCurrency, USD },
|
||||||
{ ParameterValue, value },
|
{ ParameterValue, value },
|
||||||
});
|
};
|
||||||
|
|
||||||
|
//--------- Extra data for IAP receipt ---------------
|
||||||
|
if(!string.IsNullOrEmpty(orderType)) data.Add("order_type", orderType);
|
||||||
|
if(!string.IsNullOrEmpty(productId)) data.Add("product_id", productId);
|
||||||
|
if(!string.IsNullOrEmpty(orderId)) data.Add("order_id", orderId);
|
||||||
|
if(!string.IsNullOrEmpty(timestamp)) data.Add("payed_date", timestamp);
|
||||||
|
//--------- Extra data for IAP receipt ---------------
|
||||||
|
|
||||||
|
LogEvent(evtName, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Facebook 支付上报
|
/// Facebook 支付上报
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -528,7 +552,7 @@ namespace Guru
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新用户首次 IAP 付费成功上报 (仅限应用内付费商品,不包含订阅等其它情况)【买量打点】
|
/// 新用户首次 IAP 付费成功上报 (仅限应用内付费商品,不包含订阅等其它情况)【买量打点】
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemName">product_id 商品ID</param>
|
/// <param name="itemName">productId 商品ID</param>
|
||||||
/// <param name="value">付费总金额</param>
|
/// <param name="value">付费总金额</param>
|
||||||
/// <param name="currency">币种</param>
|
/// <param name="currency">币种</param>
|
||||||
public static void FirstIAP(string itemName, double value, string currency)
|
public static void FirstIAP(string itemName, double value, string currency)
|
||||||
|
|
@ -545,7 +569,7 @@ namespace Guru
|
||||||
/// 商品购买成功上报【买量打点】
|
/// 商品购买成功上报【买量打点】
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="productName">商品名称(商品ID一样)</param>
|
/// <param name="productName">商品名称(商品ID一样)</param>
|
||||||
/// <param name="itemName">product_id 商品ID</param>
|
/// <param name="itemName">productId 商品ID</param>
|
||||||
/// <param name="value">付费总金额</param>
|
/// <param name="value">付费总金额</param>
|
||||||
/// <param name="currency">币种</param>
|
/// <param name="currency">币种</param>
|
||||||
public static void ProductIAP(string productName, string itemName, double value, string currency)
|
public static void ProductIAP(string productName, string itemName, double value, string currency)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ namespace Guru
|
||||||
{
|
{
|
||||||
return dateTime.Ticks / TICK_TO_MILLISECOND - STAMP_1970_1_1;
|
return dateTime.Ticks / TICK_TO_MILLISECOND - STAMP_1970_1_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetTimeStampString(DateTime dateTime)
|
||||||
|
{
|
||||||
|
return GetTimeStamp(dateTime).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 时间戳转日期
|
/// 时间戳转日期
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,44 @@
|
||||||
|
|
||||||
|
using PlasticPipe.PlasticProtocol.Messages;
|
||||||
|
|
||||||
namespace Guru
|
namespace Guru
|
||||||
{
|
{
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该数据用于向服务端发送对应的消息
|
||||||
|
/// 同时兼具缓存用户数据
|
||||||
|
/// 详见 <a>https://github.com/castbox/backend-dev/blob/main/saas/%E4%B8%AD%E5%8F%B0%E6%9C%8D%E5%8A%A1%E6%8E%A5%E5%85%A5%E6%89%8B%E5%86%8C.md#553apple-store%E8%AE%A2%E5%8D%95%E4%B8%8A%E6%8A%A5</a>
|
||||||
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class AppleOrderData
|
public class AppleOrderData : BaseOrderData
|
||||||
{
|
{
|
||||||
public string guid;
|
|
||||||
public int orderType;
|
public string bundleId; //应用包名
|
||||||
public string productId;
|
public string receipt; // Apple Store返回的Receipt数据
|
||||||
public string bundleId;
|
public string country; // 用户商店的国家2字大写代码
|
||||||
public string receipt;
|
|
||||||
public string country;
|
public AppleOrderData(int orderType, string productId, string receipt, string orderId,
|
||||||
public int level;
|
string date, int level, string offerId = "", string basePlanId = "") : base(orderType, productId, orderId, date, level, offerId, basePlanId)
|
||||||
public Dictionary<string, object> userInfo;
|
|
||||||
public EventConfig eventConfig;
|
|
||||||
|
|
||||||
public AppleOrderData(int orderType, string productId, string receipt, int level)
|
|
||||||
{
|
{
|
||||||
guid = Guid.NewGuid().ToString();
|
|
||||||
this.orderType = orderType;
|
|
||||||
this.productId = productId;
|
|
||||||
this.receipt = receipt;
|
this.receipt = receipt;
|
||||||
this.level = level;
|
|
||||||
bundleId = GuruSettings.Instance.GameIdentifier;
|
bundleId = GuruSettings.Instance.GameIdentifier;
|
||||||
country = IPMConfig.IPM_COUNTRY_CODE;
|
country = IPMConfig.IPM_COUNTRY_CODE;
|
||||||
userInfo = new Dictionary<string, object> { ["level"] = level };
|
|
||||||
eventConfig = EventConfig.Build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool Equals(AppleOrderData other)
|
public bool Equals(AppleOrderData other)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(guid)) guid = Guid.NewGuid().ToString();
|
if (string.IsNullOrEmpty(guid)) guid = GetGuid();
|
||||||
return guid.Equals(other.guid);
|
return guid.Equals(other.guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public string GetId() => productId;
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{nameof(orderType)}: {orderType}, {nameof(productId)}: {productId}, {nameof(bundleId)}: {bundleId}, {nameof(receipt)}: {receipt}, {nameof(country)}: {country}";
|
return "AppleOrderData: " + base.ToString() + $", {nameof(bundleId)}: {bundleId}, {nameof(receipt)}: {receipt}, {nameof(country)}: {country}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToJson() => JsonConvert.SerializeObject(this);
|
public string ToJson() => JsonConvert.SerializeObject(this);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace Guru
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
public class BaseOrderData
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public string guid; // 唯一标识
|
||||||
|
public string productId; // 商品ID 当orderType=0时,传递该参数
|
||||||
|
[JsonIgnore]
|
||||||
|
public int level; // 关卡 ID
|
||||||
|
|
||||||
|
public int orderType; // 订单类型,可选值:0:IAP 订单 1:SUB 订阅订单
|
||||||
|
public Dictionary<string, object> userInfo; // 当前用户信息。目前包含: level: 用户属性中的"b_level"的值
|
||||||
|
public EventConfig eventConfig; // 事件打点所需信息
|
||||||
|
|
||||||
|
//---- Offer Data -------
|
||||||
|
[JsonIgnore]
|
||||||
|
public string orderId; // 订单的 OrderID
|
||||||
|
[JsonIgnore]
|
||||||
|
public string payedDate; // 支付时间 (13位时间戳)
|
||||||
|
public string basePlanId; // 订阅商品的planId
|
||||||
|
public string offerId; // 订阅商品的offerId
|
||||||
|
|
||||||
|
|
||||||
|
public BaseOrderData(int orderType, string productId, string orderId,
|
||||||
|
string payedDate, int level, string offerId = "", string basePlanId = "")
|
||||||
|
{
|
||||||
|
guid = GetGuid();
|
||||||
|
this.orderType = orderType;
|
||||||
|
this.productId = productId;
|
||||||
|
this.orderId = orderId;
|
||||||
|
this.payedDate = payedDate;
|
||||||
|
this.basePlanId = basePlanId;
|
||||||
|
this.offerId = offerId;
|
||||||
|
this.level = level;
|
||||||
|
userInfo = new Dictionary<string, object> { ["level"] = level };
|
||||||
|
eventConfig = EventConfig.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetGuid() => Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public string OrderType() => orderType == 0 ? "IAP" : "SUB";
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{nameof(orderType)}: {orderType}, {nameof(productId)}: {productId}, {nameof(orderId)}: {orderId}, {nameof(payedDate)}: {payedDate}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e576b9cd108047ec91aac82aa9aca5c6
|
||||||
|
timeCreated: 1715252456
|
||||||
|
|
@ -5,35 +5,30 @@ namespace Guru
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该数据用于向服务端发送对应的消息
|
||||||
|
/// 同时兼具缓存用户数据
|
||||||
|
/// 详见 <a>https://github.com/castbox/backend-dev/blob/main/saas/%E4%B8%AD%E5%8F%B0%E6%9C%8D%E5%8A%A1%E6%8E%A5%E5%85%A5%E6%89%8B%E5%86%8C.md#552google%E8%AE%A2%E5%8D%95%E4%B8%8A%E6%8A%A5</a>
|
||||||
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class GoogleOrderData
|
public class GoogleOrderData : BaseOrderData
|
||||||
{
|
{
|
||||||
public string guid;
|
private string _productId;
|
||||||
public int orderType; // 订单类型,可选值:0:IAP订单 1: 订阅订单
|
public string subscriptionId; // 订阅道具名称
|
||||||
public string packageName; //应用包名
|
public string packageName; //应用包名
|
||||||
public string productId = ""; // 商品ID 当orderType=0时,传递该参数
|
|
||||||
public string subscriptionId = ""; // 订阅ID // 当orderType=1时,传递该参数
|
|
||||||
public string token; // 应用商店里面的购买token
|
public string token; // 应用商店里面的购买token
|
||||||
public int level;
|
|
||||||
public Dictionary<string, object> userInfo; // 当前用户信息。目前包含: level: 用户属性中的"b_level"的值
|
public GoogleOrderData(int orderType, string productId, string token, string orderId,
|
||||||
public string basePlanId = ""; // 订阅商品的planId
|
string date, int level, string offerId = "", string basePlanId = "") : base(orderType, productId, orderId, date, level, offerId, basePlanId)
|
||||||
public string offerId = ""; // 订阅商品的offerId
|
|
||||||
public EventConfig eventConfig; // 事件打点所需信息
|
|
||||||
|
|
||||||
public GoogleOrderData(int orderType, string productId, string subscriptionId, string token, int level,
|
|
||||||
string offerId = "", string basePlanId = "")
|
|
||||||
{
|
{
|
||||||
guid = Guid.NewGuid().ToString();
|
_productId = productId;
|
||||||
this.orderType = orderType;
|
|
||||||
this.packageName = GuruSettings.Instance.GameIdentifier;
|
this.packageName = GuruSettings.Instance.GameIdentifier;
|
||||||
this.productId = productId;
|
|
||||||
this.subscriptionId = subscriptionId;
|
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.offerId = offerId;
|
if (orderType == 1)
|
||||||
this.basePlanId = basePlanId;
|
{
|
||||||
this.level = level;
|
this.subscriptionId = productId;
|
||||||
userInfo = new Dictionary<string, object> { ["level"] = level };
|
this.productId = "";
|
||||||
eventConfig = EventConfig.Build();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(GoogleOrderData other)
|
public bool Equals(GoogleOrderData other)
|
||||||
|
|
@ -41,12 +36,14 @@ namespace Guru
|
||||||
if (string.IsNullOrEmpty(guid)) guid = Guid.NewGuid().ToString();
|
if (string.IsNullOrEmpty(guid)) guid = Guid.NewGuid().ToString();
|
||||||
return guid == other.guid;
|
return guid == other.guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{nameof(orderType)}: {orderType}, {nameof(packageName)}: {packageName}, {nameof(productId)}: {productId}, {nameof(subscriptionId)}: {subscriptionId}, {nameof(token)}: {token}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToJson() => JsonConvert.SerializeObject(this);
|
public string ToJson() => JsonConvert.SerializeObject(this);
|
||||||
|
|
||||||
|
public string RealProductId => _productId;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "GoogleOrderData: " + base.ToString() + $",{nameof(subscriptionId)}: {subscriptionId}, {nameof(packageName)}: {packageName}, {nameof(token)}: {token}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,26 +13,29 @@ namespace Guru
|
||||||
public int orderType;
|
public int orderType;
|
||||||
public string productId;
|
public string productId;
|
||||||
public string receipt;
|
public string receipt;
|
||||||
public int level;
|
|
||||||
public AppleOrderData orderData;
|
public AppleOrderData orderData;
|
||||||
|
|
||||||
public AppleOrderRequest(){}
|
public AppleOrderRequest(){}
|
||||||
public static AppleOrderRequest Build(int orderType, string productId, string receipt, int level)
|
public static AppleOrderRequest Build(int orderType, string productId, string receipt, string orderId, string date, int level)
|
||||||
{
|
{
|
||||||
var request = new AppleOrderRequest()
|
var request = new AppleOrderRequest()
|
||||||
{
|
{
|
||||||
orderType = orderType,
|
orderType = orderType,
|
||||||
productId = productId,
|
productId = productId,
|
||||||
receipt = receipt,
|
receipt = receipt,
|
||||||
level = level,
|
orderData = new AppleOrderData(orderType, productId, receipt, orderId, date, level),
|
||||||
orderData = new AppleOrderData(orderType, productId, receipt, level),
|
|
||||||
};
|
};
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AppleOrderRequest Build(AppleOrderData orderData)
|
public static AppleOrderRequest Build(AppleOrderData orderData)
|
||||||
{
|
{
|
||||||
return Build(orderData.orderType, orderData.productId, orderData.receipt, orderData.level);
|
return Build(orderData.orderType,
|
||||||
|
orderData.productId,
|
||||||
|
orderData.receipt,
|
||||||
|
orderData.orderId,
|
||||||
|
orderData.payedDate,
|
||||||
|
orderData.level);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string RequestURL => IPMConfig.IPM_URL + "order/api/v1/orders/ios";
|
protected override string RequestURL => IPMConfig.IPM_URL + "order/api/v1/orders/ios";
|
||||||
|
|
@ -57,18 +60,12 @@ namespace Guru
|
||||||
if (responseData != null && responseData.data != null)
|
if (responseData != null && responseData.data != null)
|
||||||
{
|
{
|
||||||
double usdPrice = responseData.data.usdPrice;
|
double usdPrice = responseData.data.usdPrice;
|
||||||
Analytics.Tch001IAPRev(usdPrice);
|
|
||||||
|
Analytics.Tch001IAPRev(usdPrice, productId, orderData.orderId, orderData.OrderType(), orderData.payedDate);
|
||||||
Analytics.Tch02IAPRev(usdPrice);
|
Analytics.Tch02IAPRev(usdPrice);
|
||||||
if (orderType == 0)
|
|
||||||
{
|
AdjustService.TrackSubPurchase(usdPrice, productId);
|
||||||
AdjustService.TrackIAPPurchase(usdPrice, productId);
|
Analytics.SubPurchase(usdPrice, productId);
|
||||||
Analytics.IAPPurchase(usdPrice, productId);
|
|
||||||
}
|
|
||||||
else if (orderType == 1)
|
|
||||||
{
|
|
||||||
AdjustService.TrackSubPurchase(usdPrice, productId);
|
|
||||||
Analytics.SubPurchase(usdPrice, productId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,31 @@
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
using Guru.LitJson;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Networking;
|
|
||||||
|
|
||||||
namespace Guru
|
namespace Guru
|
||||||
{
|
{
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
public class GoogleOrderRequest : RequestBase
|
public class GoogleOrderRequest : RequestBase
|
||||||
{
|
{
|
||||||
public int orderType; // 订单类型,可选值:0:IAP订单 1: 订阅订单
|
|
||||||
public string productId;
|
|
||||||
public string subscriptionId;
|
|
||||||
public string token;
|
public string token;
|
||||||
public string packageName;
|
|
||||||
public int level;
|
|
||||||
public string basePlanId;
|
|
||||||
public string offerId;
|
|
||||||
public GoogleOrderData orderData;
|
public GoogleOrderData orderData;
|
||||||
|
|
||||||
public GoogleOrderRequest(){}
|
public GoogleOrderRequest(){}
|
||||||
|
|
||||||
public GoogleOrderRequest(int orderType, string productId, string subscriptionId,
|
public GoogleOrderRequest(int orderType, string productId, string subscriptionId, string token, string orderId,
|
||||||
string token, int level, string offerId = "", string basePlanId = "")
|
string date, int level, string offerId = "", string basePlanId = "")
|
||||||
{
|
{
|
||||||
this.orderType = orderType;
|
|
||||||
this.packageName = Application.identifier;
|
|
||||||
this.productId = productId;
|
|
||||||
this.subscriptionId = subscriptionId;
|
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.level = level;
|
orderData = new GoogleOrderData(orderType, productId, token, orderId, date, level, offerId, basePlanId);
|
||||||
orderData = new GoogleOrderData(orderType, productId, subscriptionId, token, level, offerId, basePlanId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string RequestURL => IPMConfig.IPM_URL + "order/api/v1/orders/android";
|
protected override string RequestURL => IPMConfig.IPM_URL + "order/api/v1/orders/android";
|
||||||
protected override UnityWebRequest CreateRequest()
|
protected override UnityWebRequest CreateRequest()
|
||||||
{
|
{
|
||||||
GoogleOrderData googleOrderData = orderData;
|
this.Log($"send orderData:{orderData}");
|
||||||
this.Log($"send orderData:{googleOrderData}");
|
|
||||||
var request = new UnityWebRequest(RequestURL, "POST");
|
var request = new UnityWebRequest(RequestURL, "POST");
|
||||||
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonMapper.ToJson(googleOrderData)));
|
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(orderData.ToJson()));
|
||||||
request.downloadHandler = new DownloadHandlerBuffer();
|
request.downloadHandler = new DownloadHandlerBuffer();
|
||||||
request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType);
|
request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType);
|
||||||
request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID);
|
request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID);
|
||||||
|
|
@ -56,24 +43,15 @@ namespace Guru
|
||||||
{
|
{
|
||||||
// Analytics.Tch001IAPRev(responseData.data.usdPrice);
|
// Analytics.Tch001IAPRev(responseData.data.usdPrice);
|
||||||
double usdPrice = responseData.data.usdPrice;
|
double usdPrice = responseData.data.usdPrice;
|
||||||
Analytics.Tch001IAPRev(usdPrice);
|
string productId = orderData.RealProductId;
|
||||||
Analytics.Tch02IAPRev(usdPrice);
|
|
||||||
|
Analytics.Tch001IAPRev(usdPrice, productId, orderData.orderId, orderData.OrderType(), orderData.payedDate); // TCH 001
|
||||||
string pid = "";
|
// Analytics.Tch02IAPRev(usdPrice, productId, orderId, orderTypeString, timestamp);
|
||||||
|
Analytics.Tch02IAPRev(usdPrice); // TCH 020
|
||||||
if (!string.IsNullOrEmpty(productId))
|
|
||||||
{
|
// Adjust Track IAP Purchase
|
||||||
pid = productId;
|
AdjustService.TrackIAPPurchase(usdPrice, productId); // 上报 IAP 支付事件
|
||||||
AdjustService.TrackIAPPurchase(usdPrice, productId); // 上报 IAP 支付事件
|
Analytics.IAPPurchase(usdPrice, productId);
|
||||||
Analytics.IAPPurchase(usdPrice, pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(subscriptionId))
|
|
||||||
{
|
|
||||||
pid = subscriptionId;
|
|
||||||
AdjustService.TrackSubPurchase(usdPrice, productId); // 上报 订阅 支付事件
|
|
||||||
Analytics.SubPurchase(usdPrice, productId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -83,18 +61,13 @@ namespace Guru
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static GoogleOrderRequest Build(int orderType, string productId, string subscriptionId,
|
public static GoogleOrderRequest Build(int orderType, string productId,
|
||||||
string token, int level, string offerId = "", string basePlanId = "")
|
string token, string orderId, string date, int level, string offerId = "", string basePlanId = "")
|
||||||
{
|
{
|
||||||
var request = new GoogleOrderRequest()
|
var request = new GoogleOrderRequest()
|
||||||
{
|
{
|
||||||
orderData = new GoogleOrderData(orderType, productId, subscriptionId, token, level, offerId, basePlanId),
|
orderData = new GoogleOrderData(orderType, productId, token, orderId, date, level, offerId, basePlanId),
|
||||||
orderType = orderType,
|
|
||||||
packageName = Application.identifier,
|
|
||||||
productId = productId,
|
|
||||||
subscriptionId = subscriptionId,
|
|
||||||
token = token,
|
token = token,
|
||||||
level = level,
|
|
||||||
};
|
};
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
@ -104,12 +77,7 @@ namespace Guru
|
||||||
var request = new GoogleOrderRequest()
|
var request = new GoogleOrderRequest()
|
||||||
{
|
{
|
||||||
orderData = data,
|
orderData = data,
|
||||||
orderType = data.orderType,
|
|
||||||
packageName = data.packageName,
|
|
||||||
productId = data.productId,
|
|
||||||
subscriptionId = data.subscriptionId,
|
|
||||||
token = data.token,
|
token = data.token,
|
||||||
level = data.level,
|
|
||||||
};
|
};
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,11 @@ namespace Guru
|
||||||
_showLog = showLog;
|
_showLog = showLog;
|
||||||
InitPurchasing();
|
InitPurchasing();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 带有校验器的初始化
|
/// 带有校验器的初始化
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
/// <param name="googlePublicKey"></param>
|
/// <param name="googlePublicKey"></param>
|
||||||
/// <param name="appleRootCert"></param>
|
/// <param name="appleRootCert"></param>
|
||||||
/// <param name="showLog"></param>
|
/// <param name="showLog"></param>
|
||||||
|
|
@ -213,12 +214,12 @@ namespace Guru
|
||||||
/// <exception cref="NotImplementedException"></exception>
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
|
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
|
||||||
{
|
{
|
||||||
LogI($"--- IAP Initialized Success");
|
|
||||||
_storeController = controller;
|
_storeController = controller;
|
||||||
_storeExtensionProvider = extensions;
|
_storeExtensionProvider = extensions;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_userId)) _userId = IPMConfig.IPM_UID;
|
if (string.IsNullOrEmpty(_userId)) _userId = IPMConfig.IPM_UID;
|
||||||
|
|
||||||
|
LogI($"--- IAP Initialized Success. With UID: {_userId}");
|
||||||
|
|
||||||
#if UNITY_IOS
|
#if UNITY_IOS
|
||||||
_appleExtensions = extensions.GetExtension<IAppleExtensions>();
|
_appleExtensions = extensions.GetExtension<IAppleExtensions>();
|
||||||
_appleExtensions.SetApplicationUsername(_userId); // SetUp UID
|
_appleExtensions.SetApplicationUsername(_userId); // SetUp UID
|
||||||
|
|
@ -712,72 +713,90 @@ namespace Guru
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 支付结果上报
|
/// 支付结果上报
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void ReportPurchaseResult(PurchaseEventArgs args)
|
protected virtual bool ReportPurchaseResult(PurchaseEventArgs args)
|
||||||
{
|
{
|
||||||
int blevel = GetBLevel();
|
// 验证器判定
|
||||||
int orderType = args.purchasedProduct.definition.type == ProductType.Subscription ? 1 : 0;
|
|
||||||
|
|
||||||
|
|
||||||
// Debug.Log($"############ ReportPurchaseResult: _validator:{_validator }");
|
|
||||||
|
|
||||||
if (_validator == null)
|
if (_validator == null)
|
||||||
{
|
{
|
||||||
// Debug.Log($"############ --- Validator is null");
|
// Debug.Log($"############ --- Validator is null");
|
||||||
LogE($"{Tag} --- Validator is null. Report Order failed.");
|
LogE($"{Tag} --- Validator is null. Report Order failed.");
|
||||||
Analytics.LogCrashlytics(new Exception($"IAPService can not report order because Validator is null!"));
|
Analytics.LogCrashlytics(new Exception($"IAPService can not report order because Validator is null!"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
int level = GetBLevel();
|
||||||
|
int orderType = args.purchasedProduct.definition.type == ProductType.Subscription ? 1 : 0;
|
||||||
|
string productId = args.purchasedProduct.definition.id;
|
||||||
|
string appleReceiptString = "";
|
||||||
|
// ----- 支付后的b_level上报逻辑
|
||||||
|
LogI($"--- Report b_level:[{level}] with product id:{args.purchasedProduct.definition.id} ");
|
||||||
|
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
// // Editor 不做上报逻辑
|
||||||
|
LogI($"--- Editor Validate Success. But Editor can't report order.");
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
IPurchaseReceipt[] allReceipts = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// ----- 支付后的b_level上报逻辑
|
#if UNITY_IOS
|
||||||
LogI($"--- Report b_level:[{blevel}] with product id:{args.purchasedProduct.definition.id} ");
|
// ---- iOS 订单验证, 上报打点信息 ----
|
||||||
|
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
// Editor 不做上报逻辑
|
|
||||||
#elif UNITY_ANDROID
|
|
||||||
// Android 订单验证, 上报打点信息
|
|
||||||
var result = _validator.Validate(args.purchasedProduct.receipt);
|
|
||||||
string productID = orderType == 0 ? args.purchasedProduct.definition.id : "";
|
|
||||||
string subscriptionID = orderType == 1 ? args.purchasedProduct.definition.id : "";
|
|
||||||
|
|
||||||
LogI($"{Tag} --- Report Android IAP Order -> orderType:{orderType} productID:{productID} blevel:{blevel}");
|
|
||||||
foreach (var productReceipt in result)
|
|
||||||
{
|
|
||||||
if (productReceipt is GooglePlayReceipt google)
|
|
||||||
{
|
|
||||||
Debug.Log($"{Tag} --- Report Android IAP Order -> orderType:{orderType} productID:{productID} blevel:{blevel}");
|
|
||||||
// new GoogleOrderRequest(orderType, productID, subscriptionID, google.purchaseToken, blevel).SetTimeOut(5).Send();
|
|
||||||
ReportGoogleOrder(orderType, productID, subscriptionID, google.purchaseToken, blevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#elif UNITY_IOS
|
|
||||||
// iOS 订单验证, 上报打点信息
|
|
||||||
var jsonData = JsonConvert.DeserializeObject<JObject>(args.purchasedProduct.receipt);
|
var jsonData = JsonConvert.DeserializeObject<JObject>(args.purchasedProduct.receipt);
|
||||||
if (jsonData.TryGetValue("Payload", out var receipt))
|
if (jsonData != null && jsonData.TryGetValue("Payload", out var recp))
|
||||||
{
|
{
|
||||||
ReportAppleOrder(orderType, args.purchasedProduct.definition.id, receipt.ToString(),blevel);
|
appleReceiptString = recp.ToString();
|
||||||
Debug.Log($"{Tag} --- Report iOS IAP Order -> orderType:{orderType} productID:{args.purchasedProduct.definition.id} blevel:{blevel}");
|
LogI($"--- [{productId}] iOS receipt: {appleReceiptString}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
allReceipts = _validator.Validate(args.purchasedProduct.receipt);
|
||||||
|
|
||||||
|
// ---- Android 订单验证, 上报打点信息 ----
|
||||||
|
string offerId = "";
|
||||||
|
string basePlanId = "";
|
||||||
|
|
||||||
|
foreach (var receipt in allReceipts)
|
||||||
|
{
|
||||||
|
if (receipt == null) continue;
|
||||||
|
|
||||||
|
if (receipt is GooglePlayReceipt googleReceipt)
|
||||||
|
{
|
||||||
|
ReportGoogleOrder(orderType,
|
||||||
|
googleReceipt.productID,
|
||||||
|
googleReceipt.purchaseToken,
|
||||||
|
googleReceipt.orderID,
|
||||||
|
googleReceipt.purchaseDate,
|
||||||
|
level, offerId, basePlanId);
|
||||||
|
}
|
||||||
|
else if (receipt is AppleInAppPurchaseReceipt appleReceipt)
|
||||||
|
{
|
||||||
|
ReportAppleOrder(orderType,
|
||||||
|
appleReceipt.productID,
|
||||||
|
appleReceiptString,
|
||||||
|
appleReceipt.transactionID,
|
||||||
|
appleReceipt.purchaseDate, level);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string exmsg = $"--- [{productId}] :: Unknown Receipt Type: {receipt.GetType()} can't report order.";
|
||||||
|
Analytics.LogCrashlytics(exmsg);
|
||||||
|
LogE(exmsg);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// string receipt = jsonData["Payload"].ToString();
|
|
||||||
// if (HasReceipt(receipt))
|
|
||||||
// {
|
|
||||||
// Debug.Log($"[IAP] Receipt has already reported: {receipt}");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// AddReceipt(receipt);
|
|
||||||
// new AppleOrderRequest(orderType, args.purchasedProduct.definition.id, receipt,blevel).Send();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LogE($" [IAPManager.RevenueUpload] got Exception: {e.Message}");
|
LogE($" [IAPManager.RevenueUpload] got Exception: {ex.Message}");
|
||||||
Analytics.LogCrashlytics(new Exception($"[IAP] Unity report purchase data with b_level={blevel} got error: {e.Message}"));
|
Analytics.LogCrashlytics(new Exception($"[IAP] Unity report purchase data with b_level={level} got error: {ex.Message}"));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogI("--- All Receipt is valid ---");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IOS Orders Collection
|
#region IOS Orders Collection
|
||||||
|
|
@ -903,8 +922,8 @@ namespace Guru
|
||||||
|
|
||||||
private bool isOrderSending = false;
|
private bool isOrderSending = false;
|
||||||
private Queue<RequestBase> _orderRequests = new Queue<RequestBase>(20);
|
private Queue<RequestBase> _orderRequests = new Queue<RequestBase>(20);
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 上报 Google Order Request
|
/// 上报 Google Order Request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -912,13 +931,16 @@ namespace Guru
|
||||||
/// <param name="productId"></param>
|
/// <param name="productId"></param>
|
||||||
/// <param name="subscriptionId"></param>
|
/// <param name="subscriptionId"></param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
|
/// <param name="date"></param>
|
||||||
/// <param name="level"></param>
|
/// <param name="level"></param>
|
||||||
/// <param name="offerId"></param>
|
/// <param name="offerId"></param>
|
||||||
/// <param name="basePlanId"></param>
|
/// <param name="basePlanId"></param>
|
||||||
private void ReportGoogleOrder(int orderType, string productId, string subscriptionId, string token, int level,
|
/// <param name="orderId"></param>
|
||||||
string offerId = "", string basePlanId = "")
|
private void ReportGoogleOrder(int orderType, string productId, string token,
|
||||||
|
string orderId, DateTime date, int level, string offerId = "", string basePlanId = "")
|
||||||
{
|
{
|
||||||
var request = GoogleOrderRequest.Build(orderType, productId, subscriptionId, token, level, offerId, basePlanId);
|
var payedDate = TimeUtil.GetTimeStampString(date);
|
||||||
|
var request = GoogleOrderRequest.Build(orderType, productId, token, orderId, payedDate, level, offerId, basePlanId);
|
||||||
ReportNextOrder(request);
|
ReportNextOrder(request);
|
||||||
}
|
}
|
||||||
private void ReportGoogleOrder(GoogleOrderData data)
|
private void ReportGoogleOrder(GoogleOrderData data)
|
||||||
|
|
@ -927,9 +949,22 @@ namespace Guru
|
||||||
ReportNextOrder(request);
|
ReportNextOrder(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReportAppleOrder(int orderType, string productId, string receipt, int level)
|
/// <summary>
|
||||||
|
/// 上报 Apple Order Request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orderType"></param>
|
||||||
|
/// <param name="productId"></param>
|
||||||
|
/// <param name="receipt"></param>
|
||||||
|
/// <param name="orderId"></param>
|
||||||
|
/// <param name="date"></param>
|
||||||
|
/// <param name="level"></param>
|
||||||
|
/// <param name="offerId"></param>
|
||||||
|
/// <param name="basePlanId"></param>
|
||||||
|
private void ReportAppleOrder(int orderType, string productId, string receipt,
|
||||||
|
string orderId, DateTime date,int level, string offerId = "", string basePlanId = "")
|
||||||
{
|
{
|
||||||
var request = AppleOrderRequest.Build(orderType, productId, receipt, level);
|
var payedDate = TimeUtil.GetTimeStampString(date);
|
||||||
|
var request = AppleOrderRequest.Build(orderType, productId, receipt, orderId, payedDate, level);
|
||||||
ReportNextOrder(request);
|
ReportNextOrder(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -968,26 +1003,26 @@ namespace Guru
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GoogleOrderRequest go = request as GoogleOrderRequest;
|
GoogleOrderRequest googleReq = request as GoogleOrderRequest;
|
||||||
AppleOrderRequest ao = request as AppleOrderRequest;
|
AppleOrderRequest appReq = request as AppleOrderRequest;
|
||||||
|
|
||||||
if (go != null)
|
if (googleReq != null)
|
||||||
{
|
{
|
||||||
if (_model.IsTokenExists(go.token))
|
if (_model.IsTokenExists(googleReq.token))
|
||||||
{
|
{
|
||||||
OnSendNextOrder(); // 跳过上报过的 Google 订单
|
OnSendNextOrder(); // 跳过上报过的 Google 订单
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_model.AddGoogleOrder(go.orderData); // 缓存当前 orderData 等待上报后再消除
|
_model.AddGoogleOrder(googleReq.orderData); // 缓存当前 orderData 等待上报后再消除
|
||||||
}
|
}
|
||||||
else if( ao != null)
|
else if( appReq != null)
|
||||||
{
|
{
|
||||||
if (_model.IsReceiptExist(ao.receipt))
|
if (_model.IsReceiptExist(appReq.receipt))
|
||||||
{
|
{
|
||||||
OnSendNextOrder(); // 跳过上报过的 Apple 订单
|
OnSendNextOrder(); // 跳过上报过的 Apple 订单
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_model.AddAppleOrder(ao.orderData); // 缓存当前 orderData 等待上报后再消除
|
_model.AddAppleOrder(appReq.orderData); // 缓存当前 orderData 等待上报后再消除
|
||||||
}
|
}
|
||||||
|
|
||||||
request.SetTimeOut(OrderRequestTimeout)
|
request.SetTimeOut(OrderRequestTimeout)
|
||||||
|
|
@ -995,28 +1030,28 @@ namespace Guru
|
||||||
.SetSuccessCallBack(() =>
|
.SetSuccessCallBack(() =>
|
||||||
{
|
{
|
||||||
//---------------- Success ------------------------
|
//---------------- Success ------------------------
|
||||||
if (go != null)
|
if (googleReq != null)
|
||||||
{
|
{
|
||||||
_model.AddToken(go.token); // 记录当前的 Google 订单
|
_model.AddToken(googleReq.token); // 记录当前的 Google 订单
|
||||||
_model.RemoveGoogleOrder(go.orderData); // 成功后清除缓存 orderData
|
_model.RemoveGoogleOrder(googleReq.orderData); // 成功后清除缓存 orderData
|
||||||
}
|
}
|
||||||
else if (ao != null)
|
else if (appReq != null)
|
||||||
{
|
{
|
||||||
_model.AddReceipt(ao.receipt); // 记录当前的 Apple 订单
|
_model.AddReceipt(appReq.receipt); // 记录当前的 Apple 订单
|
||||||
_model.RemoveAppleOrder(ao.orderData); // 成功后清除缓存 orderData
|
_model.RemoveAppleOrder(appReq.orderData); // 成功后清除缓存 orderData
|
||||||
}
|
}
|
||||||
OnSendNextOrder(); // NEXT Order
|
OnSendNextOrder(); // NEXT Order
|
||||||
})
|
})
|
||||||
.SetFailCallBack(() =>
|
.SetFailCallBack(() =>
|
||||||
{
|
{
|
||||||
//---------------- Fail ------------------------
|
//---------------- Fail ------------------------
|
||||||
if (go != null)
|
if (googleReq != null)
|
||||||
{
|
{
|
||||||
ReportGoogleOrderLost(go.orderData); // 上报 Google 订单缺失打点
|
ReportGoogleOrderLost(googleReq.orderData); // 上报 Google 订单缺失打点
|
||||||
}
|
}
|
||||||
else if (ao != null)
|
else if (appReq != null)
|
||||||
{
|
{
|
||||||
ReportAppleOrderLost(ao.orderData); // 上报 Apple 订单缺失打点
|
ReportAppleOrderLost(appReq.orderData); // 上报 Apple 订单缺失打点
|
||||||
}
|
}
|
||||||
OnSendNextOrder(); // NEXT Order
|
OnSendNextOrder(); // NEXT Order
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"description": "Guru SDK core for Unity developers",
|
"description": "Guru SDK core for Unity developers",
|
||||||
"unity": "2021.3",
|
"unity": "2021.3",
|
||||||
"author":{
|
"author":{
|
||||||
"name": "Guru Games"
|
"name": "Guru Game"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"category": ["game","tool","development", "Guru"],
|
"category": ["game","tool","development", "Guru"],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue