2 Commits

Author SHA1 Message Date
3da65b8cbd update:enjoy-pay 接入 2026-06-05 16:58:17 +08:00
ea03c8023f update:更新接口的修改 2026-06-05 16:57:07 +08:00
22 changed files with 1270 additions and 254 deletions

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.arkgame.ft.ep"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application android:extractNativeLibs="true" android:name="com.unity3d.player.TYApp" android:usesCleartextTraffic="true">
<activity android:name="com.unity3d.player.TYUnityActivity"
android:theme="@style/UnityThemeSelector">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="fishingtraveliae.onelink.me" android:pathPrefix="/jT1Q"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="mainactivity" android:scheme="zzft" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
<meta-data android:name="google_analytics_default_allow_analytics_storage" android:value="true" />
<meta-data android:name="google_analytics_default_allow_ad_storage" android:value="true" />
<meta-data android:name="google_analytics_default_allow_ad_user_data" android:value="true" />
<meta-data android:name="google_analytics_default_allow_ad_personalization_signals" android:value="true" />
</activity>
<meta-data android:name="CHANNEL" android:value="EnjoyPay" />
</application>
<queries>
<provider android:authorities="com.facebook.katana.provider.PlatformProvider" />
<provider android:authorities="com.facebook.orca.provider.PlatformProvider" />
<intent>
<action android:name="android.intent.action.MAIN"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,13 @@
package com.unity3d.player;
public class ChannelEntry {
public static final String CHANNEL = "EnjoyPay";
public static SDKManager.IChannelSDKAdapter createSDKAdapter() {
return null;
}
public static String normalizeSkuList(String msg) {
return msg;
}
}

View File

@@ -0,0 +1,26 @@
package com.unity3d.player;
public class ConfigManager {
// Common SDK parameters
public static int SDK_APPID = 20587;
public static String SDK_GAMEID = "20587";
public static String SDK_PROJECTID = "20587";
public static String SDK_CLOUDID = "128";
public static String SDK_CLIENTID = "Android_5.00_tyGuest,facebook.googleplay.0-hall20587.googleplay.FishingMasterRussia";
public static String SDK_LOGIN_SERVER_URL = "https://128-hwsfsdk-sdk-online01.qijihdhk.com";
// FCM (anti-addiction system)
public static String FCM_NAMESPACE = "128";
public static String FCM_IAM_HOST = "tcp://fcmtcp.tuyoo.com:3563";
public static String FCM_ANTISERVERURL = "https://fcmapi.tuyoo.com";
// Unity Facade callback names
public static final String UNITY_FACADE_NAME = "TYSdkFacade";
public static final String LOGIN_CALLBACK_FUNCTION_NAME = "LoginResult";
public static final String LINKACCOUT_CALLBACK_FUNCTION_NAME = "LinkAccoutResult";
public static final String CHANGELINK_CALLBACK_FUNCTION_NAME = "ChangeLinkAccountResult";
public static final String CHECKLINK_CALLBACK_FUNCTION_NAME = "CheckLinkResult";
public static final String PAY_CALLBACK_FUNCTION_NAME = "PayResult";
public static final String PAY_TYPE_CALLBACK_FUNCTION_NAME = "SKUListResult";
public static final String FCM_NOTICE_FUNCTION_NAME = "FCMCallback";
public static final String FCM_REALNAME_CALLBACK_FUNCTION_NAME = "RealNameCallback";
public static final String INIT_CALLBACK_FUNCTION_NAME = "InitResult";
}

View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "404468630883",
"project_id": "fishing-travel-ep-1ab1e",
"storage_bucket": "fishing-travel-ep-1ab1e.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:404468630883:android:de3cb115158a5e4cdfff56",
"android_client_info": {
"package_name": "com.arkgame.ft.ep"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAL8YVO4U6A9WRiQ581Et0o-BmtMf3k7Hg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -0,0 +1,11 @@
org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M
org.gradle.parallel=true
unityStreamingAssets=**STREAMING_ASSETS**
# Android Resolver Properties Start
android.useAndroidX=true
android.enableJetifier=true
# Android Resolver Properties End
android.suppressUnsupportedCompileSdk=35
**ADDITIONAL_PROPERTIES**

View File

@@ -0,0 +1,65 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
dependencies {
implementation project(':unityLibrary')
implementation(platform("com.google.firebase:firebase-bom:33.6.0"))
implementation("com.google.firebase:firebase-analytics")
}
android {
namespace "**NAMESPACE**"
ndkPath "**NDKPATH**"
compileSdkVersion **APIVERSION**
buildToolsVersion '**BUILDTOOLS**'
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
defaultConfig {
minSdkVersion **MINSDKVERSION**
targetSdkVersion **TARGETSDKVERSION**
applicationId '**APPLICATIONID**'
ndk {
abiFilters **ABIFILTERS**
}
versionCode **VERSIONCODE**
versionName '**VERSIONNAME**'
}
aaptOptions {
noCompress = **BUILTIN_NOCOMPRESS** + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~"
}**SIGN**
lintOptions {
abortOnError false
}
buildTypes {
debug {
minifyEnabled **MINIFY_DEBUG**
proguardFiles getDefaultProguardFile('proguard-android.txt')**SIGNCONFIG**
jniDebuggable true
}
release {
minifyEnabled **MINIFY_RELEASE**
proguardFiles getDefaultProguardFile('proguard-android.txt')**SIGNCONFIG**
}
}**PACKAGING_OPTIONS****PLAY_ASSET_PACKS****SPLITS**
**BUILT_APK_LOCATION**
bundle {
language {
enableSplit = false
}
density {
enableSplit = false
}
abi {
enableSplit = true
}
}
}**SPLITS_VERSION_CODE****LAUNCHER_SOURCE_BUILD_SETUP**

View File

@@ -0,0 +1,112 @@
apply plugin: 'com.android.library'
**APPLY_PLUGINS**
// Android Resolver Exclusions Start
android {
packagingOptions {
exclude ('/lib/armeabi/*' + '*')
exclude ('/lib/mips/*' + '*')
exclude ('/lib/mips64/*' + '*')
exclude ('/lib/x86/*' + '*')
exclude ('/lib/x86_64/*' + '*')
}
}
// Android Resolver Exclusions End
android {
namespace "com.unity3d.player"
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api "com.android.support:support-v4:28.0.0"
api "com.android.support:appcompat-v7:28.0.0"
api "com.google.code.gson:gson:2.8.6"
api "com.squareup.okio:okio:1.15.0"
api "com.squareup.okhttp3:okhttp:3.12.13"
api "com.squareup.retrofit2:retrofit:2.3.0"
api "com.squareup.retrofit2:converter-gson:2.2.0"
api "com.squareup.retrofit2:adapter-rxjava:2.1.0"
api "io.reactivex:rxandroid:1.2.1"
api "io.reactivex:rxjava:1.1.9"
// Google Play In-App Review
implementation 'com.google.android.play:review:2.0.1'
implementation 'net.aihelp:android-aihelp-aar:5.3.+'
implementation 'com.google.android.gms:play-services-auth:20.2.0'
implementation('com.android.billingclient:billing:7.0.0')
implementation 'com.google.android.gms:play-services-basement:18.2.0'
implementation 'androidx.fragment:fragment:1.3.6'
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.41"
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.3.41'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41'
implementation 'org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.41'
implementation 'androidx.core:core:1.8.0'
implementation 'com.miui.referrer:homereferrer:1.0.0.6'
implementation platform('com.google.firebase:firebase-bom:34.6.0')
implementation 'com.google.firebase:firebase-analytics'
implementation('com.facebook.android:facebook-android-sdk:16.2.0')
implementation 'com.google.android.play:asset-delivery:2.2.2'
implementation "androidx.credentials:credentials:1.3.0"
implementation "androidx.credentials:credentials-play-services-auth:1.3.0"
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
// EnjoyPay SDK dependency goes here after the official Maven/AAR coordinates are confirmed.
// Android Resolver Dependencies Start
implementation 'com.android.installreferrer:installreferrer:2.1' // Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml:7
implementation 'com.applovin.mediation:facebook-adapter:[6.18.0.1]' // Assets/MaxSdk/Mediation/Facebook/Editor/Dependencies.xml:8
implementation 'com.applovin.mediation:fyber-adapter:8.4.2.0' // Assets/MaxSdk/Mediation/Fyber/Editor/Dependencies.xml:4
implementation 'com.applovin.mediation:google-adapter:[23.6.0.1]' // Assets/MaxSdk/Mediation/Google/Editor/Dependencies.xml:5
implementation 'com.applovin.mediation:ironsource-adapter:8.6.1.0.0' // Assets/MaxSdk/Mediation/IronSource/Editor/Dependencies.xml:8
implementation 'com.applovin.mediation:unityads-adapter:4.12.3.0' // Assets/MaxSdk/Mediation/UnityAds/Editor/Dependencies.xml:4
implementation 'com.applovin.mediation:vungle-adapter:7.4.2.2' // Assets/MaxSdk/Mediation/Vungle/Editor/Dependencies.xml:4
implementation 'com.applovin:applovin-sdk:13.5.1' // Assets/MaxSdk/AppLovin/Editor/Dependencies.xml:4
implementation 'com.appsflyer:af-android-sdk:6.17.3' // Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml:5
implementation 'com.appsflyer:purchase-connector:2.2.0' // Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml:8
implementation 'com.appsflyer:unity-wrapper:6.17.7' // Assets/AppsFlyer/Editor/Dependencies.xml:6
implementation 'com.google.android.ump:user-messaging-platform:2.1.0' // Assets/MaxSdk/AppLovin/Editor/Dependencies.xml:5
// Android Resolver Dependencies End
**DEPS**}
android {
ndkPath "**NDKPATH**"
compileSdkVersion **APIVERSION**
buildToolsVersion '**BUILDTOOLS**'
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
defaultConfig {
minSdkVersion **MINSDKVERSION**
targetSdkVersion **TARGETSDKVERSION**
ndk {
abiFilters **ABIFILTERS**
}
versionCode **VERSIONCODE**
versionName '**VERSIONNAME**'
consumerProguardFiles 'proguard-unity.txt'**USER_PROGUARD**
}
lintOptions {
abortOnError false
}
aaptOptions {
noCompress = **BUILTIN_NOCOMPRESS** + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~"
}**PACKAGING_OPTIONS**
}
**IL_CPP_BUILD_SETUP**
**SOURCE_BUILD_SETUP**
**EXTERNAL_SOURCES**

View File

@@ -0,0 +1,35 @@
pluginManagement {
repositories {
**ARTIFACTORYREPOSITORY**
gradlePluginPortal()
google()
mavenCentral()
}
}
include ':launcher', ':unityLibrary'
**INCLUDES**
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
**ARTIFACTORYREPOSITORY**
google()
mavenCentral()
// Android Resolver Repos Start
def unityProjectPath = $/file:///**DIR_UNITYPROJECT**/$.replace("\\", "/")
maven {
url "https://android-sdk.is.com/" // Assets/MaxSdk/Mediation/IronSource/Dependencies.xml:8
}
mavenLocal()
// Android Resolver Repos End
flatDir {
dirs "${project(':unityLibrary').projectDir}/libs"
}
maven {
url "http://sdkck.tuyoo.com/artifactory/tuyoo-component/"
allowInsecureProtocol = true
}
}
}

View File

