package com.unity3d.player; import android.app.Activity; import android.util.Log; import com.flexionmobile.ddpx.listener.ConnectionStateListener; import com.flexionmobile.ddpx.listener.PurchasesUpdateListener; import com.flexionmobile.ddpx.model.BillingResult; import com.flexionmobile.ddpx.model.BillingResults; import com.flexionmobile.ddpx.model.Purchase; import com.flexionmobile.ddpx.model.ProductDetails; import com.flexionmobile.ddpx.model.params.BillingFlowParams; import com.flexionmobile.ddpx.model.params.ConsumeParams; import com.flexionmobile.ddpx.model.params.QueryPurchasesParams; import com.flexionmobile.ddpx.model.params.ProductDetailsParams; import com.flexionmobile.ddpx.service.BillingService; import com.flexionmobile.fdk.FLX; import com.tuyoo.gamesdk.api.SDKAPI; import com.tuyoo.gamesdk.api.SDKCallBack; import com.tuyoo.gamesdk.model.InitParam; import com.tuyoo.gamesdk.util.SDKLog; 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 { private static final String TAG = "FlexionSDKHelper"; private static BillingService billingService; 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) { Log.i(TAG, "[PAY-FLOW] user cancelled"); sendPayCallback(1, "user_cancelled", null, null, null); } else { Log.e(TAG, "[PAY-FLOW] purchase failed: code=" + billingResult.getResponseCode()); sendPayCallback(1, "purchase_failed_" + billingResult.getResponseCode(), null, null, null); } }; @Override public void init(Activity activity) { activity.runOnUiThread(() -> { SDKAPI.getIns().lightModeEnable(); boolean sdkInitOk = false; try { SDKAPI.getIns().init(new InitParam.Builder() .withActivity(activity) .withAppId(ConfigManager.SDK_APPID) .withClientId(ConfigManager.SDK_CLIENTID) .withServerUrl(ConfigManager.SDK_LOGIN_SERVER_URL) .build()); SDKAPI.getIns().onActivityStart(activity); SDKAPI.getIns().onActivityResume(activity); sdkInitOk = true; } catch (Exception e) { Log.e(TAG, "TuYoo SDK init failed, skip FLX init: " + e); TYUnityActivity.setSdkInited(); } if (sdkInitOk) { FLX.init(activity, (initCode, initMsg) -> { Log.i(TAG, "FLX.init result=" + initCode); if (initCode == 0) { FLX.showFlexionScreens(activity, (screenCode, screenMsg) -> { Log.i(TAG, "FLX.showFlexionScreens result=" + screenCode); initBillingService(activity); TYUnityActivity.setSdkInited(); }); } else { Log.e(TAG, "FLX.init failed: " + initCode); TYUnityActivity.setSdkInited(); } }); } }); } @Override public void pay(Activity activity, String productId, String productPrice, String productName, String productCount, String prodorderId, String appInfo) { Log.i(TAG, "[PAY-FLOW] pay called: productId=" + productId + " billingReady=" + billingConnected); if (billingService == null || !billingConnected) { Log.e(TAG, "[PAY-FLOW] billing not ready"); sendPayCallback(1, "billing_not_ready", null, null, null); return; } String developerPayload = ""; try { byte[] decoded = Base64.getDecoder().decode(appInfo); JSONObject json = new JSONObject(new String(decoded, "UTF-8")); JSONObject payload = new JSONObject(); payload.put("userId", json.optString("userId", "")); payload.put("pfid", json.optString("pfid", "")); payload.put("customData", json.optString("customData", "")); payload.put("prodPrice", productPrice); payload.put("prodCount", productCount); developerPayload = payload.toString(); } catch (Exception e) { Log.w(TAG, "Failed to extract payload fields 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; } JSONObject jsonObject = new JSONObject(); try { jsonObject.put("action", "ACTION_GET_SKU_DETAILS_LIST"); } catch (JSONException e) { throw new RuntimeException(e); } 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); List productIds = new java.util.ArrayList<>(); for (int i = 0; i < products.length(); i++) { JSONObject p = products.getJSONObject(i); 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()); } } else { sendExtensionError("tuyoo_query_failed: " + msg); } } }); } // ---- 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(); } } @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) { 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) { Log.i(TAG, "[PAY-FLOW] purchase state=2 (pending)"); sendPayCallback(2, "pending", null, null, null); } } private static void queryPendingPurchases() { if (billingService == null || !billingConnected) return; billingService.queryPurchasesAsync( new QueryPurchasesParams("inapp"), (result, purchases) -> { if (purchases != null) for (Purchase p : purchases) handlePurchase(p); }); } private static void queryProductDetails(List productIds) { billingService.queryProductDetailsAsync( new ProductDetailsParams("inapp", productIds), (billingResult, productDetails) -> { try { JSONObject result = new JSONObject(); if (billingResult.getResponseCode() == BillingResults.ResultCode.QUERY_PRODUCT_DETAILS_SUCCESS_CODE && productDetails != null) { JSONArray arr = new JSONArray(); for (ProductDetails detail : productDetails) { JSONObject item = new JSONObject(); item.put("productId", detail.getId()); item.put("ourProductId", detail.getId()); item.put("title", detail.getTitle()); item.put("description", detail.getDescription()); item.put("price", detail.getPrice()); item.put("price_amount_micros", detail.getPriceAmountMicros()); item.put("price_currency_code", detail.getPriceCurrencyCode()); item.put("type", detail.getType()); arr.put(item); } result.put("code", 0); result.put("respObj", arr.toString()); } else { result.put("code", 1); result.put("thirdExtend_errStr", "flexion_query_failed: " + billingResult.getDebugMessage()); } UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, result.toString()); } catch (JSONException e) { UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, "{\"code\":1}"); } }); } // ---- Callbacks to Unity ---- 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) { UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_CALLBACK_FUNCTION_NAME, "{\"code\":1,\"errStr\":\"json_error\"}"); } } @Override public void consume(String token) { consumePurchase(token); } 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(); result.put("code", 1); result.put("thirdExtend_errStr", errStr); UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, result.toString()); } catch (JSONException e) { UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, "{\"code\":1}"); } } }