using System; using System.Collections.Generic; using System.Threading.Tasks; using asap.core; using Newtonsoft.Json.Linq; using tysdk; using UnityEngine; using Newtonsoft.Json; using System.Net.Http; using System.Security.Cryptography; using System.Text; using UnityEngine.Networking; using System.Threading; namespace game { public class AggIAPService : IIAPService { [Inject] public IUserService userService { get; set; } [Inject] public IConfig config { get; set; } [Inject] public ICustomServerMgr svr { get; set; } public void Init() { } public void Release() { } #if UNITY_ANDROID private const string payment = "google"; #else private const string payment = "apple"; #endif class IAPConfig { public bool Active { get; set; } public string OrderSvrUrl { get; set; } public string FuncKey { get; set; } } private async Task BeforePurch() { var configRemoteUrl = config.Get(GConstant.K_Static_Res_URL, GConstant.V_Static_Res_URL); string iapConfigUrl = null; //var encrypted = false; if (configRemoteUrl == GConstant.V_Static_Res_URL) { iapConfigUrl = String.Format(configRemoteUrl, $"{Application.platform}iapcfg.json"); //encrypted = true; } else { iapConfigUrl = configRemoteUrl.Replace(VersionTool.PackVer, $"{Application.platform}iapcfg.json"); } var timeoutValues = new float[]{1,2,4}; var maxRetryTimes = timeoutValues.Length; for (int retryTime = 0; retryTime < maxRetryTimes; retryTime++) { var iapConfig = await DoGetRawConfig(iapConfigUrl,timeoutValues[retryTime], retryTime, maxRetryTimes); if (iapConfig != null ) { Debug.Log($"Load iapConfig success {retryTime + 1}/{maxRetryTimes}"); return iapConfig; } } return null; } private async Task DoGetRawConfig(string url, float timeoutInSec, int retryTimes, int maxRetryTimes) { using(var req = UnityWebRequest.Get(url)) { var stopwatch = new System.Diagnostics.Stopwatch(); var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(timeoutInSec)); var ct = cts.Token; stopwatch.Start(); req.timeout = (int)timeoutInSec; req.SetRequestHeader("Accept", "application/json"); var op = req.SendWebRequest(); try { while (!op.isDone && !ct.IsCancellationRequested) { await Awaiters.NextFrame; } ct.ThrowIfCancellationRequested(); stopwatch.Stop(); if (req.result == UnityWebRequest.Result.Success) { var decryptKey = "tbambooz"; DESCryptoServiceProvider des = new DESCryptoServiceProvider(); des.Key = Encoding.ASCII.GetBytes(decryptKey); des.IV = Encoding.ASCII.GetBytes(decryptKey); var rawCfg = req.downloadHandler.text; ICryptoTransform decryptor = des.CreateDecryptor(des.Key, des.IV); byte[] inputBytes = Convert.FromBase64String(rawCfg); byte[] outputBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); rawCfg = Encoding.UTF8.GetString(outputBytes); var iapConfig = JsonConvert.DeserializeObject(rawCfg); Debug.Log($"[AggIAPService] DoGetRawConfig Success {url}"); return iapConfig; } #if AGG using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "iap_config"); e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds); e.AddContent("reason", req.result.ToString()); e.AddContent("skip", retryTimes >= maxRetryTimes - 1); e.AddContent("error", req.error); } #endif // AGG Debug.LogWarning($"Load iap config failed {req.result.ToString()} {req.error} {retryTimes + 1}/{maxRetryTimes}"); } catch (OperationCanceledException) { req.Abort(); stopwatch.Stop(); #if AGG using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "iap_config"); e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds); e.AddContent("reason", "cancel"); e.AddContent("skip", retryTimes >= maxRetryTimes - 1); e.AddContent("error", req.error ?? "cancel"); } #endif // AGG Debug.LogWarning($"Load remote config cancel {req.result.ToString()} {req.error} {retryTimes + 1}/{maxRetryTimes}"); } finally { cts.Dispose(); } return null; } } public async Task> GetSKUs() { Debug.Log("[AggIAPService] GetSKUList fetch"); var prodList = await TYSdkFacade.Instance.GetSKUList(); if (prodList.code != 0) { Debug.LogWarning("[AggIAPService] GetSKUList Failed"); return null; } Debug.Log($"[AggIAPService] GetSKUList Success prod Num {prodList.products.Count}"); return prodList.products; } public async Task Pay(Action OnSuccessFromSdk, string replenishmentDataStr, SKUDetail sku, int count, float usdPrice, JObject extraPurcheInfo) { await Awaiters.NextFrame; GameCore.UIManager.ShowWaitingBlock("AggIAPService::Pay"); await Awaiters.NextFrame; //======= check iap config var iapCfg = await BeforePurch(); if (iapCfg == null) { Debug.LogWarning("Fetch iap cfg fail"); GameCore.UIManager.HideWaitingBlock("AggIAPService::Pay"); SendRechargeEvent(sku, usdPrice, "iapcfg_fail", extraPurcheInfo, count); return new PaymentInfo() { code = "10002" }; } else if (!iapCfg.Active) { GameCore.UIManager.HideWaitingBlock("AggIAPService::Pay"); return new PaymentInfo() { code = "10001" }; } //===== check iap config end====== RTServiceBgFgManager.keepAlive = true; SendRechargeEvent(sku, usdPrice, "start", extraPurcheInfo, count); var purchaseInfo = new JObject(); purchaseInfo["ver"] = 3; purchaseInfo["pfid"] = userService.UserId; purchaseInfo["sum"] = usdPrice * count; purchaseInfo["customData"] = replenishmentDataStr; PaymentInfo paymentInfo = await TYSdkFacade.Instance.Pay(sku, count, usdPrice, purchaseInfo); if (!paymentInfo.isSuccessFromSdk) { SendRechargeEvent(sku, usdPrice, "fail", extraPurcheInfo, count); } else { OnSuccessFromSdk(paymentInfo.orderId); await WaitForSvrOrder(paymentInfo, sku, usdPrice, count, extraPurcheInfo, iapCfg); } if (paymentInfo.isSuccess) { await ConfirmOrder(paymentInfo, iapCfg); } GameCore.UIManager.HideWaitingBlock("AggIAPService::Pay"); RTServiceBgFgManager.keepAlive = false; return paymentInfo; } private async Task WaitForSvrOrder(PaymentInfo paymentInfo, SKUDetail sku, float usdPrice, int count, JObject extraPurcheInfo, IAPConfig iapCfg) { Debug.Log("[AggIAPService] WaitForSvrOrder"); await Awaiters.Seconds(2); var startTime = ZZTimeHelper.UtcNow(); var confirmEvt = new CS_OrderConfirmEvt() { orderId = paymentInfo.orderId, sdkUserId = userService.CustomId }.ToJson(); while (true) { var result = await IAPPost("csconfirm", iapCfg, confirmEvt); Debug.Log("[AggIAPService] WaitForSvrOrder request done"); if (!string.IsNullOrEmpty(result)) { Debug.Log($"[AggIAPService] WaitForSvrOrder respon {result}"); var sconfirmEvt = JsonConvert.DeserializeObject(result); if (sconfirmEvt.isSuccess) { if (paymentInfo.Check(sconfirmEvt.orderId, sconfirmEvt.productId)) { SendRevenueEvent(sku, usdPrice, paymentInfo.orderId, extraPurcheInfo, count); } else { SendRechargeEvent(sku, usdPrice, "checkfail", extraPurcheInfo, paymentInfo.count); } return; } } else { Debug.Log("[AggIAPService] WaitForSvrOrder request error"); } await Awaiters.Seconds(2); if (ZZTimeHelper.UtcNow() - startTime > TimeSpan.FromSeconds(50)) { SendRechargeEvent(sku, usdPrice, "timeout", extraPurcheInfo, paymentInfo.count); Debug.Log("[AggIAPService] WaitForSvrOrder timeout"); break; } Debug.Log("[AggIAPService] WaitForSvrOrder retry"); } } private async Task ConfirmOrder(PaymentInfo paymentInfo, IAPConfig iapCfg) { var confirmEvt = new CS_OrderConfirmEvt() { orderId = paymentInfo.orderId, sdkUserId = userService.CustomId }.ToJson(); int count = 3; string result; bool re = false; while (count > 0) { count--; result = await IAPPost("receive", iapCfg, confirmEvt); if (bool.TryParse(result, out re) && re) { return; } } if (!re) { paymentInfo.code = "10000"; } } /// /// 获取未领奖的订单号列表 时间戳 /// /// 时间戳 最早订单时间 -1m /// public async Task> GetOrderUnreceived() { var iapCfg = await BeforePurch(); if (iapCfg == null) { Debug.LogError("Fetch iap cfg fail, on get unreceive order "); return new Dictionary(); } var orderUnreceived = new CS_OrderUnreceived() { sdkUserId = userService.CustomId, customData = true }.ToJson(); string result = await IAPPost("unreceive", iapCfg, orderUnreceived); if (string.IsNullOrEmpty(result) || result == "[]") { return new Dictionary(); } else { SC_OrderUnreceived response = JsonConvert.DeserializeObject(result); return response?.orders; } } public async Task ConfirmOrders(List orderIDs) { if (orderIDs == null || orderIDs.Count == 0) return false; var confirmEvt = new CS_OrdersConfirmEvt() { orderIds = orderIDs, sdkUserId = userService.CustomId }.ToJson(); var iapCfg = await BeforePurch(); if (iapCfg == null) { return false; } string result = await IAPPost("receiveorders", iapCfg, confirmEvt); if (string.IsNullOrEmpty(result) || result == "[]" || !bool.TryParse(result, out bool re)) { return false; } return true; } private void SendRechargeEvent(SKUDetail sku, float usdPrice, string state, JObject extraPurcheInfo, int count = 1) { using (var e = GEvent.TackEvent("recharge_state")) { e.AddContent(AFInAppEvents.CONTENT_ID, sku.ProdID) .AddContent(AFInAppEvents.PRICE, usdPrice) .AddContent(AFInAppEvents.CURRENCY, sku.price_currency_code) .AddContent("payment", payment) .AddContent("payment_state", state) .AddContent(AFInAppEvents.QUANTITY, count); if (null != extraPurcheInfo) { foreach (var item in extraPurcheInfo) { e.AddContent(item.Key, item.Value); } } } } private void SendRevenueEvent(SKUDetail sku, float usdPrice, string orderId, JObject extraPurcheInfo, int count = 1) { using (var e = GEvent.RevenueEvent()) { e.AddContent(AFInAppEvents.CONTENT_ID, sku.ProdID) .AddContent(AFInAppEvents.PRICE, usdPrice) .AddContent(AFInAppEvents.CURRENCY, sku.price_currency_code) .AddContent("payment", payment) .AddContent(AFInAppEvents.QUANTITY, 1) .AddContent("order_id", orderId) .AddContent("restore", false) .AddContent("payment_state", "finish") .AddContent("af_revenue", sku.ProdPrice * count); if (null != extraPurcheInfo) { foreach (var item in extraPurcheInfo) { e.AddContent(item.Key, item.Value); } } } } async Task IAPPost(string route, IAPConfig iapCfg, string bodyString) { var apiurl = config.Get(GConstant.K_Event_API_URL, iapCfg.OrderSvrUrl); if (apiurl == iapCfg.OrderSvrUrl) { return await svr.CustomServerPost($"order/{route}", bodyString); } else { var httpClient = new HttpClient(); httpClient.BaseAddress = new System.Uri(iapCfg.OrderSvrUrl); httpClient.Timeout = TimeSpan.FromSeconds(10); httpClient.DefaultRequestHeaders.Add("x-functions-key", iapCfg.FuncKey); try { var content = new StringContent(bodyString, Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync(route, content); string result = await response.Content.ReadAsStringAsync(); if (response.StatusCode == System.Net.HttpStatusCode.OK) { return result; } else { Debug.LogError($"iap post fail {response.StatusCode}"); return null; } } catch (Exception e) { Debug.LogError($"iap post fail {e.Message}"); return null; } } } } }