@@ -25,12 +25,14 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class FlexionSDKAdapter implements public class FlexionSDKAdapter implements
SDKManager.IChannelSDKAdapter, SDKManager.IChannelSDKAdapter,
@@ -39,13 +41,23 @@ public class FlexionSDKAdapter implements
SDKManager.IPendingPurchaseAdapter, SDKManager.IPendingPurchaseAdapter,
SDKManager.IBillingLifecycleAdapter { SDKManager.IBillingLifecycleAdapter {
private static final String TAG = "FlexionSDKAdapter"; private static final String TAG = "FlexionSDKAdapter";
private static final int FLX_INIT_MAX_ATTEMPTS = 3;
private static final int QUERY_PURCHASES_SUCCESS_CODE = 5000;
private static BillingService billingService; private static BillingService billingService;
private static boolean billingConnected = false; private static boolean billingConnected = false;
private static boolean billingConnecting = false; private static boolean billingConnecting = false;
private static boolean tuyooSdkInitialized = false;
private static boolean flexionInitialized = false;
private static boolean flexionScreensStarted = false;
private static boolean flexionScreensShown = false;
private static Activity billingActivity; private static Activity billingActivity;
private static final Handler mainHandler = new Handler(Looper.getMainLooper()); private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private static final List<String> pendingPurchases = new ArrayList<>(); private static final List<String> pendingPurchases = new ArrayList<>();
private static final List<PendingBillingOperation> pendingBillingOperations = new ArrayList<>(); private static final List<PendingBillingOperation> pendingBillingOperations = new ArrayList<>();
private static final Object pendingQueryLock = new Object();
private static boolean pendingPurchaseQueryInProgress = false;
private static boolean pendingPurchaseCacheReady = false;
private static boolean pendingPurchaseUnityCallbackWaiting = false;
private static final ExecutorService billingExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "FlexionBillingThread")); private static final ExecutorService billingExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "FlexionBillingThread"));
private interface BillingReadyOperation { private interface BillingReadyOperation {
@@ -65,12 +77,12 @@ public class FlexionSDKAdapter implements
} }
private static final PurchasesUpdateListener purchasesListener = (billingResult, purchase) -> { private static final PurchasesUpdateListener purchasesListener = (billingResult, purchase) -> {
Log.w(TAG, "[PAY-FLOW] PurchasesUpdateListener: code=" + billingResult.getResponseCode() // Log.w(TAG, "[PAY-FLOW] PurchasesUpdateListener: code=" + billingResult.getResponseCode()
+ " hasPurchase=" + (purchase != null)); // + " hasPurchase=" + (purchase != null));
if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_SUCCESS_CODE) { if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_SUCCESS_CODE) {
if (purchase != null) handlePurchase(purchase); if (purchase != null) handlePurchase(purchase);
} else if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_USER_CANCELLED_CODE) { } else if (billingResult.getResponseCode() == BillingResults.ResultCode.PURCHASE_USER_CANCELLED_CODE) {
Log.i(TAG, "[PAY-FLOW] user cancelled"); // Log.i(TAG, "[PAY-FLOW] user cancelled");
sendPayCallback(1, "user_cancelled", null, null, null, null); sendPayCallback(1, "user_cancelled", null, null, null, null);
} else { } else {
Log.e(TAG, "[PAY-FLOW] purchase failed: code=" + billingResult.getResponseCode()); Log.e(TAG, "[PAY-FLOW] purchase failed: code=" + billingResult.getResponseCode());
@@ -80,67 +92,160 @@ public class FlexionSDKAdapter implements
@Override @Override
public void init(Activity activity) { public void init(Activity activity) {
Log.i(TAG, "[FLX-INIT] adapter init flexionInitialized=" + flexionInitialized
+ " flexionScreensStarted=" + flexionScreensStarted
+ " flexionScreensShown=" + flexionScreensShown);
activity.runOnUiThread(() -> { activity.runOnUiThread(() -> {
SDKAPI.getIns().lightModeEnable(); SDKAPI.getIns().lightModeEnable();
boolean sdkInitOk = false; if (ensureTuYooSdkInitialized(activity)) {
try { if (flexionInitialized) {
SDKAPI.getIns().init(new InitParam.Builder() notifyFlexionInitResult(true, "ok");
.withActivity(activity) showFlexionScreensThenInitBilling(activity);
.withAppId(ConfigManager.SDK_APPID) } else {
.withClientId(ConfigManager.SDK_CLIENTID) initFlexionWithRetry(activity, 1);
.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();
SDKManager.notifyInitResult(false, "tuyoo_init_failed: " + e.getMessage());
}
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();
SDKManager.notifyInitResult(true, "ok");
});
} else {
Log.e(TAG, "FLX.init failed: " + initCode);
TYUnityActivity.setSdkInited();
SDKManager.notifyInitResult(false, "flx_init_failed: " + initCode);
}
});
} }
}); });
} }
private static boolean ensureTuYooSdkInitialized(Activity activity) {
if (tuyooSdkInitialized) {
Log.i(TAG, "[FLX-INIT] TuYoo SDK already initialized, skip SDKAPI.init");
return true;
}
try {
Log.i(TAG, "[FLX-INIT] TuYoo SDK init start");
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);
tuyooSdkInitialized = true;
Log.i(TAG, "[FLX-INIT] TuYoo SDK init success");
return true;
} catch (Exception e) {
Log.e(TAG, "TuYoo SDK init failed, skip FLX init: " + e);
TYUnityActivity.setSdkInited();
SDKManager.notifyInitResult(false, "tuyoo_init_failed: " + e.getMessage());
return false;
}
}
private static void initFlexionWithRetry(Activity activity, int attempt) {
if (!isActivityAlive(activity)) {
Log.w(TAG, "FLX.init skipped: activity not alive");
SDKManager.notifyInitResult(false, "flx_init_activity_not_alive");
return;
}
Log.i(TAG, "[FLX-INIT] FLX.init start attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS);
try {
FLX.init(activity, (initCode, initMsg) -> {
Log.i(TAG, "[FLX-INIT] FLX.init result=" + initCode
+ " msg=" + initMsg
+ " attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS);
if (initCode == 0) {
flexionInitialized = true;
notifyFlexionInitResult(true, "ok");
showFlexionScreensThenInitBilling(activity);
return;
}
onFlexionInitFailed(activity, attempt, "code=" + initCode + " msg=" + initMsg);
});
} catch (Exception e) {
Log.e(TAG, "FLX.init exception attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS
+ " error=" + e.getMessage());
onFlexionInitFailed(activity, attempt, "exception=" + e.getMessage());
}
}
private static void onFlexionInitFailed(Activity activity, int attempt, String detail) {
if (attempt < FLX_INIT_MAX_ATTEMPTS) {
long delayMs = attempt * 1000L;
Log.w(TAG, "FLX.init failed, retry after " + delayMs + "ms"
+ " attempt=" + attempt + "/" + FLX_INIT_MAX_ATTEMPTS
+ " detail=" + detail);
mainHandler.postDelayed(() -> initFlexionWithRetry(activity, attempt + 1), delayMs);
return;
}
Log.e(TAG, "FLX.init failed after retry: " + detail);
TYUnityActivity.setSdkInited();
SDKManager.notifyInitResult(false, "flx_init_failed_after_retry: " + detail);
}
private static void showFlexionScreensThenInitBilling(Activity activity) {
if (flexionScreensShown) {
Log.i(TAG, "[FLX-INIT] showFlexionScreens skipped, already shown");
initBillingServiceAfterScreens(activity);
return;
}
if (flexionScreensStarted) {
Log.w(TAG, "[FLX-INIT] showFlexionScreens already started without result, skip duplicate show");
return;
}
Log.i(TAG, "[FLX-INIT] showFlexionScreens start");
flexionScreensStarted = true;
FLX.showFlexionScreens(activity, (screenCode, screenMsg) -> {
Log.i(TAG, "[FLX-INIT] showFlexionScreens result=" + screenCode
+ " msg=" + screenMsg);
flexionScreensShown = true;
initBillingServiceAfterScreens(activity);
});
}
private static void initBillingServiceAfterScreens(Activity activity) {
Log.i(TAG, "[FLX-INIT] initBillingServiceAfterScreens start");
initBillingService(activity,
() -> {
Log.i(TAG, "[FLX-INIT] initBillingServiceAfterScreens success");
},
() -> {
Log.w(TAG, "[FLX-INIT] initBillingServiceAfterScreens failed");
});
}
private static void notifyFlexionInitResult(boolean success, String msg) {
TYUnityActivity.setSdkInited();
SDKManager.notifyInitResult(success, msg);
}
private static boolean isActivityAlive(Activity activity) {
return activity != null && !activity.isFinishing() && !activity.isDestroyed();
}
@Override @Override
public void pay(Activity activity, String productId, String productPrice, public void pay(Activity activity, String productId, String productPrice,
String productName, String productCount, String prodorderId, String productName, String productCount, String prodorderId,
String appInfo) { String appInfo) {
Log.i(TAG, "[PAY-FLOW] pay called: productId=" + productId + " billingConnected=" + billingConnected); // Log.i(TAG, "[PAY-FLOW] pay called: productId=" + productId + " billingConnected=" + billingConnected);
String developerPayload = ""; String developerPayload = "";
try { try {
byte[] decoded = Base64.getDecoder().decode(appInfo); byte[] decoded = Base64.getDecoder().decode(appInfo);
JSONObject json = new JSONObject(new String(decoded, "UTF-8")); JSONObject json = new JSONObject(new String(decoded, StandardCharsets.UTF_8));
Jsonobject json = new JSONObject();
JSONObject payload = new JSONObject(); JSONObject payload = new JSONObject();
String customData = json.optString("customData", ""); String customData = json.optString("customData", "");
payload.put("orderId", prodorderId); payload.put("orderId", prodorderId);
payload.put("userId", json.optString("userId", "")); payload.put("userId", json.optString("userId", ""));
payload.put("pfid", json.optString("pfid", "")); payload.put("pfid", json.optString("pfid", ""));
if (!customData.isEmpty()) { payload.put("customData", json.optString("customData", ""));
payload.put("customDataB64", Base64.getEncoder().encodeToString(customData.getBytes("UTF-8")));
} // if (!customData.isEmpty()) {
// payload.put("customData", parseCustomDataValue(customData));
// }
payload.put("prodPrice", productPrice); payload.put("prodPrice", productPrice);
payload.put("prodCount", productCount); payload.put("prodCount", productCount);
developerPayload = payload.toString(); developerPayload = Base64.getEncoder().encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
Log.i(TAG, "[PAY-CUSTOMDATA] customData length=" + customData.length() // Log.i(TAG, "[PAY-CUSTOMDATA] customData length=" + customData.length()
+ " developerPayload length=" + developerPayload.length()); // + " developerPayload length=" + developerPayload.length());
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "Failed to extract payload fields from appInfo: " + e.getMessage()); Log.w(TAG, "Failed to extract payload fields from appInfo: " + e.getMessage());
} }
@@ -153,11 +258,11 @@ public class FlexionSDKAdapter implements
sendPayCallback(1, "billing_not_ready", null, null, null, null); sendPayCallback(1, "billing_not_ready", null, null, null, null);
return; return;
} }
Log.i(TAG, "[PAY-FLOW] launching billing flow for: " + productId + " payload=" + finalPayload); // Log.i(TAG, "[PAY-FLOW] launching billing flow for: " + productId + " payload=" + finalPayload);
BillingResult result = bs.launchBillingFlow(activity, BillingResult result = bs.launchBillingFlow(activity,
new BillingFlowParams(productId, "inapp", finalPayload)); new BillingFlowParams(productId, "inapp", finalPayload));
Log.i(TAG, "[PAY-FLOW] launchBillingFlow result: code=" + result.getResponseCode() // Log.i(TAG, "[PAY-FLOW] launchBillingFlow result: code=" + result.getResponseCode()
+ " debugMsg=" + result.getDebugMessage()); // + " debugMsg=" + result.getDebugMessage());
if (result.getResponseCode() != 2000) { if (result.getResponseCode() != 2000) {
Log.e(TAG, "[PAY-FLOW] launchBillingFlow error: code=" + result.getResponseCode() Log.e(TAG, "[PAY-FLOW] launchBillingFlow error: code=" + result.getResponseCode()
+ " debugMsg=" + result.getDebugMessage()); + " debugMsg=" + result.getDebugMessage());
@@ -173,6 +278,21 @@ public class FlexionSDKAdapter implements
() -> sendPayCallback(1, "billing_not_ready", null, null, null, null)); () -> sendPayCallback(1, "billing_not_ready", null, null, null, null));
} }
private static Object parseCustomDataValue(String customData) {
String trimmed = customData == null ? "" : customData.trim();
try {
if (trimmed.startsWith("{")) {
return new JSONObject(trimmed);
}
if (trimmed.startsWith("[")) {
return new JSONArray(trimmed);
}
} catch (JSONException e) {
Log.w(TAG, "[PAY-CUSTOMDATA] customData json parse failed, keep raw string: " + e.getMessage());
}
return customData;
}
@Override @Override
public void queryProducts() { public void queryProducts() {
// Flexion 渠道不通过 thirdExtend 获取商品列表,由 queryProducts(String) 替代 // Flexion 渠道不通过 thirdExtend 获取商品列表,由 queryProducts(String) 替代
@@ -180,19 +300,29 @@ public class FlexionSDKAdapter implements
@Override @Override
public void queryProducts(String productIds) { public void queryProducts(String productIds) {
Log.i(TAG, "queryProducts(productIds) called, billingConnected=" + billingConnected); // Log.i(TAG, "queryProducts(productIds) called, billingConnected=" + billingConnected);
List<String> ids = Arrays.asList(productIds.split(",")); List<String> ids = Arrays.asList(productIds.split(","));
Log.i(TAG, "queryProductDetails with " + ids.size() + " items from config"); // Log.i(TAG, "queryProductDetails with " + ids.size() + " items from config");
ensureBillingReady(null, "queryProducts", ensureBillingReady(null, "queryProducts",
bs -> queryProductDetails(bs, ids), bs -> queryProductDetails(bs, ids),
() -> sendExtensionError("billing_not_ready")); () -> sendExtensionError("billing_not_ready"));
} }
private static void initBillingService(Activity activity) { private static void initBillingService(Activity activity) {
Log.i(TAG, "[PAY-FLOW] initBillingService"); initBillingService(activity, null, null);
}
private static void initBillingService(Activity activity, Runnable onReady, Runnable onFail) {
// Log.i(TAG, "[PAY-FLOW] initBillingService");
ensureBillingReady(activity, "initBillingService", ensureBillingReady(activity, "initBillingService",
bs -> queryPendingPurchases(), bs -> {
() -> Log.w(TAG, "[PAY-FLOW] initBillingService failed: billing not ready")); queryPendingPurchases(false);
if (onReady != null) onReady.run();
},
() -> {
Log.w(TAG, "[PAY-FLOW] initBillingService failed: billing not ready");
if (onFail != null) onFail.run();
});
} }
private static void ensureBillingReady(Activity activity, String operation, private static void ensureBillingReady(Activity activity, String operation,
@@ -216,16 +346,16 @@ public class FlexionSDKAdapter implements
targetActivity.runOnUiThread(() -> { targetActivity.runOnUiThread(() -> {
try { try {
if (billingService == null) { if (billingService == null) {
Log.i(TAG, "[PAY-FLOW] create BillingService operation=" + operation); // Log.i(TAG, "[PAY-FLOW] create BillingService operation=" + operation);
billingService = FLX.createBillingService(targetActivity, purchasesListener); billingService = FLX.createBillingService(targetActivity, purchasesListener);
} }
boolean ready = billingService != null && billingService.isReady(); boolean ready = billingService != null && billingService.isReady();
Log.i(TAG, "[PAY-FLOW] ensureBillingReady operation=" + operation // Log.i(TAG, "[PAY-FLOW] ensureBillingReady operation=" + operation
+ " connected=" + billingConnected // + " connected=" + billingConnected
+ " ready=" + ready // + " ready=" + ready
+ " connecting=" + billingConnecting // + " connecting=" + billingConnecting
+ " forceReconnect=" + forceReconnect); // + " forceReconnect=" + forceReconnect);
if (ready && !forceReconnect) { if (ready && !forceReconnect) {
billingConnected = true; billingConnected = true;
@@ -235,22 +365,22 @@ public class FlexionSDKAdapter implements
pendingBillingOperations.add(new PendingBillingOperation(operation, onReady, onFail)); pendingBillingOperations.add(new PendingBillingOperation(operation, onReady, onFail));
if (billingConnecting) { if (billingConnecting) {
Log.i(TAG, "[PAY-FLOW] billing connection already in progress, queued operation=" + operation); // Log.i(TAG, "[PAY-FLOW] billing connection already in progress, queued operation=" + operation);
return; return;
} }
billingConnecting = true; billingConnecting = true;
Log.i(TAG, "[PAY-FLOW] billing reconnect start operation=" + operation); // Log.i(TAG, "[PAY-FLOW] billing reconnect start operation=" + operation);
billingService.startConnection(targetActivity, new ConnectionStateListener() { billingService.startConnection(targetActivity, new ConnectionStateListener() {
@Override @Override
public void onBillingSetupFinished(BillingResult result) { public void onBillingSetupFinished(BillingResult result) {
int code = result.getResponseCode(); int code = result.getResponseCode();
Log.i(TAG, "[PAY-FLOW] onBillingSetupFinished: code=" + code); // Log.i(TAG, "[PAY-FLOW] onBillingSetupFinished: code=" + code);
billingConnecting = false; billingConnecting = false;
billingConnected = code == BillingResults.ResultCode.CONNECTION_SUCCESS_CODE; billingConnected = code == BillingResults.ResultCode.CONNECTION_SUCCESS_CODE;
drainBillingOperations(billingConnected); drainBillingOperations(billingConnected);
if (billingConnected) { if (billingConnected) {
queryPendingPurchases(); queryPendingPurchases(false);
} }
} }
@@ -282,8 +412,8 @@ public class FlexionSDKAdapter implements
private static void drainBillingOperations(boolean success) { private static void drainBillingOperations(boolean success) {
List<PendingBillingOperation> operations = new ArrayList<>(pendingBillingOperations); List<PendingBillingOperation> operations = new ArrayList<>(pendingBillingOperations);
pendingBillingOperations.clear(); pendingBillingOperations.clear();
Log.i(TAG, "[PAY-FLOW] drainBillingOperations success=" + success // Log.i(TAG, "[PAY-FLOW] drainBillingOperations success=" + success
+ " count=" + operations.size()); // + " count=" + operations.size());
for (PendingBillingOperation operation : operations) { for (PendingBillingOperation operation : operations) {
if (success) { if (success) {
if (operation.onReady != null) operation.onReady.run(billingService); if (operation.onReady != null) operation.onReady.run(billingService);
@@ -295,149 +425,283 @@ public class FlexionSDKAdapter implements
} }
private static void handlePurchase(Purchase purchase) { private static void handlePurchase(Purchase purchase) {
Log.i(TAG, "[PAY-FLOW] handlePurchase: state=" + purchase.getPurchaseState() // Log.i(TAG, "[PAY-FLOW] handlePurchase: state=" + purchase.getPurchaseState()
+ " orderId=" + purchase.getOrderId() // + " orderId=" + purchase.getOrderId()
+ " token=" + purchase.getToken()); // + " token=" + purchase.getToken());
if (purchase.getPurchaseState() == 0) { if (purchase.getPurchaseState() == 0) {
Log.i(TAG, "[PAY-FLOW] purchase success, sending to Unity for verification"); // Log.i(TAG, "[PAY-FLOW] purchase success, sending to Unity for verification");
sendPayCallback(0, "success", purchase.getOrderId(), sendPayCallback(0, "success", purchase.getOrderId(),
purchase.getPurchaseJson(), purchase.getSignature(), purchase.getToken()); purchase.getPurchaseJson(), purchase.getSignature(), purchase.getToken());
} else if (purchase.getPurchaseState() == 2) { } else if (purchase.getPurchaseState() == 2) {
Log.i(TAG, "[PAY-FLOW] purchase state=2 (pending), skip"); // Log.i(TAG, "[PAY-FLOW] purchase state=2 (pending), skip");
} }
} }
private static void queryPendingPurchases() { private static void queryPendingPurchases(boolean sendUnityCallback) {
if (sendUnityCallback) {
JSONArray cached = drainCachedPendingPurchases();
boolean cacheReady = isPendingPurchaseCacheReady();
if (cached.length() > 0 || cacheReady) {
Log.i(TAG, "[PAY-FLOW] queryPendingPurchases use cached result count=" + cached.length()
+ " cacheReady=" + cacheReady);
markPendingPurchaseCacheConsumed(cached);
sendPendingPurchasesCallback(0, null, cached);
return;
}
}
synchronized (pendingQueryLock) {
if (sendUnityCallback) {
pendingPurchaseUnityCallbackWaiting = true;
}
if (pendingPurchaseQueryInProgress) {
Log.i(TAG, "[PAY-FLOW] queryPendingPurchases wait running query"
+ " sendUnityCallback=" + sendUnityCallback
+ " cacheReady=" + pendingPurchaseCacheReady);
return;
}
pendingPurchaseQueryInProgress = true;
pendingPurchaseCacheReady = false;
}
Log.i(TAG, "[PAY-FLOW] queryPendingPurchases start: billingService=" + billingService Log.i(TAG, "[PAY-FLOW] queryPendingPurchases start: billingService=" + billingService
+ " billingConnected=" + billingConnected); + " billingConnected=" + billingConnected
+ " sendUnityCallback=" + sendUnityCallback);
ensureBillingReady(null, "queryPendingPurchases", ensureBillingReady(null, "queryPendingPurchases",
bs -> bs.queryPurchasesAsync( bs -> {
new QueryPurchasesParams("inapp"), billingExecutor.execute(() -> {
(result, purchases) -> { try {
Log.i(TAG, "[PAY-FLOW] queryPendingPurchases result: code=" + result.getResponseCode() bs.queryPurchasesAsync(
+ " count=" + (purchases == null ? 0 : purchases.size())); new QueryPurchasesParams("inapp"),
if (purchases != null) for (Purchase p : purchases) cachePendingPurchase(p); (result, purchases) -> {
}), int responseCode = result == null ? -1 : result.getResponseCode();
() -> Log.w(TAG, "[PAY-FLOW] queryPendingPurchases skipped: billing not connected")); String debugMessage = result == null ? "billing_result_null" : result.getDebugMessage();
Log.i(TAG, "[PAY-FLOW] queryPendingPurchases result: code=" + responseCode
+ " debugMsg=" + debugMessage
+ " count=" + (purchases == null ? 0 : purchases.size()));
if (responseCode != QUERY_PURCHASES_SUCCESS_CODE) {
boolean shouldCallback = finishPendingQueryFailed();
if (shouldCallback) {
sendPendingPurchasesCallback(1,
"query_pending_failed_" + responseCode + ": " + debugMessage,
new JSONArray());
}
return;
}
JSONArray response = new JSONArray();
if (purchases != null) {
for (Purchase p : purchases) {
JSONObject item = pendingPurchaseToJson(p);
if (item != null) response.put(item);
}
}
boolean shouldCallback = finishPendingQuerySuccess(response);
if (shouldCallback) {
sendPendingPurchasesCallback(0, null, response);
}
});
} catch (Exception e) {
Log.e(TAG, "[PAY-FLOW] queryPendingPurchases exception: " + e.getMessage());
boolean shouldCallback = finishPendingQueryFailed();
if (shouldCallback) {
sendPendingPurchasesCallback(1, "query_pending_exception: " + e.getMessage(), new JSONArray());
}
}
});
},
() -> {
Log.w(TAG, "[PAY-FLOW] queryPendingPurchases skipped: billing not connected");
boolean shouldCallback = finishPendingQueryFailed();
if (shouldCallback) sendPendingPurchasesCallback(1, "billing_not_ready", new JSONArray());
});
} }
private static void cachePendingPurchase(Purchase purchase) { @Override
if (purchase == null) { public void queryPendingPurchases() {
Log.w(TAG, "[PAY-FLOW] cachePendingPurchase skipped: purchase is null"); queryPendingPurchases(true);
return; }
private static boolean finishPendingQuerySuccess(JSONArray purchases) {
synchronized (pendingQueryLock) {
boolean shouldCallback = pendingPurchaseUnityCallbackWaiting;
if (shouldCallback) {
clearCachedPendingPurchases();
} else {
replaceCachedPendingPurchases(purchases);
}
pendingPurchaseQueryInProgress = false;
pendingPurchaseCacheReady = !shouldCallback || purchases == null || purchases.length() == 0;
pendingPurchaseUnityCallbackWaiting = false;
return shouldCallback;
} }
Log.i(TAG, "[PAY-FLOW] cachePendingPurchase state=" + purchase.getPurchaseState() }
+ " orderId=" + purchase.getOrderId() + " token=" + purchase.getToken());
if (purchase.getPurchaseState() != 0) return; private static boolean finishPendingQueryFailed() {
synchronized (pendingQueryLock) {
pendingPurchaseQueryInProgress = false;
pendingPurchaseCacheReady = false;
boolean shouldCallback = pendingPurchaseUnityCallbackWaiting;
pendingPurchaseUnityCallbackWaiting = false;
return shouldCallback;
}
}
private static boolean isPendingPurchaseCacheReady() {
synchronized (pendingQueryLock) {
return pendingPurchaseCacheReady;
}
}
private static void markPendingPurchaseCacheConsumed(JSONArray purchases) {
if (purchases == null || purchases.length() == 0) return;
synchronized (pendingQueryLock) {
pendingPurchaseCacheReady = false;
}
}
private static JSONObject pendingPurchaseToJson(Purchase purchase) {
if (purchase == null || purchase.getPurchaseState() != 0) return null;
try { try {
JSONObject result = new JSONObject(); JSONObject result = new JSONObject();
if (purchase.getOrderId() != null) result.put("orderId", purchase.getOrderId()); if (purchase.getOrderId() != null) result.put("orderId", purchase.getOrderId());
if (purchase.getPurchaseJson() != null) result.put("purchaseJson", purchase.getPurchaseJson()); if (purchase.getPurchaseJson() != null) result.put("purchaseJson", purchase.getPurchaseJson());
if (purchase.getSignature() != null) result.put("signature", purchase.getSignature()); if (purchase.getSignature() != null) result.put("signature", purchase.getSignature());
if (purchase.getToken() != null) result.put("purchaseToken", purchase.getToken()); if (purchase.getToken() != null) result.put("purchaseToken", purchase.getToken());
return result;
synchronized (pendingPurchases) {
String token = purchase.getToken();
for (String item : pendingPurchases) {
JSONObject cached = new JSONObject(item);
if (token != null && token.equals(cached.optString("purchaseToken", null))) {
Log.i(TAG, "[PAY-FLOW] pending purchase already cached token=" + token);
return;
}
}
pendingPurchases.add(result.toString());
}
Log.i(TAG, "[PAY-FLOW] cached pending purchase: orderId=" + purchase.getOrderId()
+ " cachedCount=" + pendingPurchases.size());
} catch (JSONException e) { } catch (JSONException e) {
Log.e(TAG, "[PAY-FLOW] cachePendingPurchase json error: " + e.getMessage()); Log.e(TAG, "[PAY-FLOW] pendingPurchaseToJson error: " + e.getMessage());
return null;
} }
} }
@Override private static JSONArray drainCachedPendingPurchases() {
public String getPendingPurchases() {
synchronized (pendingPurchases) { synchronized (pendingPurchases) {
Log.i(TAG, "[PAY-FLOW] getPendingPurchases start cachedCount=" + pendingPurchases.size()); Log.i(TAG, "[PAY-FLOW] drainCachedPendingPurchases start cachedCount=" + pendingPurchases.size()
+ " queryInProgress=" + pendingPurchaseQueryInProgress
+ " cacheReady=" + pendingPurchaseCacheReady);
JSONArray result = new JSONArray(); JSONArray result = new JSONArray();
for (String item : pendingPurchases) { for (String item : pendingPurchases) {
try { try {
result.put(new JSONObject(item)); result.put(new JSONObject(item));
} catch (JSONException e) { } catch (JSONException e) {
Log.e(TAG, "[PAY-FLOW] getPendingPurchases json error: " + e.getMessage()); Log.e(TAG, "[PAY-FLOW] drainCachedPendingPurchases json error: " + e.getMessage());
} }
} }
pendingPurchases.clear(); pendingPurchases.clear();
Log.i(TAG, "[PAY-FLOW] getPendingPurchases count=" + result.length()); return result;
return result.toString(); }
}
private static void replaceCachedPendingPurchases(JSONArray purchases) {
synchronized (pendingPurchases) {
pendingPurchases.clear();
if (purchases == null) return;
for (int i = 0; i < purchases.length(); i++) {
JSONObject item = purchases.optJSONObject(i);
if (item != null) pendingPurchases.add(item.toString());
}
Log.i(TAG, "[PAY-FLOW] replaceCachedPendingPurchases count=" + pendingPurchases.size());
}
}
private static void clearCachedPendingPurchases() {
synchronized (pendingPurchases) {
pendingPurchases.clear();
}
}
private static void sendPendingPurchasesCallback(int code, String errStr, JSONArray purchases) {
try {
JSONObject result = new JSONObject();
result.put("code", code);
if (errStr != null) result.put("errStr", errStr);
result.put("respObj", purchases == null ? "[]" : purchases.toString());
Log.i(TAG, "[PAY-FLOW] PendingPurchasesResult callback: code=" + code
+ " count=" + (purchases == null ? 0 : purchases.length()));
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME,
"PendingPurchasesResult", result.toString());
} catch (JSONException e) {
Log.e(TAG, "[PAY-FLOW] sendPendingPurchasesCallback json error: " + e.getMessage());
} }
} }
@Override @Override
public void refreshBilling() { public void refreshBilling() {
Log.i(TAG, "[PAY-FLOW] refreshBilling requested"); // Log.i(TAG, "[PAY-FLOW] refreshBilling requested");
billingConnected = false; billingConnected = false;
ensureBillingReady(null, "refreshBilling", ensureBillingReady(null, "refreshBilling",
bs -> Log.i(TAG, "[PAY-FLOW] refreshBilling ready"), bs -> {
// Log.i(TAG, "[PAY-FLOW] refreshBilling ready");
},
() -> Log.w(TAG, "[PAY-FLOW] refreshBilling failed"), () -> Log.w(TAG, "[PAY-FLOW] refreshBilling failed"),
true); true);
} }
private static void queryProductDetails(BillingService service, List<String> productIds) { private static void queryProductDetails(BillingService service, List<String> productIds) {
final boolean[] completed = {false}; final AtomicBoolean completed = new AtomicBoolean(false);
mainHandler.postDelayed(() -> { mainHandler.postDelayed(() -> {
if (completed[0]) return; if (!completed.compareAndSet(false, true)) return;
completed[0] = true;
Log.w(TAG, "[PAY-FLOW] queryProductDetails timeout count=" + productIds.size()); Log.w(TAG, "[PAY-FLOW] queryProductDetails timeout count=" + productIds.size());
sendExtensionError("flexion_query_timeout"); sendExtensionError("flexion_query_timeout");
}, 30000); }, 30000);
service.queryProductDetailsAsync( billingExecutor.execute(() -> {
new ProductDetailsParams("inapp", productIds), try {
(billingResult, productDetails) -> { service.queryProductDetailsAsync(
if (completed[0]) { new ProductDetailsParams("inapp", productIds),
Log.w(TAG, "[PAY-FLOW] queryProductDetails callback ignored after timeout"); (billingResult, productDetails) -> {
return; if (!completed.compareAndSet(false, true)) {
} Log.w(TAG, "[PAY-FLOW] queryProductDetails callback ignored after timeout");
completed[0] = true; return;
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); try {
result.put("respObj", arr.toString()); JSONObject result = new JSONObject();
} else { if (billingResult != null
result.put("code", 1); && billingResult.getResponseCode() == BillingResults.ResultCode.QUERY_PRODUCT_DETAILS_SUCCESS_CODE
result.put("thirdExtend_errStr", && productDetails != null) {
"flexion_query_failed: " + billingResult.getDebugMessage()); JSONArray arr = new JSONArray();
} for (ProductDetails detail : productDetails) {
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, JSONObject item = new JSONObject();
ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, result.toString()); item.put("productId", detail.getId());
} catch (JSONException e) { item.put("ourProductId", detail.getId());
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, item.put("title", detail.getTitle());
ConfigManager.PAY_TYPE_CALLBACK_FUNCTION_NAME, "{\"code\":1}"); 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 == null ? "billing_result_null" : 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}");
}
});
} catch (Exception e) {
if (!completed.compareAndSet(false, true)) return;
Log.e(TAG, "[PAY-FLOW] queryProductDetails exception: " + e.getMessage());
sendExtensionError("flexion_query_exception: " + e.getMessage());
}
});
} }
private static void sendPayCallback(int code, String errStr, String storeOrderId, private static void sendPayCallback(int code, String errStr, String storeOrderId,
String purchaseJson, String signature, String purchaseToken) { String purchaseJson, String signature, String purchaseToken) {
Log.i(TAG, "[PAY-FLOW] sendPayCallback: code=" + code + " errStr=" + errStr // Log.i(TAG, "[PAY-FLOW] sendPayCallback: code=" + code + " errStr=" + errStr
+ " storeOrderId=" + storeOrderId // + " storeOrderId=" + storeOrderId
+ " hasPurchaseJson=" + (purchaseJson != null) + " hasSignature=" + (signature != null) // + " hasPurchaseJson=" + (purchaseJson != null) + " hasSignature=" + (signature != null)
+ " hasToken=" + (purchaseToken != null)); // + " hasToken=" + (purchaseToken != null));
try { try {
JSONObject result = new JSONObject(); JSONObject result = new JSONObject();
result.put("code", code); result.put("code", code);
@@ -446,7 +710,7 @@ public class FlexionSDKAdapter implements
if (purchaseJson != null) result.put("purchaseJson", purchaseJson); if (purchaseJson != null) result.put("purchaseJson", purchaseJson);
if (signature != null) result.put("signature", signature); if (signature != null) result.put("signature", signature);
if (purchaseToken != null) result.put("purchaseToken", purchaseToken); if (purchaseToken != null) result.put("purchaseToken", purchaseToken);
Log.i(TAG, "[PAY-FLOW] UnitySendMessage callback: " + result.toString()); // Log.i(TAG, "[PAY-FLOW] UnitySendMessage callback: " + result.toString());
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME,
ConfigManager.PAY_CALLBACK_FUNCTION_NAME, result.toString()); ConfigManager.PAY_CALLBACK_FUNCTION_NAME, result.toString());
} catch (JSONException e) { } catch (JSONException e) {
@@ -465,17 +729,19 @@ public class FlexionSDKAdapter implements
Log.w(TAG, "[PAY-FLOW] consumePurchase: token is null"); Log.w(TAG, "[PAY-FLOW] consumePurchase: token is null");
return; return;
} }
Log.i(TAG, "[PAY-FLOW] consumePurchase: token=" + token); // Log.i(TAG, "[PAY-FLOW] consumePurchase: token=" + token);
ensureBillingReady(null, "consumePurchase", ensureBillingReady(null, "consumePurchase",
bs -> bs.consumeAsync( bs -> bs.consumeAsync(
new ConsumeParams(token), new ConsumeParams(token),
r -> Log.i(TAG, "[PAY-FLOW] consume result: " + r -> {
(r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail"))), // Log.i(TAG, "[PAY-FLOW] consume result: " +
// (r.getResponseCode() == BillingResults.ResultCode.CONSUME_SUCCESS_CODE ? "ok" : "fail"));
}),
() -> Log.w(TAG, "[PAY-FLOW] consumePurchase skipped: billing not connected")); () -> Log.w(TAG, "[PAY-FLOW] consumePurchase skipped: billing not connected"));
} }
public static void cancelPurchase() { public static void cancelPurchase() {
Log.i(TAG, "[PAY-FLOW] cancelPurchase: TODO - dismiss billing dialog"); // Log.i(TAG, "[PAY-FLOW] cancelPurchase: TODO - dismiss billing dialog");
} }
private static void sendExtensionError(String errStr) { private static void sendExtensionError(String errStr) {

View File

@@ -1,43 +0,0 @@
#!/bin/bash
# Usage: ./switch-channel.sh <channel>
# channel: googleplay | flexion | rustore
#
# 切换渠道后重新用 Unity 打 Android 包即可打出对应渠道的 APK。
# 脚本会同步AAR、Gradle 配置、AndroidManifest、Java 文件ConfigManager、ChannelHelpers 等)
CHANNEL=${1:-googleplay}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHANNEL_DIR="$SCRIPT_DIR/$CHANNEL"
PROJ_DIR="$SCRIPT_DIR/../../sdk-intergration"
TYSDK_PLUGINS="$PROJ_DIR/Packages/tysdk/Plugins/Android"
ASSETS_PLUGINS="$PROJ_DIR/Assets/Plugins/Android"
# 校验渠道目录
if [ ! -d "$CHANNEL_DIR" ]; then
echo "ERROR: Unknown channel '$CHANNEL'. Available: googleplay, flexion, rustore"
exit 1
fi
echo "=== Switching to channel: $CHANNEL ==="
# 1. Sync channel AAR to tysdk package (only tuyoosdk, gasdk stays permanently)
echo "[AAR] Syncing tuyoosdk to $TYSDK_PLUGINS"
find "$TYSDK_PLUGINS" -maxdepth 1 -name "tuyoosdk_*.aar" -delete
cp "$CHANNEL_DIR/Plugins/Android/"*.aar "$TYSDK_PLUGINS/" 2>/dev/null
# 2. Sync Gradle + Manifest to Assets/Plugins/Android
echo "[Config] Syncing gradle/manifest to $ASSETS_PLUGINS"
for f in "$CHANNEL_DIR"/*.gradle "$CHANNEL_DIR"/*.properties "$CHANNEL_DIR"/AndroidManifest.xml; do
[ -f "$f" ] && cp "$f" "$ASSETS_PLUGINS/"
done
# 3. Sync Java files to tysdk package
echo "[Java] Syncing ConfigManager + ChannelHelpers to $TYSDK_PLUGINS"
for f in "$CHANNEL_DIR"/*.java; do
[ -f "$f" ] && cp "$f" "$TYSDK_PLUGINS/"
done
echo "=== Done. Channel: $CHANNEL ==="
echo ""
echo "Next: Open Unity and build Android APK."

View File

@@ -0,0 +1,144 @@
# EnjoyPay 本次接入改动记录
## 本次目标
为 Android 测试工程增加 EnjoyPay 渠道切换与打包配置,使 Unity 菜单可以切换到 EnjoyPay 并使用俄罗斯包参数进行打包测试。
## 参数来源
- wiki MCP 文档:
- `https://wiki.bamboogames.top/doc/5oqa5pyv5rkf6yca-dF8FesCMt3`
- `https://wiki.bamboogames.top/doc/sdk-PiMyYd30k5`
- PDF 参数文档:
- `D:/.devup/【128-FishingTravel】【Google俄罗斯】接入支持.pdf`
- Firebase 配置:
- `D:/.devup/google-services (1).json`
- `D:/.devup/google-services (2).json`
两份 `google-services` 文件经 `fc` 对比完全一致,已使用其中一份作为 EnjoyPay 渠道配置。
## 已确认参数
- 渠道名:`EnjoyPay`
- Android 包名:`com.arkgame.ft.ep`
- 客户端支付类型:`enjoypay.global.h5.web`
- clientId`Android_5.00_tyGuest,facebook.googleplay.0-hall20587.googleplay.FishingMasterRussia`
- Firebase project id`fishing-travel-ep-1ab1e`
- Firebase package name`com.arkgame.ft.ep`
## 代码改动
### 1. 增加 EnjoyPay 切渠道入口
文件:
- `sdk-intergration/Assets/Editor/ChannelBuildTool.cs`
改动:
- `SupportedChannels` 增加 `EnjoyPay`
- 增加菜单 `Tools/Switch Channel/EnjoyPay`
- 增加菜单 `Tools/Build Android (Channel)/EnjoyPay`
- 增加菜单 `Tools/Build Android (Channel)/EnjoyPay Debug`
- `SwitchChannel("EnjoyPay")` 设置 Android bundle id 为 `com.arkgame.ft.ep`
### 2. 增加 EnjoyPay 支付类型映射
文件:
- `sdk-intergration/Packages/tysdk/Runtime/ChannelConfig.cs`
改动:
```csharp
"EnjoyPay" => "enjoypay.global.h5.web",
```
说明:
- 客户端传给 SDK 的 `paytype` 使用 `enjoypay.global.h5.web`
- PDF 中的 `enj` 是后台支付方式配置类型,不作为客户端 `paytype`
### 3. 增加 EnjoyPay 渠道配置目录
目录:
- `ExtraPluginCfgs/Android/enjoypay`
包含:
- `AndroidManifest.xml`
- `ChannelEntry.java`
- `ConfigManager.java`
- `launcherTemplate.gradle`
- `mainTemplate.gradle`
- `settingsTemplate.gradle`
- `gradleTemplate.properties`
- `Files/google-services.json`
- `Plugins/Android/tuyoosdk_1.0.0.aar`
关键配置:
- `AndroidManifest.xml`
- package`com.arkgame.ft.ep`
- `CHANNEL = EnjoyPay`
- `ChannelEntry.java`
- `CHANNEL = "EnjoyPay"`
- 不创建专用 adapter继续走默认 Tuyoo SDK 流程。
- `ConfigManager.java`
- `SDK_CLIENTID = "Android_5.00_tyGuest,facebook.googleplay.0-hall20587.googleplay.FishingMasterRussia"`
- `Files/google-services.json`
- 来自 `D:/.devup/google-services (1).json`
- package name`com.arkgame.ft.ep`
### 4. Firebase / Gradle 配置核对
Unity Gradle 模板对应关系:
- 项目级 Gradle
- `sdk-intergration/Assets/Plugins/Android/baseProjectTemplate.gradle`
- 当前已有 `com.google.gms.google-services` 插件版本声明。
- 应用模块 Gradle
- `ExtraPluginCfgs/Android/enjoypay/launcherTemplate.gradle`
- 当前已应用 `com.google.gms.google-services` 插件。
- UnityLibrary 模块 Gradle
- `ExtraPluginCfgs/Android/enjoypay/mainTemplate.gradle`
- 当前包含 Firebase Analytics 依赖。
## 未改动 / 延后项
- 当前测试工程不新增 EnjoyPay 商品列表逻辑。
- 正式项目接入时,再从正式项目本地 json table / 商品表构造 SKU。
- 当前不新增 EnjoyPay 专用 Billing adapter。
- 当前不复用 Flexion / RuStore 的 pending、consume、receipt 验签逻辑。
- 后台支付方式无需本次配置。
## 打包前检查
在 Unity 执行 `Tools/Switch Channel/EnjoyPay` 后,检查:
1. `sdk-intergration/Packages/tysdk/Plugins/Android/ChannelEntry.java`
- `CHANNEL = "EnjoyPay"`
2. `sdk-intergration/Packages/tysdk/Plugins/Android/ConfigManager.java`
- `SDK_CLIENTID` 为俄罗斯包 clientId。
3. `sdk-intergration/Assets/Plugins/Android/AndroidManifest.xml`
- package 为 `com.arkgame.ft.ep`
- `CHANNEL = EnjoyPay`
4. `sdk-intergration/Packages/tysdk/Files/google-services.json`
- package name 为 `com.arkgame.ft.ep`
5. `sdk-intergration/Packages/tysdk/Plugins/Android/tuyoosdk_1.0.0.aar`
- 应同步为 EnjoyPay 目录中的新版 AAR。
## 打包步骤
1. 打开 Unity 工程:`sdk-intergration`
2. 执行菜单:`Tools/Switch Channel/EnjoyPay`
3. 执行菜单:`Tools/Build Android (Channel)/EnjoyPay Debug`
4. 安装生成的 APK。
5. 验证初始化、登录、支付拉起和 SDK 回调。
## 已知注意点
- `google-services.json` 的 package name 是 `com.arkgame.ft.ep`,所以 EnjoyPay 包名必须与其一致。
- 已删除历史脚本 `ExtraPluginCfgs/Android/switch-channel.sh`,正式入口是 Unity 菜单中的 `ChannelBuildTool.SwitchChannel`
- 当前仓库内 `SDKTest.cs` 存在行尾状态提示;忽略行尾后无业务差异。

View File

@@ -0,0 +1,172 @@
# EnjoyPay Android 接入开发步骤
## 资料来源
- 主要依据:通过 `wiki` MCP 读取的文档
- `https://wiki.bamboogames.top/doc/5oqa5pyv5rkf6yca-dF8FesCMt3`
- `https://wiki.bamboogames.top/doc/sdk-PiMyYd30k5`
- 辅助参考:`D:/.devup/【Enjoypay三方支付】接入指引.pdf`
- 参数来源:`D:/.devup/【128-FishingTravel】【Google俄罗斯】接入支持.pdf`
## 参数读取规则
- EnjoyPay / Google 俄罗斯包相关参数统一从 `【128-FishingTravel】【Google俄罗斯】接入支持.pdf` 读取。
- 当前代码侧只写入打包和 SDK 初始化必需、且文档中已经明确给出的参数。
- PDF 中覆盖的参数范围包括:
- 基础信息包名、clientId、服务器地址等。
- EnjoyPayEnjoyPay 应用 AppId、商户号、AppSecret、计费点配置、发货通知地址、退款通知地址等。
- AppsFlyer`af_appid``devkey`
- Firebase`google-services.json`、Gradle / Firebase 依赖说明。
- AIHelpApp key、domain、Android platform 配置。
- 设备指纹businessId、secretId、secretKey。
- 广告变现:按平台申请对应参数。
## 已确认结论
- 渠道名:`EnjoyPay`
- Android 包名:`com.arkgame.ft.ep`
- 俄罗斯包 clientId`Android_5.00_tyGuest,facebook.googleplay.0-hall20587.googleplay.FishingMasterRussia`
- 客户端支付类型:`enjoypay.global.h5.web`
- 支付入口:复用现有 Tuyoo SDK `SDKAPI.payNew` 支付链路。
- SDK 依赖:需要替换为支持 EnjoyPay 的新版 Tuyoo AAR。
- 商品列表:当前测试工程不处理;接入正式项目时再从正式项目本地 json table / 表格数据构造 SKU。
- 服务端发货:复用现有 OrderFunc / `WaitForSvrOrder` / `ConfirmOrder` 流程。
- 不新增独立 EnjoyPay Billing adapter。
- 不复用 Flexion / RuStore 的 pending、consume、receipt 验签逻辑。
- 后台支付方式无需本次配置。
## 开发步骤
### 1. 在 ChannelBuildTool 中增加 EnjoyPay 渠道
文件:
- `sdk-intergration/Assets/Editor/ChannelBuildTool.cs`
改动:
- `SupportedChannels` 增加 `EnjoyPay`
- 增加菜单 `Tools/Switch Channel/EnjoyPay`
- 增加菜单 `Tools/Build Android (Channel)/EnjoyPay`
- 增加菜单 `Tools/Build Android (Channel)/EnjoyPay Debug`
- `SwitchChannel` 中为 `EnjoyPay` 设置 bundle id`com.arkgame.ft.ep`
- 已删除历史脚本 `ExtraPluginCfgs/Android/switch-channel.sh`;真实入口是 `ChannelBuildTool.SwitchChannel`
### 2. 增加 EnjoyPay 渠道配置目录
目录:
- `ExtraPluginCfgs/Android/enjoypay`
需要包含:
- `ChannelEntry.java`
- `ConfigManager.java`
- `AndroidManifest.xml`
- `mainTemplate.gradle`
- `launcherTemplate.gradle`
- `settingsTemplate.gradle`
- `gradleTemplate.properties`
- `Plugins/Android/<新版 Tuyoo AAR>`
- 可选:`Files/google-services.json`,仅在 EnjoyPay / Appsflyer / Firebase 需要独立配置时添加。
关键配置:
- `ChannelEntry.CHANNEL = "EnjoyPay"`
- Manifest package`com.arkgame.ft.ep`
- Manifest 渠道标识:`CHANNEL = EnjoyPay`
- `ConfigManager.SDK_CLIENTID` 使用参数文档中的俄罗斯包 clientId`Android_5.00_tyGuest,facebook.googleplay.0-hall20587.googleplay.FishingMasterRussia`
### 3. 配置 Unity 侧支付类型
文件:
- `sdk-intergration/Packages/tysdk/Runtime/ChannelConfig.cs`
增加映射:
```csharp
"EnjoyPay" => "enjoypay.global.h5.web",
```
说明:
- `enj` 是 SDK 后台“支付方式配置”使用的类型。
- `enj` 不是客户端传给 `SDKAPI.payNew``paytype`
### 4. Java 支付链路保持默认实现
文件:
- `sdk-intergration/Packages/tysdk/Plugins/Android/SDKManager.java`
预期行为:
- EnjoyPay 不新增专用 `IPaymentAdapter`
- `UnityKnowNew(...)` 走现有默认支付链路:
```java
SDKAPI.getIns().payNew(payData, payType, mPay);
```
其中 `payType.paytype` 由 Unity 传入,值为 `enjoypay.global.h5.web`
### 5. 商品列表接入延后到正式项目
当前测试工程:
- 不新增 EnjoyPay 商品列表逻辑。
- 不改 `SDKTest` 的商品列表测试流程。
正式项目接入时:
- EnjoyPay 不调用渠道商品列表查询接口。
- 客户端从正式项目本地 table / json 构造 SKU 列表。
- `productId``ourProductId` 使用同一个商品具体 ID。
- 如果 `TbIAPItemList.ProductId1` 就是 EnjoyPay 支付所需的商品具体 ID则直接使用 `ProductId1` 构造 SKU并复用现有 `SKUDetail.ProdID -> TYSdkFacade.Pay -> SDKAPI.payNew` 链路。
### 6. 替换新版 Tuyoo AAR
预期行为:
- 将支持 EnjoyPay 的新版 Tuyoo AAR 放入:
```text
ExtraPluginCfgs/Android/enjoypay/Plugins/Android/
```
- `ChannelBuildTool.SwitchChannel("EnjoyPay")` 会同步到:
```text
sdk-intergration/Packages/tysdk/Plugins/Android/
```
### 7. 后台与配置系统工作
这些工作在代码外完成:
- 配置 EnjoyPay AppId、商户号、AppSecret。
- 配置 EnjoyPay 计费点。
- 配置途游商品 ID 与 EnjoyPay 计费点 ID 映射。
- 确认 Appsflyer 是否需要与 RuStore 分开统计,以及是否需要独立 Appsflyer 配置。
## 验证清单
1. 在 Unity 执行 `Tools/Switch Channel/EnjoyPay`
2. 确认 `sdk-intergration/Packages/tysdk/Plugins/Android/ChannelEntry.java``CHANNEL = "EnjoyPay"`
3. 确认 Android bundle id 为 `com.arkgame.ft.ep`
4. 确认 `ChannelConfig.PayType` 在 EnjoyPay 渠道下返回 `enjoypay.global.h5.web`
5. 确认 `SDKManager.UnityKnowNew` 使用默认 `SDKAPI.payNew` 链路。
6. 确认 EnjoyPay 未启用 Flexion / RuStore 的 pending、consume、receipt 逻辑。
7. 构建 `Tools/Build Android (Channel)/EnjoyPay Debug`
8. 测试支付拉起与 SDK 回调。
9. 确认 OrderFunc 匹配和发货流程复用现有逻辑。
## 待确认项
- 新版 Tuyoo AAR 的具体文件名和版本。
- EnjoyPay / Google 俄罗斯包是否需要独立 `google-services.json`,以参数 PDF 中最新文件为准。
- 外链支付回跳是否需要新增 Android `scheme / host / path / intent-filter`
- 正式项目接入时的本地商品表字段映射。
- 是否必须展示 RUB 价格;如果需要,确认 RUB 金额来自哪张表、哪个字段。
- 参数 PDF 与 `google-services.json` 中的包名均为 `com.arkgame.ft.ep`;当前代码按 `com.arkgame.ft.ep` 执行。

View File

@@ -5,7 +5,7 @@ using UnityEngine;
public class ChannelBuildTool public class ChannelBuildTool
{ {
public static readonly string[] SupportedChannels = { "GooglePlay", "Rustore", "Flexion" }; public static readonly string[] SupportedChannels = { "GooglePlay", "Rustore", "Flexion", "EnjoyPay" };
private const string DefaultChannel = "GooglePlay"; private const string DefaultChannel = "GooglePlay";
private const string DefaultAndroidBundleIdentifier = "com.arkgame.ft"; private const string DefaultAndroidBundleIdentifier = "com.arkgame.ft";
@@ -101,6 +101,7 @@ public class ChannelBuildTool
var bundleId = channel switch var bundleId = channel switch
{ {
"Flexion" => "com.arkgame.ft.flexion", "Flexion" => "com.arkgame.ft.flexion",
"EnjoyPay" => "com.arkgame.ft.ep",
_ => "com.arkgame.ft" _ => "com.arkgame.ft"
}; };
PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, bundleId); PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, bundleId);
@@ -176,6 +177,9 @@ public class ChannelBuildTool
[MenuItem("Tools/Switch Channel/Flexion")] [MenuItem("Tools/Switch Channel/Flexion")]
public static void Switch_Flexion() => SwitchChannel("Flexion"); public static void Switch_Flexion() => SwitchChannel("Flexion");
[MenuItem("Tools/Switch Channel/EnjoyPay")]
public static void Switch_EnjoyPay() => SwitchChannel("EnjoyPay");
// ==================== Menu: Build ==================== // ==================== Menu: Build ====================
[MenuItem("Tools/Build Android (Channel)/GooglePlay")] [MenuItem("Tools/Build Android (Channel)/GooglePlay")]
@@ -195,4 +199,10 @@ public class ChannelBuildTool
[MenuItem("Tools/Build Android (Channel)/Flexion Debug")] [MenuItem("Tools/Build Android (Channel)/Flexion Debug")]
public static void Build_Flexion_Debug() => BuildAndroidForChannel("Flexion", true); public static void Build_Flexion_Debug() => BuildAndroidForChannel("Flexion", true);
[MenuItem("Tools/Build Android (Channel)/EnjoyPay")]
public static void Build_EnjoyPay() => BuildAndroidForChannel("EnjoyPay", false);
[MenuItem("Tools/Build Android (Channel)/EnjoyPay Debug")]
public static void Build_EnjoyPay_Debug() => BuildAndroidForChannel("EnjoyPay", true);
} }

View File

@@ -82,10 +82,10 @@ public class SDKTest : MonoBehaviour
//====================== Test Server =========================== //====================== Test Server ===========================
//=============================================================================== //===============================================================================
// //
_InitBtn.onClick.AddListener(Init); _InitBtn.onClick.AddListener(Init);
TYSdkFacade.OnInitComplete += (success, msg) => TYSdkFacade.OnInitComplete += (success, msg) =>
{ {
_messageTxt.text = success ? $"Init OK: {msg}" : $"Init Failed: {msg}"; _messageTxt.text = success ? $"Init OK: {msg}" : $"Init Failed: {msg}";
}; };
_loginGuestBtn.onClick.AddListener(() => OnLogin(EAccoutType.hwGuest)); _loginGuestBtn.onClick.AddListener(() => OnLogin(EAccoutType.hwGuest));
_loginGoogleBtn.onClick.AddListener(() => OnLogin(EAccoutType.hwGoogle)); _loginGoogleBtn.onClick.AddListener(() => OnLogin(EAccoutType.hwGoogle));
@@ -223,6 +223,18 @@ public class SDKTest : MonoBehaviour
type = "inapp" type = "inapp"
}; };
} }
else if (ChannelConfig.Channel.Equals("EnjoyPay"))
{
sku = new SKUDetail
{
productId = "comarkgameftpack0199",
ourProductId = "comarkgameftpack0199",
price = "1.99",
price_amount_micros = "1990000",
title = "EnjoyPay Test Pack",
type = "inapp"
};
}
if (sku == null){ if (sku == null){
_messageTxt.text = "no product"; _messageTxt.text = "no product";
return; return;
@@ -240,18 +252,18 @@ public class SDKTest : MonoBehaviour
// purchaseInfo["customData"] ="{\"BuyType\":{\"type\":1,\"ID\":100502,\"key\":null,\"endDay\":0,\"extraPurcheData\":null,\"IsHide\":false},\"IAPID\":302,\"ItemData\":[{\"id\":1005,\"curCount\":22728,\"count\":260}],\"ShowType\":8,\"CONTENT_ID\":\"com.arkgame.ft.pack0499\",\"PRICE\":4.99,\"CURRENCY\":\"USD\",\"ProdPrice\":4.99,\"extraPurcheData\":{\"purchase_count\":9,\"trade_name\":\"商店-钻石直充4.99\",\"trade_type\":\"商店-钻石直充\",\"usd_price\":4.99,\"iap_id\":302,\"drop_id\":1005,\"trade_group\":\"商店\",\"item_1005\":260},\"orderId\":null,\"time\":\"5/20/2026 3:59:07 AM\"}"; // purchaseInfo["customData"] ="{\"BuyType\":{\"type\":1,\"ID\":100502,\"key\":null,\"endDay\":0,\"extraPurcheData\":null,\"IsHide\":false},\"IAPID\":302,\"ItemData\":[{\"id\":1005,\"curCount\":22728,\"count\":260}],\"ShowType\":8,\"CONTENT_ID\":\"com.arkgame.ft.pack0499\",\"PRICE\":4.99,\"CURRENCY\":\"USD\",\"ProdPrice\":4.99,\"extraPurcheData\":{\"purchase_count\":9,\"trade_name\":\"商店-钻石直充4.99\",\"trade_type\":\"商店-钻石直充\",\"usd_price\":4.99,\"iap_id\":302,\"drop_id\":1005,\"trade_group\":\"商店\",\"item_1005\":260},\"orderId\":null,\"time\":\"5/20/2026 3:59:07 AM\"}";
purchaseInfo["userId"] = TYSdkFacade.TYAccountInfo.strUserId; purchaseInfo["userId"] = TYSdkFacade.TYAccountInfo.strUserId;
PaymentInfo payResult = await TYSdkFacade.Instance.Pay(sku, 1, sku.ProdPrice, purchaseInfo); PaymentInfo payResult = await TYSdkFacade.Instance.Pay(sku, 1, sku.ProdPrice, purchaseInfo);
_messageTxt.text = "\n" + $"Success : {payResult.isSuccess}"; _messageTxt.text = "\n" + $"Success : {payResult.isSuccess}";
_messageTxt.text += "\n" + $"message : {payResult.msg}"; _messageTxt.text += "\n" + $"message : {payResult.msg}";
_messageTxt.text += "\n" + $"orderId : {payResult.orderId}"; _messageTxt.text += "\n" + $"orderId : {payResult.orderId}";
_messageTxt.text += "\n" + $"storeOrderId : {payResult.storeOrderId}"; _messageTxt.text += "\n" + $"storeOrderId : {payResult.storeOrderId}";
var consumeToken = payResult.Receipt?.PurchaseToken ?? payResult.purchaseToken; var consumeToken = payResult.Receipt?.PurchaseToken ?? payResult.purchaseToken;
_messageTxt.text += "\n" + $"hasToken : {!string.IsNullOrEmpty(consumeToken)}"; _messageTxt.text += "\n" + $"hasToken : {!string.IsNullOrEmpty(consumeToken)}";
if (!string.IsNullOrEmpty(consumeToken)) if (!string.IsNullOrEmpty(consumeToken))
{ {
Debug.Log($"[SDKTest] Consume purchase token after pay result token={consumeToken}"); Debug.Log($"[SDKTest] Consume purchase token after pay result token={consumeToken}");
UnityBridgeFunc.ConsumePurchase(consumeToken); UnityBridgeFunc.ConsumePurchase(consumeToken);
_messageTxt.text += "\nconsume requested"; _messageTxt.text += "\nconsume requested";
} }
} }

View File

@@ -16,6 +16,8 @@ import com.barton.log.logapi.IGASDK;
import com.facebook.share.model.ShareLinkContent; import com.facebook.share.model.ShareLinkContent;
import com.facebook.share.widget.MessageDialog; import com.facebook.share.widget.MessageDialog;
import com.facebook.share.widget.ShareDialog; import com.facebook.share.widget.ShareDialog;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
import com.google.android.play.core.review.ReviewException; import com.google.android.play.core.review.ReviewException;
import com.google.android.play.core.review.ReviewInfo; import com.google.android.play.core.review.ReviewInfo;
@@ -41,7 +43,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* Unified SDKManager for all channels (GooglePlay / Flexion / RuStore). * Unified SDKManager for all channels (GooglePlay / Flexion / RuStore / EnjoyPay).
* C# passes the channel string via InitSDK(activity, channel); Java dispatches * C# passes the channel string via InitSDK(activity, channel); Java dispatches
* internally via switch. All login/link/GA/share logic is shared. * internally via switch. All login/link/GA/share logic is shared.
* Channel-specific behaviour is limited to: Init, Pay, thirdExtend, Review. * Channel-specific behaviour is limited to: Init, Pay, thirdExtend, Review.
@@ -73,7 +75,7 @@ public class SDKManager {
} }
public interface IPendingPurchaseAdapter { public interface IPendingPurchaseAdapter {
String getPendingPurchases(); void queryPendingPurchases();
} }
public interface IBillingLifecycleAdapter { public interface IBillingLifecycleAdapter {
@@ -91,21 +93,31 @@ public class SDKManager {
} }
public static void ConsumePurchase(String token) { public static void ConsumePurchase(String token) {
Log.i(TAG, "ConsumePurchase: token=" + token); // Log.i(TAG, "ConsumePurchase: token=" + token);
if (sdkAdapter instanceof IPaymentAdapter) { if (sdkAdapter instanceof IPaymentAdapter) {
((IPaymentAdapter) sdkAdapter).consume(token); ((IPaymentAdapter) sdkAdapter).consume(token);
} }
} }
public static String GetPendingPurchases() { public static void QueryPendingPurchases() {
Log.i(TAG, "QueryPendingPurchases");
if (sdkAdapter instanceof IPendingPurchaseAdapter) { if (sdkAdapter instanceof IPendingPurchaseAdapter) {
return ((IPendingPurchaseAdapter) sdkAdapter).getPendingPurchases(); ((IPendingPurchaseAdapter) sdkAdapter).queryPendingPurchases();
return;
}
try {
JSONObject result = new JSONObject();
result.put("code", 0);
result.put("respObj", "[]");
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME,
"PendingPurchasesResult", result.toString());
} catch (JSONException e) {
Log.e(TAG, "QueryPendingPurchases fallback json error: " + e.getMessage());
} }
return "[]";
} }
public static void RefreshBillingService() { public static void RefreshBillingService() {
Log.i(TAG, "RefreshBillingService"); // Log.i(TAG, "RefreshBillingService");
if (sdkAdapter instanceof IBillingLifecycleAdapter) { if (sdkAdapter instanceof IBillingLifecycleAdapter) {
((IBillingLifecycleAdapter) sdkAdapter).refreshBilling(); ((IBillingLifecycleAdapter) sdkAdapter).refreshBilling();
} }
@@ -116,6 +128,24 @@ public class SDKManager {
// FlexionSDKAdapter.cancelPurchase(); // FlexionSDKAdapter.cancelPurchase();
} }
public static boolean IsGooglePlayServicesAvailable() {
try {
Activity activity = UnityPlayer.currentActivity;
if (activity == null) {
Log.w(TAG, "IsGooglePlayServicesAvailable: activity is null");
return false;
}
int resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity);
boolean available = resultCode == ConnectionResult.SUCCESS;
Log.i(TAG, "IsGooglePlayServicesAvailable: available=" + available + " resultCode=" + resultCode);
return available;
} catch (Exception e) {
Log.w(TAG, "IsGooglePlayServicesAvailable exception: " + e.getMessage());
return false;
}
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Init — with channel helper support // Init — with channel helper support
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -124,9 +154,9 @@ public class SDKManager {
channel = ChannelEntry.CHANNEL; channel = ChannelEntry.CHANNEL;
sdkAdapter = ChannelEntry.createSDKAdapter(); sdkAdapter = ChannelEntry.createSDKAdapter();
Log.i(TAG, "[CHANNEL-VERIFY] Java InitSDK channel=" + channel // Log.i(TAG, "[CHANNEL-VERIFY] Java InitSDK channel=" + channel
+ " fromCSharp=" + ch // + " fromCSharp=" + ch
+ " adapter=" + (sdkAdapter != null ? sdkAdapter.getClass().getSimpleName() : "default")); // + " adapter=" + (sdkAdapter != null ? sdkAdapter.getClass().getSimpleName() : "default"));
// Sync channel to C# ChannelConfig // Sync channel to C# ChannelConfig
UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME, UnityPlayer.UnitySendMessage(ConfigManager.UNITY_FACADE_NAME,
@@ -431,6 +461,13 @@ public class SDKManager {
public static void UnityKnowNew(String productId, String productPrice, String productName, public static void UnityKnowNew(String productId, String productPrice, String productName,
String productCount, String prodorderId, String appInfo, String productCount, String prodorderId, String appInfo,
String ptype) { String ptype) {
SDKLog.i("[SDKManager.UnityKnowNew] channel=" + channel
+ ", ptype=" + ptype
+ ", productId=" + productId
+ ", productPrice=" + productPrice
+ ", productName=" + productName
+ ", productCount=" + productCount
+ ", orderId=" + prodorderId);
if (sdkAdapter instanceof IPaymentAdapter) { if (sdkAdapter instanceof IPaymentAdapter) {
((IPaymentAdapter) sdkAdapter).pay(UnityPlayer.currentActivity, productId, productPrice, productName, productCount, prodorderId, appInfo); ((IPaymentAdapter) sdkAdapter).pay(UnityPlayer.currentActivity, productId, productPrice, productName, productCount, prodorderId, appInfo);
return; return;
@@ -448,6 +485,7 @@ public class SDKManager {
payData.payReq = mPayReq; payData.payReq = mPayReq;
PayType payType = new PayType(); PayType payType = new PayType();
payType.paytype = ptype; payType.paytype = ptype;
SDKLog.i("[SDKManager.UnityKnowNew] payNew payType.paytype=" + payType.paytype);
SDKAPI.getIns().payNew(payData, payType, mPay); SDKAPI.getIns().payNew(payData, payType, mPay);
} }
@@ -515,13 +553,13 @@ public class SDKManager {
public static void SetGaUserInfo(String userId) { public static void SetGaUserInfo(String userId) {
Log.e(TAG, "SetGaUserInfo : " + userId); Log.e(TAG, "SetGaUserInfo : " + userId);
SDKLog.i("SetGaUserInfo : " + userId); // SDKLog.i("SetGaUserInfo : " + userId);
GetGameGa().setUserId(userId); GetGameGa().setUserId(userId);
} }
public static void SetGaCommonInfo(String SetGaCommonInfo) { public static void SetGaCommonInfo(String SetGaCommonInfo) {
Log.e(TAG, "SetGaCommonInfo : " + SetGaCommonInfo); Log.e(TAG, "SetGaCommonInfo : " + SetGaCommonInfo);
SDKLog.i("SetGaCommonInfo : " + SetGaCommonInfo); // SDKLog.i("SetGaCommonInfo : " + SetGaCommonInfo);
Gson gson = new Gson(); Gson gson = new Gson();
Map<String, Object> map = gson.fromJson(SetGaCommonInfo, Map<String, Object> map = gson.fromJson(SetGaCommonInfo,
new TypeToken<Map<String, Object>>() {}.getType()); new TypeToken<Map<String, Object>>() {}.getType());

View File

@@ -8,6 +8,7 @@ namespace tysdk
{ {
"Rustore" => "rustore.global.app", "Rustore" => "rustore.global.app",
"Flexion" => "flexion.global.app", "Flexion" => "flexion.global.app",
"EnjoyPay" => "enjoypay.global.h5.web",
_ => "googleiab.global.app" _ => "googleiab.global.app"
}; };

View File

@@ -106,6 +106,9 @@ namespace tysdk
private static TYSdkFacade _instance; private static TYSdkFacade _instance;
public static bool IsSdkInited { get; private set; } public static bool IsSdkInited { get; private set; }
public static bool IsInitStarted { get; private set; }
public static bool HasInitResult { get; private set; }
public static string LastInitMessage { get; private set; }
public static event System.Action<bool, string> OnInitComplete; public static event System.Action<bool, string> OnInitComplete;
public static TYSdkFacade Instance public static TYSdkFacade Instance
@@ -122,7 +125,10 @@ namespace tysdk
} }
public void Init() public void Init()
{ {
Debug.Log($"[TYSdkFacade] Init()"); Debug.Log("[FLX-INIT] TYSdkFacade Init");
IsInitStarted = true;
HasInitResult = false;
LastInitMessage = string.Empty;
#if UNITY_ANDROID #if UNITY_ANDROID
UnityBridgeFunc.InitSDK(ChannelConfig.Channel); UnityBridgeFunc.InitSDK(ChannelConfig.Channel);
#endif #endif
@@ -131,13 +137,13 @@ namespace tysdk
public void SetChannelFromNative(string channel) public void SetChannelFromNative(string channel)
{ {
Debug.Log($"[TYSdkFacade] SetChannelFromNative: {channel}"); // Debug.Log($"[TYSdkFacade] SetChannelFromNative: {channel}");
ChannelConfig.SetChannel(channel); ChannelConfig.SetChannel(channel);
} }
public void InitResult(string json) public void InitResult(string json)
{ {
Debug.Log($"[TYSdkFacade] InitResult: {json}"); Debug.Log($"[FLX-INIT] TYSdkFacade InitResult: {json}");
var success = false; var success = false;
var msg = string.Empty; var msg = string.Empty;
@@ -154,6 +160,8 @@ namespace tysdk
} }
IsSdkInited = success; IsSdkInited = success;
HasInitResult = true;
LastInitMessage = msg;
OnInitComplete?.Invoke(success, msg); OnInitComplete?.Invoke(success, msg);
} }
@@ -173,7 +181,7 @@ namespace tysdk
public void Logout() public void Logout()
{ {
UnityEngine.Debug.Log("Logout"); // UnityEngine.Debug.Log("Logout");
if (_accountInfo != null) if (_accountInfo != null)
{ {
UnityBridgeFunc.UnityLogOutByChannel(_accountInfo.channel); UnityBridgeFunc.UnityLogOutByChannel(_accountInfo.channel);
@@ -253,7 +261,7 @@ namespace tysdk
public async Task<LoginInfo> LoginBySns() public async Task<LoginInfo> LoginBySns()
{ {
Debug.Log("[TYSdkFacade] LoginBySns"); // Debug.Log("[TYSdkFacade] LoginBySns");
var task = TYSDKCallbackManager.Instance.RegisterCallback<LoginInfo>(TimeSpan.FromSeconds(30)); var task = TYSDKCallbackManager.Instance.RegisterCallback<LoginInfo>(TimeSpan.FromSeconds(30));
@@ -286,7 +294,7 @@ namespace tysdk
public void LoginResult(string json) public void LoginResult(string json)
{ {
Debug.Log("[TYSdkFacade] Login callback:"); // Debug.Log("[TYSdkFacade] Login callback:");
var result = new LoginInfo(); var result = new LoginInfo();
var data = JObject.Parse(json); var data = JObject.Parse(json);
var code = (int)data["code"]; var code = (int)data["code"];
@@ -553,7 +561,7 @@ namespace tysdk
//对sign进行MD5加密 //对sign进行MD5加密
string signMd5 = CalculateMD5Hash(sign); string signMd5 = CalculateMD5Hash(sign);
string url = $"{tycs}?{listStr}&sign={signMd5}&isAbroad=1&lan=en"; string url = $"{tycs}?{listStr}&sign={signMd5}&isAbroad=1&lan=en";
Debug.Log($"[TYSdkFacade:Signout] url \n{url}"); // Debug.Log($"[TYSdkFacade:Signout] url \n{url}");
Application.OpenURL(url); Application.OpenURL(url);
} }

View File

@@ -36,7 +36,7 @@ namespace tysdk
string extraInfo = purchaseInfo.ToString(Newtonsoft.Json.Formatting.None); string extraInfo = purchaseInfo.ToString(Newtonsoft.Json.Formatting.None);
//to base64 //to base64
extraInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes(extraInfo)); extraInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes(extraInfo));
Debug.Log("[TYSdk] extraInfo: " + extraInfo); // Debug.Log("[TYSdk] extraInfo: " + extraInfo);
var prodId = prod.ProdID; var prodId = prod.ProdID;
var prodPrice = usdprice.ToString(); var prodPrice = usdprice.ToString();
var prodName = prod.title; var prodName = prod.title;
@@ -59,6 +59,7 @@ namespace tysdk
#elif UNITY_ANDROID #elif UNITY_ANDROID
var pType = ChannelConfig.PayType; var pType = ChannelConfig.PayType;
Debug.Log($"[TYSdkFacade::Pay] Channel={ChannelConfig.Channel}, PayType={pType}, prodId={prodId}, price={prodPrice}, orderId={orderId}");
var task = TYSDKCallbackManager.Instance.RegisterCallback<PaymentInfo>(TimeSpan.FromSeconds(90)); var task = TYSDKCallbackManager.Instance.RegisterCallback<PaymentInfo>(TimeSpan.FromSeconds(90));
try try
{ {
@@ -68,7 +69,6 @@ namespace tysdk
result.orderId = orderId; result.orderId = orderId;
result.productId = prodId; result.productId = prodId;
result.price = prodPrice; result.price = prodPrice;
result.Check(orderId, prodId);
return result; return result;
} }
catch (Exception e) catch (Exception e)
@@ -109,7 +109,7 @@ namespace tysdk
//支付的回调 //支付的回调
public void PayResult(string json) public void PayResult(string json)
{ {
Debug.Log("[TYSdkFacade] pay callback:" + json); // Debug.Log("[TYSdkFacade] pay callback:" + json);
var jresult = JObject.Parse(json); var jresult = JObject.Parse(json);
var result = new PaymentInfo(); var result = new PaymentInfo();
result.code = jresult["code"].ToString(); result.code = jresult["code"].ToString();
@@ -204,20 +204,71 @@ namespace tysdk
//商品列表的回调 //商品列表的回调
public void SKUListResult(string json) public void SKUListResult(string json)
{ {
Debug.Log("[TYSdkFacade] GetSKUList callback"); // Debug.Log("[TYSdkFacade] GetSKUList callback");
var jresult = JObject.Parse(json);
int code = int.Parse(jresult["code"].ToString());
var result = new ProductListInfo(); var result = new ProductListInfo();
if (code == 0) try
{ {
string productStr = jresult["respObj"].ToString(); var jresult = JObject.Parse(json);
int code = jresult["code"]?.Value<int>() ?? 1;
result.code = code; result.code = code;
if (code == 0)
{
string productStr = jresult["respObj"]?.ToString();
#if UNITY_ANDROID && !UNITY_EDITOR #if UNITY_ANDROID && !UNITY_EDITOR
var jsonObjList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<SKUDetail>>(productStr); var jsonObjList = Newtonsoft.Json.JsonConvert.DeserializeObject<List<SKUDetail>>(productStr);
result.products = jsonObjList; result.products = jsonObjList;
#elif UNITY_IOS && !UNITY_EDITOR #elif UNITY_IOS && !UNITY_EDITOR
result.ReadSKUFromJson(productStr); result.ReadSKUFromJson(productStr);
#endif #endif
}
}
catch (Exception e)
{
Debug.LogWarning($"[TYSdkFacade] SKUListResult parse failed: {e.Message}");
result.code = 1;
result.msg = e.Message;
}
TYSDKCallbackManager.Instance.TryTriggerCallback(result);
}
public async Task<PendingPurchaseListInfo> QueryPendingPurchases()
{
#if UNITY_ANDROID && !UNITY_EDITOR
var task = TYSDKCallbackManager.Instance.RegisterCallback<PendingPurchaseListInfo>(TimeSpan.FromSeconds(30));
UnityBridgeFunc.QueryPendingPurchases();
try
{
return await task;
}
catch (TaskCanceledException e)
{
Debug.LogWarning($"[TYSdkFacade] QueryPendingPurchases timeout: {e.Message}");
return new PendingPurchaseListInfo { code = 1, msg = "pending_purchase_callback_timeout" };
}
#else
await Task.Yield();
return new PendingPurchaseListInfo { code = 0, purchasesJson = "[]" };
#endif
}
public void PendingPurchasesResult(string json)
{
Debug.Log($"[TYSdkFacade] PendingPurchasesResult: {json}");
var result = new PendingPurchaseListInfo();
try
{
var jresult = JObject.Parse(json);
result.code = jresult["code"]?.Value<int>() ?? 1;
result.msg = jresult["errStr"]?.ToString() ?? string.Empty;
result.purchasesJson = jresult["respObj"]?.ToString() ?? "[]";
}
catch (Exception e)
{
Debug.LogWarning($"[TYSdkFacade] PendingPurchasesResult parse failed: {e.Message}");
result.code = 1;
result.msg = e.Message;
result.purchasesJson = "[]";
} }
TYSDKCallbackManager.Instance.TryTriggerCallback(result); TYSDKCallbackManager.Instance.TryTriggerCallback(result);

View File

@@ -158,6 +158,13 @@ namespace tysdk
} }
public class PendingPurchaseListInfo
{
public int code = 1;
public string msg = string.Empty;
public string purchasesJson = "[]";
}
#if UNITY_IOS #if UNITY_IOS
public class SKUDetail public class SKUDetail
{ {

View File

@@ -46,10 +46,12 @@ namespace tysdk
public static void ConsumePurchase(string token) { } public static void ConsumePurchase(string token) { }
public static string GetPendingPurchases() => "[]"; public static void QueryPendingPurchases() { }
public static void RefreshBillingService() { } public static void RefreshBillingService() { }
public static bool IsGooglePlayServicesAvailable() { return true; }
//打点 //打点
public static void SetGaUserInfo(string userId){} public static void SetGaUserInfo(string userId){}
public static void SetGaCommonInfo(string SetGaCommonInfo){} public static void SetGaCommonInfo(string SetGaCommonInfo){}
@@ -218,11 +220,11 @@ namespace tysdk
} }
} }
public static string GetPendingPurchases() public static void QueryPendingPurchases()
{ {
using(var sdkManager = new AndroidJavaClass(SDK_CLASS)) using(var sdkManager = new AndroidJavaClass(SDK_CLASS))
{ {
return sdkManager.CallStatic<string>("GetPendingPurchases"); sdkManager.CallStatic("QueryPendingPurchases");
} }
} }
@@ -244,6 +246,14 @@ namespace tysdk
} }
} }
public static bool IsGooglePlayServicesAvailable()
{
using(var sdkManager = new AndroidJavaClass(SDK_CLASS))
{
return sdkManager.CallStatic<bool>("IsGooglePlayServicesAvailable");
}
}
//打点 //打点
public static void SetGaUserInfo(string userId) public static void SetGaUserInfo(string userId)
{ {
@@ -479,10 +489,12 @@ namespace tysdk
public static void ConsumePurchase(string token) { } public static void ConsumePurchase(string token) { }
public static string GetPendingPurchases() => "[]"; public static void QueryPendingPurchases() { }
public static void RefreshBillingService() { } public static void RefreshBillingService() { }
public static bool IsGooglePlayServicesAvailable() { return true; }
#endif #endif
} }
} }

View File

@@ -163,7 +163,7 @@ PlayerSettings:
androidSupportedAspectRatio: 1 androidSupportedAspectRatio: 1
androidMaxAspectRatio: 2.1 androidMaxAspectRatio: 2.1
applicationIdentifier: applicationIdentifier:
Android: com.arkgame.ft.flexion Android: com.arkgame.ft
Standalone: com.Unity-Technologies.com.unity.template.urp-blank Standalone: com.Unity-Technologies.com.unity.template.urp-blank
iPhone: com.arkgame.ft iPhone: com.arkgame.ft
buildNumber: buildNumber: