diff --git a/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs new file mode 100644 index 0000000..a148e49 --- /dev/null +++ b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs @@ -0,0 +1,189 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace tysdk +{ + public interface IPaymentVerifier + { + Task Verify(PaymentInfo info); + void Consume(string token); + } + + public class VerifyResult + { + public bool success; + public string msg; + } + + public static class PaymentVerifierFactory + { + public static IPaymentVerifier GetVerifier() + { + return ChannelConfig.Channel switch + { + "Flexion" => new RetryablePaymentVerifier(new FlexionPaymentVerifier()), + "Dummy" => new RetryablePaymentVerifier(new DummyPaymentVerifier()), + _ => null + }; + } + } + + public class RetryablePaymentVerifier : IPaymentVerifier + { + private readonly IPaymentVerifier _inner; + private const int MaxRetries = 2; + private static readonly TimeSpan RetryInterval = TimeSpan.FromSeconds(2); + + public RetryablePaymentVerifier(IPaymentVerifier inner) + { + _inner = inner; + } + + public async Task Verify(PaymentInfo info) + { + VerifyResult lastResult = null; + for (int attempt = 0; attempt <= MaxRetries; attempt++) + { + if (attempt > 0) + { + Debug.Log($"[RetryableVerifier] Flow retry {attempt}/{MaxRetries}, waiting {RetryInterval.TotalSeconds}s"); + await Task.Delay(RetryInterval); + } + lastResult = await _inner.Verify(info); + if (lastResult.success) return lastResult; + Debug.LogWarning($"[RetryableVerifier] Flow attempt {attempt + 1} failed: {lastResult.msg}"); + } + return lastResult; + } + + public void Consume(string token) => _inner.Consume(token); + } + + public class FlexionPaymentVerifier : IPaymentVerifier + { + // TODO: 替换为实际游戏服务端验签 URL + private const string VERIFY_URL = "https://YOUR_GAME_SERVER/api/pay/verify"; + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30); + private const int MaxRetries = 2; + private static readonly TimeSpan RetryBackoff = TimeSpan.FromSeconds(2); + +#if UNITY_EDITOR + public async Task Verify(PaymentInfo info) + { + Debug.LogWarning("[FlexionPaymentVerifier] Editor placeholder: auto success"); + await Task.Yield(); + return new VerifyResult { success = true, msg = "editor_placeholder" }; + } +#else + public async Task Verify(PaymentInfo info) + { + VerifyResult lastResult = null; + for (int attempt = 0; attempt <= MaxRetries; attempt++) + { + if (attempt > 0) + { + var delay = TimeSpan.FromSeconds(RetryBackoff.TotalSeconds * attempt); + Debug.Log($"[FlexionPaymentVerifier] HTTP retry {attempt}/{MaxRetries}, waiting {delay.TotalSeconds}s"); + await Task.Delay(delay); + } + lastResult = await SendVerifyRequest(BuildRequestBody(info)); + if (lastResult.success) return lastResult; + Debug.LogWarning($"[FlexionPaymentVerifier] HTTP attempt {attempt + 1} failed: {lastResult.msg}"); + } + return lastResult; + } + + private async Task SendVerifyRequest(string body) + { + var cts = new CancellationTokenSource(Timeout); + try + { + Debug.Log("[FlexionPaymentVerifier] Sending verification request"); + + using var req = new UnityWebRequest(VERIFY_URL, "POST"); + req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)); + req.downloadHandler = new DownloadHandlerBuffer(); + req.SetRequestHeader("Content-Type", "application/json"); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + cts.Token.Register(() => tcs.TrySetResult(new VerifyResult { success = false, msg = "timeout" })); + + req.SendWebRequest().completed += _ => + { + if (req.result == UnityWebRequest.Result.Success) + { + Debug.Log("[FlexionPaymentVerifier] Response: " + req.downloadHandler.text); + // TODO: 按实际服务端响应格式解析 + tcs.TrySetResult(new VerifyResult { success = true, msg = "ok" }); + } + else + { + Debug.LogWarning("[FlexionPaymentVerifier] Failed: " + req.error); + tcs.TrySetResult(new VerifyResult { success = false, msg = req.error }); + } + }; + + return await tcs.Task; + } + catch (Exception e) + { + Debug.LogError("[FlexionPaymentVerifier] Exception: " + e.Message); + return new VerifyResult { success = false, msg = e.Message }; + } + finally + { + cts.Dispose(); + } + } + + private static string BuildRequestBody(PaymentInfo info) + { + var body = new StringBuilder(); + body.Append('{'); + body.Append("\"purchaseJson\":").Append(Escape(info.purchaseJson)).Append(','); + body.Append("\"signature\":").Append(Escape(info.signature)).Append(','); + body.Append("\"orderId\":").Append(Escape(info.orderId)).Append(','); + body.Append("\"productId\":").Append(Escape(info.productId)).Append(','); + body.Append("\"purchaseToken\":").Append(Escape(info.purchaseToken)); + body.Append('}'); + return body.ToString(); + } +#endif + + private static string Escape(string s) + { + if (s == null) return "null"; + return '"' + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + '"'; + } + +#if UNITY_EDITOR + public void Consume(string token) { } +#elif UNITY_ANDROID + public void Consume(string token) + { + if (string.IsNullOrEmpty(token)) return; + using var sdkManager = new AndroidJavaClass("com.unity3d.player.SDKManager"); + sdkManager.CallStatic("ConsumeFlexionPurchase", token); + } +#endif + } + + public class DummyPaymentVerifier : IPaymentVerifier + { + public async Task Verify(PaymentInfo info) + { + Debug.Log("[DummyPaymentVerifier] Verifying purchase: " + info.orderId); + await Task.Delay(500); + return new VerifyResult { success = true, msg = "dummy_ok" }; + } + + public void Consume(string token) + { + Debug.Log("[DummyPaymentVerifier] Consume: " + token); + } + } +} diff --git a/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta new file mode 100644 index 0000000..705f354 --- /dev/null +++ b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b7f5b1ccc26bf047b00f9b79d7fa3da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs index 7d4b2b1..a791136 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs @@ -128,7 +128,47 @@ namespace tysdk } return; } - TYSDKCallbackManager.Instance.TryTriggerCallback(result); + var verifier = PaymentVerifierFactory.GetVerifier(); + if (verifier != null && !string.IsNullOrEmpty(result.purchaseJson)) + { + _VerifyPurchase(result, verifier); + } + else + { + TYSDKCallbackManager.Instance.TryTriggerCallback(result); + } + } + + private async void _VerifyPurchase(PaymentInfo result, IPaymentVerifier verifier) + { + TYSDKCallbackManager.Instance.ResetCallbackTimeout(TimeSpan.FromSeconds(60)); + try + { + var verifyResult = await verifier.Verify(result); + if (verifyResult.success) + { + result.verified = true; + if (!string.IsNullOrEmpty(result.purchaseToken)) + { + verifier.Consume(result.purchaseToken); + } + TYSDKCallbackManager.Instance.TryTriggerCallback(result); + } + else + { + Debug.LogWarning("[TYSdkFacade] Payment verification failed: " + verifyResult.msg); + result.code = "-1"; + result.msg = "verification_failed: " + verifyResult.msg; + TYSDKCallbackManager.Instance.TryTriggerCallback(result); + } + } + catch (Exception e) + { + Debug.LogError("[TYSdkFacade] Verification exception: " + e.Message); + result.code = "-1"; + result.msg = "verification_error: " + e.Message; + TYSDKCallbackManager.Instance.TryTriggerCallback(result); + } } public async Task GetSKUList() diff --git a/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs b/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs index 375df67..b5b5c6a 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs @@ -24,6 +24,8 @@ namespace tysdk // Flexion: purchase token, used to consume after server verification public string purchaseToken; + public bool verified; + public bool Check(string orderId, string productId) { confirmed = orderId == this.orderId && productId == this.productId; diff --git a/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs b/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs index 681b7ea..e665a43 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs @@ -168,14 +168,6 @@ namespace tysdk } } - public static void ConsumeFlexionPurchase(string token) - { - using (var sdkManager = new AndroidJavaClass(SDK_CLASS)) - { - sdkManager.CallStatic("ConsumeFlexionPurchase", token); - } - } - public static void UnityKnow(String productId, String productName, String productCount, String prodorderId, String appInfo) { using (var sdkManager = new AndroidJavaClass(SDK_CLASS))