feat: Flexion 先验签后消费支付流程 + playerId 绑定

实现 Flexion 渠道支付验签重构:支付成功后不立即消费,将 purchaseJson/signature/purchaseToken
回调至 Unity,由服务端 SHA1withRSA 验签并校验 developerPayload.playerId 后再调用 consume。
同步更新 SDKManager 接口、UnityBridgeFunc 桥接、TYSdkModel 字段及 ExtraPluginCfgs。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 16:04:29 +08:00
parent 7fddc4e0f3
commit f60a1dd0c2
16 changed files with 343 additions and 96 deletions

View File

@@ -5,7 +5,7 @@ public class ChannelHelpers {
public static final String CHANNEL = "Flexion";
static void register() {
SDKManager.registerHelper(CHANNEL, new FlexionSDKHelper());
SDKManager.registerHelper(new FlexionSDKHelper());
}
@@ -14,4 +14,7 @@ public class ChannelHelpers {
public static String normalizeSkuList(String msg) {
return msg;
}
//public SDKExtBuilder;
}

View File

@@ -25,6 +25,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Base64;
import java.util.List;
public class FlexionSDKHelper implements SDKManager.ISDKHelper {
@@ -33,12 +34,18 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
private static boolean billingConnected = false;
private static final PurchasesUpdateListener purchasesListener = (billingResult, purchase) -> {
Log.i(TAG, "[PAY-FLOW] PurchasesUpdateListener: code=" + billingResult.getResponseCode()
+ " hasPurchase=" + (purchase != null));
if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_SUCCESS_CODE) {
if (purchase != null) handlePurchase(purchase);
} else if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_USER_CANCELLED_CODE) {
sendPayCallback(1, "user_cancelled", null, null);
Log.i(TAG, "[PAY-FLOW] user cancelled");
sendPayCallback(1, "user_cancelled", null, null, null);
} else {
sendPayCallback(1, "purchase_failed_" + billingResult.getResponseCode(), null, null);
Log.e(TAG, "[PAY-FLOW] purchase failed: code=" + billingResult.getResponseCode());
sendPayCallback(1, "purchase_failed_" + billingResult.getResponseCode(), null, null, null);
}
};
@@ -83,19 +90,36 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
@Override
public void pay(Activity activity, String productId, String productName,
String productCount, String prodorderId, String appInfo) {
Log.i(TAG, "pay: " + productId);
Log.i(TAG, "[PAY-FLOW] pay called: productId=" + productId + " billingReady=" + billingConnected);
if (billingService == null || !billingConnected) {
sendPayCallback(1, "billing_not_ready", null, null);
Log.e(TAG, "[PAY-FLOW] billing not ready");
sendPayCallback(1, "billing_not_ready", null, null, null);
return;
}
activity.runOnUiThread(() ->
billingService.launchBillingFlow(activity,
new BillingFlowParams(productId, "inapp", "")));
String developerPayload = "";
try {
byte[] decoded = Base64.getDecoder().decode(appInfo);
JSONObject json = new JSONObject(new String(decoded, "UTF-8"));
JSONObject payload = new JSONObject();
payload.put("playerId", json.optString("playerId", ""));
payload.put("customData", json.optString("customData", ""));
developerPayload = payload.toString();
} catch (Exception e) {
Log.w(TAG, "Failed to extract playerId/customData from appInfo: " + e.getMessage());
}
String finalPayload = developerPayload;
activity.runOnUiThread(() -> {
Log.i(TAG, "[PAY-FLOW] launching billing flow for: " + productId + " payload=" + finalPayload);
billingService.launchBillingFlow(activity,
new BillingFlowParams(productId, "inapp", finalPayload));
});
}
@Override
public void queryProducts() {
Log.i(TAG, "queryProducts called, billingConnected=" + billingConnected);
if (billingService == null || !billingConnected) {
Log.w(TAG, "queryProducts: billing not ready");
sendExtensionError("billing_not_ready");
return;
}
@@ -108,6 +132,7 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
SDKAPI.getIns().thirdExtend(jsonObject.toString(), new SDKCallBack.Extend() {
@Override
public void callback(int code, String msg) {
Log.i(TAG, "TuYoo thirdExtend callback: code=" + code + " msg=" + msg);
if (code == SDKCallBack.Extend.EXTEND_CODE_SUCCESS && msg != null) {
try {
JSONArray products = new JSONArray(msg);
@@ -117,6 +142,7 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
String pid = p.optString("productId", "");
if (!pid.isEmpty()) productIds.add(pid);
}
Log.i(TAG, "queryProductDetails with " + productIds.size() + " items: " + productIds);
queryProductDetails(productIds);
} catch (JSONException e) {
sendExtensionError("parse_error: " + e.getMessage());
@@ -131,10 +157,12 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
// ---- Billing Infrastructure ----
private static void initBillingService(Activity activity) {
Log.i(TAG, "[PAY-FLOW] initBillingService");
billingService = FLX.createBillingService(activity, purchasesListener);
billingService.startConnection(activity, new ConnectionStateListener() {
@Override
public void onBillingSetupFinished(BillingResult result) {
Log.i(TAG, "[PAY-FLOW] onBillingSetupFinished: code=" + result.getResponseCode());
if (result.getResponseCode() == BillingResults.ResultCode.CONNECTION_SUCCESS_CODE) {
billingConnected = true;
queryPendingPurchases();
@@ -142,20 +170,22 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
}
@Override
public void onBillingServiceDisconnected() {
Log.w(TAG, "[PAY-FLOW] billing service disconnected");
billingConnected = false;
}
});
}
private static void handlePurchase(Purchase purchase) {
Log.i(TAG, "[PAY-FLOW] handlePurchase: state=" + purchase.getPurchaseState()
+ " token=" + purchase.getToken());
if (purchase.getPurchaseState() == 0) {
sendPayCallback(0, "success", purchase.getPurchaseJson(), purchase.getSignature());
billingService.consumeAsync(
new ConsumeParams(purchase.getToken()),
r -> Log.i(TAG, "Consume " +
(r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail")));
Log.i(TAG, "[PAY-FLOW] purchaseJson=" + purchase.getPurchaseJson());
Log.i(TAG, "[PAY-FLOW] signature=" + purchase.getSignature());
sendPayCallback(0, "success", purchase.getPurchaseJson(), purchase.getSignature(), purchase.getToken());
} else if (purchase.getPurchaseState() == 2) {
sendPayCallback(2, "pending", null, null);
Log.i(TAG, "[PAY-FLOW] purchase state=2 (pending)");
sendPayCallback(2, "pending", null, null, null);
}
}
@@ -207,13 +237,19 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
// ---- Callbacks to Unity ----
private static void sendPayCallback(int code, String errStr, String purchaseJson, String signature) {
private static void sendPayCallback(int code, String errStr,
String purchaseJson, String signature, String purchaseToken) {
Log.i(TAG, "[PAY-FLOW] sendPayCallback: code=" + code + " errStr=" + errStr
+ " hasPurchaseJson=" + (purchaseJson != null) + " hasSignature=" + (signature != null)
+ " hasToken=" + (purchaseToken != null));
try {
JSONObject result = new JSONObject();
result.put("code", code);
result.put("errStr", errStr != null ? errStr : "");
if (purchaseJson != null) result.put("purchaseJson", purchaseJson);
if (signature != null) result.put("signature", signature);
if (purchaseToken != null) result.put("purchaseToken", purchaseToken);
Log.i(TAG, "[PAY-FLOW] UnitySendMessage callback: " + result.toString());
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME,
ConfigManager.PAY_CALLBACK_FUNCTION_NAME, result.toString());
} catch (JSONException e) {
@@ -222,6 +258,18 @@ public class FlexionSDKHelper implements SDKManager.ISDKHelper {
}
}
public static void consumePurchase(String token) {
if (billingService == null || token == null || token.isEmpty()) {
Log.w(TAG, "[PAY-FLOW] consumePurchase: billingService or token is null");
return;
}
Log.i(TAG, "[PAY-FLOW] consumePurchase: token=" + token);
billingService.consumeAsync(
new ConsumeParams(token),
r -> Log.i(TAG, "[PAY-FLOW] consume result: " +
(r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail")));
}
private static void sendExtensionError(String errStr) {
try {
JSONObject result = new JSONObject();

View File

@@ -57,19 +57,26 @@ public class SDKManager {
private static SnsInfo tmpSnsInfo;
private static String channel = "GooglePlay";
// Channel helper registry
// Channel helper
public interface ISDKHelper {
void init(Activity activity);
void pay(Activity activity, String productId, String productName,
String productCount, String prodorderId, String appInfo);
void queryProducts();
void consume(String token);
}
private static final java.util.Map<String, ISDKHelper> helperMap = new java.util.HashMap<>();
private static ISDKHelper sdkHelper;
public static void registerHelper(String ch, ISDKHelper helper) {
helperMap.put(ch, helper);
public static void registerHelper(ISDKHelper helper) {
sdkHelper = helper;
}
public static void ConsumeFlexionPurchase(String token) {
Log.i(TAG, "ConsumeFlexionPurchase: token=" + token);
if (sdkHelper != null) {
sdkHelper.consume(token);
}
}
// -------------------------------------------------------------------------
@@ -79,7 +86,16 @@ public class SDKManager {
public static void InitSDK(Activity activity, String ch) {
channel = ChannelHelpers.CHANNEL;
ChannelHelpers.register();
sdkHelper = helperMap.get(channel);
// var builder = SDKManager.ExtendBuilder()
// .WithChannel(SDKManagerExt.CHANNEL)
// .WithInit(SDKManagerExt.GetInit())
// .WithPay(SDKManagerExt.GetPay())
// .WithQuery(SDKManagerExt.GetQuery())
// .Builder()
Log.i(TAG, "[CHANNEL-VERIFY] Java InitSDK channel=" + channel
+ " fromCSharp=" + ch
+ " helper=" + (sdkHelper != null ? sdkHelper.getClass().getSimpleName() : "default"));
@@ -87,7 +103,8 @@ public class SDKManager {
// Sync channel to C# ChannelConfig
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME,
"SetChannelFromNative", channel);
// build.init(activity)
if (sdkHelper != null) {
sdkHelper.init(activity);
} else {
@@ -402,6 +419,7 @@ public class SDKManager {
// -------------------------------------------------------------------------
public static void thirdExtend() {
Log.e(TAG, "thirdExtend called, sdkHelper=" + (sdkHelper != null ? sdkHelper.getClass().getSimpleName() : "null"));
if (sdkHelper != null) {
sdkHelper.queryProducts();
return;

View File

@@ -30,6 +30,7 @@ namespace tysdk
if (purchaseInfo == null)
purchaseInfo = new JObject();
// purchaseInfo["playerId"] = _accountInfo.strUserId;
string extraInfo = purchaseInfo.ToString(Newtonsoft.Json.Formatting.None);
extraInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes(extraInfo));
@@ -56,18 +57,16 @@ namespace tysdk
#elif UNITY_ANDROID
var pType = ChannelConfig.PayType;
var task = TYSDKCallbackManager.Instance.RegisterCallback<PaymentInfo>(TimeSpan.FromSeconds(30));
var task = TYSDKCallbackManager.Instance.RegisterCallback<PaymentInfo>(TimeSpan.FromSeconds(90));
try
{
UnityBridgeFunc.UnityKnowNew(prodId, prodPrice, prodName, count.ToString(), orderId, extraInfo, pType);
var result = await task;
result.count = count;
result.orderId = orderId;
result.productId = prodId;
result.price = prodPrice;
result.Check(orderId, prodId);
return result;
}
catch (Exception e)
@@ -112,6 +111,9 @@ namespace tysdk
var result = new PaymentInfo();
result.code = jresult["code"].ToString();
result.msg = jresult["errStr"].ToString();
result.purchaseJson = jresult["purchaseJson"]?.ToString();
result.signature = jresult["signature"]?.ToString();
result.purchaseToken = jresult["purchaseToken"]?.ToString();
if (result.code == "-1")
{
if (!_payFirstNegativeHandled)

View File

@@ -21,6 +21,8 @@ namespace tysdk
public string purchaseJson;
// Flexion: purchase.getSignature(), used by server for RSA signature verification
public string signature;
// Flexion: purchase token, used to consume after server verification
public string purchaseToken;
public bool Check(string orderId, string productId)
{

View File

@@ -168,6 +168,14 @@ 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))