diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3f2ccca..bef7ba5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -38,11 +38,15 @@
-
+
+
+
+
diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..344635e
--- /dev/null
+++ b/android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 8be1cec..a6c7c3f 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,13 +1,91 @@
-import Flutter
-import UIKit
+import Flutter
+import Security
+import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-}
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ registerDurableAuthStorageChannel()
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ private func registerDurableAuthStorageChannel() {
+ guard let controller = window?.rootViewController as? FlutterViewController else {
+ return
+ }
+ let channel = FlutterMethodChannel(
+ name: "com.org.yumiparty/durable_auth_storage",
+ binaryMessenger: controller.binaryMessenger
+ )
+ channel.setMethodCallHandler { [weak self] call, result in
+ guard
+ let self,
+ let args = call.arguments as? [String: Any],
+ let key = args["key"] as? String
+ else {
+ result(FlutterError(code: "bad_args", message: "Missing key", details: nil))
+ return
+ }
+
+ switch call.method {
+ case "write":
+ guard let value = args["value"] as? String else {
+ result(FlutterError(code: "bad_args", message: "Missing value", details: nil))
+ return
+ }
+ self.writeKeychainValue(value, key: key)
+ result(nil)
+ case "read":
+ result(self.readKeychainValue(key: key))
+ case "delete":
+ self.deleteKeychainValue(key: key)
+ result(nil)
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+ }
+
+ private func keychainQuery(key: String) -> [String: Any] {
+ [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "com.org.yumiparty.durable_auth_storage",
+ kSecAttrAccount as String: key
+ ]
+ }
+
+ private func writeKeychainValue(_ value: String, key: String) {
+ deleteKeychainValue(key: key)
+ guard let data = value.data(using: .utf8) else {
+ return
+ }
+ var query = keychainQuery(key: key)
+ query[kSecValueData as String] = data
+ query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
+ SecItemAdd(query as CFDictionary, nil)
+ }
+
+ private func readKeychainValue(key: String) -> String? {
+ var query = keychainQuery(key: key)
+ query[kSecReturnData as String] = true
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+ guard
+ status == errSecSuccess,
+ let data = result as? Data
+ else {
+ return nil
+ }
+ return String(data: data, encoding: .utf8)
+ }
+
+ private func deleteKeychainValue(key: String) {
+ SecItemDelete(keychainQuery(key: key) as CFDictionary)
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 848e63a..506c5e5 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -27,6 +27,7 @@ import 'app/routes/sc_routes.dart';
import 'app/routes/sc_lk_application.dart';
import 'shared/tools/sc_deep_link_handler.dart';
import 'shared/tools/sc_deviceId_utils.dart';
+import 'shared/data_sources/sources/local/user_manager.dart';
import 'modules/splash/splash_page.dart';
import 'services/general/sc_app_general_manager.dart';
import 'services/payment/apple_payment_manager.dart';
@@ -94,6 +95,7 @@ Future _prepareAppShell() async {
SCKeybordUtil.init();
// 使用重试机制初始化存储
await _initStore();
+ await AccountStorage().restoreDurableSessionIfNeeded();
await SCDeviceIdUtils.initDeviceinfo();
_configureImageCache();
}
diff --git a/lib/modules/room_game/views/leader_game_page.dart b/lib/modules/room_game/views/leader_game_page.dart
index 25ca8a0..f5c0a36 100644
--- a/lib/modules/room_game/views/leader_game_page.dart
+++ b/lib/modules/room_game/views/leader_game_page.dart
@@ -346,24 +346,23 @@ class _LeaderGamePageState extends State {
return 0;
}
- double _calculateBodyHeight(BuildContext context, double headerHeight) {
+ double _calculateBodyHeight(BuildContext context) {
final screenMode = _resolveScreenMode();
final maxDialogHeight = resolveRoomGameAvailableHeight(
context,
- reservedTop: 24.w,
+ reservedTop: 12.w,
);
- final maxBodyHeight = maxDialogHeight - headerHeight;
- if (maxBodyHeight <= 0) {
+ if (maxDialogHeight <= 0) {
return 0;
}
if (screenMode == 'full') {
- return maxBodyHeight;
+ return maxDialogHeight;
}
return clampRoomGameHeight(
_calculatePreferredBodyHeight(context),
- maxBodyHeight,
+ maxDialogHeight,
);
}
@@ -544,8 +543,7 @@ class _LeaderGamePageState extends State {
@override
Widget build(BuildContext context) {
- final headerHeight = 44.w;
- final bodyHeight = _calculateBodyHeight(context, headerHeight);
+ final bodyHeight = _calculateBodyHeight(context);
final bottomFrameHeight = resolveRoomGameCompatibilityBottomHeight(context);
return PopScope(
canPop: false,
@@ -566,7 +564,7 @@ class _LeaderGamePageState extends State {
),
child: Container(
width: ScreenUtil().screenWidth,
- height: bodyHeight + headerHeight + bottomFrameHeight,
+ height: bodyHeight + bottomFrameHeight,
color: const Color(0xFF081915),
child: Stack(
children: [
@@ -574,13 +572,6 @@ class _LeaderGamePageState extends State {
left: 0,
right: 0,
top: 0,
- height: headerHeight,
- child: _buildHeader(),
- ),
- Positioned(
- left: 0,
- right: 0,
- top: headerHeight,
bottom: bottomFrameHeight,
child: WebViewWidget(
controller: _controller,
@@ -614,36 +605,6 @@ class _LeaderGamePageState extends State {
);
}
- Widget _buildHeader() {
- return Container(
- color: const Color(0xFF102D28),
- padding: EdgeInsets.symmetric(horizontal: 12.w),
- child: Row(
- children: [
- Expanded(
- child: Text(
- widget.game.name.isEmpty ? 'Leader Game' : widget.game.name,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- color: Colors.white,
- fontSize: 14.sp,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- IconButton(
- onPressed: () {
- unawaited(_closeAndExit());
- },
- icon: Icon(Icons.close, color: Colors.white, size: 20.w),
- splashRadius: 18.w,
- ),
- ],
- ),
- );
- }
-
Widget _buildErrorState() {
return Positioned.fill(
child: Container(
diff --git a/lib/modules/splash/splash_page.dart b/lib/modules/splash/splash_page.dart
index c73369f..5f4c7e0 100644
--- a/lib/modules/splash/splash_page.dart
+++ b/lib/modules/splash/splash_page.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
@@ -6,7 +7,11 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/shared/tools/sc_version_utils.dart';
+import 'package:yumi/shared/tools/sc_google_auth_utils.dart';
+import 'package:yumi/shared/data_sources/models/enum/sc_auth_type.dart';
+import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
+import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/app/routes/sc_routes.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
@@ -448,6 +453,15 @@ class _SplashPageState extends State {
var token = AccountStorage().getToken();
if (user != null && token.isNotEmpty) {
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
+ return;
+ }
+ final restoredUser = await _tryRestoreLoginSession();
+ if (!mounted) {
+ return;
+ }
+ if (restoredUser != null &&
+ (restoredUser.token ?? "").trim().isNotEmpty) {
+ SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
} else {
SCNavigatorUtils.pushLoginIfNeeded(context, replace: true);
}
@@ -459,6 +473,28 @@ class _SplashPageState extends State {
}
}
+ Future _tryRestoreLoginSession() async {
+ if (!Platform.isAndroid) {
+ return null;
+ }
+ try {
+ final googleUser = await SCGoogleAuthService.signInSilently();
+ final uid = googleUser?.uid ?? "";
+ if (uid.isEmpty) {
+ return null;
+ }
+ final user = await SCAccountRepository().loginForChannel(
+ SCAuthType.GOOGLE.name,
+ uid,
+ );
+ AccountStorage().setCurrentUser(user);
+ return user;
+ } catch (e) {
+ debugPrint('restore login session failed: $e');
+ return null;
+ }
+ }
+
/// 取消倒计时的计时器。
void _cancelTimer() {
// 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。
diff --git a/lib/modules/user/vip/vip_detail_page.dart b/lib/modules/user/vip/vip_detail_page.dart
index e005536..28bd00a 100644
--- a/lib/modules/user/vip/vip_detail_page.dart
+++ b/lib/modules/user/vip/vip_detail_page.dart
@@ -58,6 +58,7 @@ class _VipDetailPageState extends State {
final status = results[1] as SCVipStatusRes;
final resolvedStatus = status.levelInt > 0 ? status : home.state;
final selectedLevel = _resolveInitialLevel(home, resolvedStatus);
+ _logLoadedVipData(home, resolvedStatus, selectedLevel);
if (!mounted) return;
setState(() {
@@ -122,6 +123,11 @@ class _VipDetailPageState extends State {
if (_home?.configured == false ||
config?.enabled == false ||
config?.canPurchase != true) {
+ debugPrint(
+ '[VIP][Page] skip preview level=$level '
+ 'configured=${_home?.configured} enabled=${config?.enabled} '
+ 'canPurchase=${config?.canPurchase}',
+ );
return;
}
@@ -163,6 +169,10 @@ class _VipDetailPageState extends State {
if (!mounted) return;
if (purchase.success == true) {
+ debugPrint(
+ '[VIP][Page] purchase success targetLevel=$_selectedLevel '
+ 'orderId=${purchase.order?.id} paidGold=${purchase.order?.paidGold}',
+ );
SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful);
profileManager.fetchUserProfileData(loadGuardCount: false);
profileManager.balance();
@@ -183,6 +193,31 @@ class _VipDetailPageState extends State {
return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}';
}
+ void _logLoadedVipData(
+ SCVipHomeRes home,
+ SCVipStatusRes? status,
+ int selectedLevel,
+ ) {
+ debugPrint(
+ '[VIP][Page] home loaded configured=${home.configured} '
+ 'levels=${home.levels.length} selectedLevel=$selectedLevel '
+ 'statusActive=${status?.active} statusLevel=${status?.level} '
+ 'statusExpireAt=${status?.expireAt}',
+ );
+ if (home.configured != true) {
+ debugPrint(
+ '[VIP][Page][Warn] vip home configured=${home.configured}; '
+ 'check backend VIP config for sysOrigin=${home.sysOrigin}',
+ );
+ }
+ if (home.levels.isEmpty) {
+ debugPrint(
+ '[VIP][Page][Warn] vip home levels is empty; '
+ 'frontend has no level data to render.',
+ );
+ }
+ }
+
@override
Widget build(BuildContext context) {
return Scaffold(
diff --git a/lib/shared/data_sources/sources/local/durable_auth_storage.dart b/lib/shared/data_sources/sources/local/durable_auth_storage.dart
new file mode 100644
index 0000000..6f2519b
--- /dev/null
+++ b/lib/shared/data_sources/sources/local/durable_auth_storage.dart
@@ -0,0 +1,83 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+
+class DurableAuthStorage {
+ static const MethodChannel _channel = MethodChannel(
+ 'com.org.yumiparty/durable_auth_storage',
+ );
+ static const String _sessionKey = 'auth_session_v1';
+
+ static Future saveSession({
+ required String token,
+ required String currentUserJson,
+ }) async {
+ if (!Platform.isIOS) {
+ return;
+ }
+ if (token.trim().isEmpty || currentUserJson.trim().isEmpty) {
+ await clearSession();
+ return;
+ }
+ await _invokeSafely('write', {
+ 'key': _sessionKey,
+ 'value': jsonEncode({
+ 'token': token,
+ 'currentUser': currentUserJson,
+ }),
+ });
+ }
+
+ static Future readSession() async {
+ if (!Platform.isIOS) {
+ return null;
+ }
+ try {
+ final raw = await _channel.invokeMethod('read', {
+ 'key': _sessionKey,
+ });
+ if (raw == null || raw.trim().isEmpty) {
+ return null;
+ }
+ final json = jsonDecode(raw);
+ if (json is! Map) {
+ return null;
+ }
+ final token = (json['token'] as String? ?? '').trim();
+ final currentUserJson = json['currentUser'] as String? ?? '';
+ if (token.isEmpty || currentUserJson.trim().isEmpty) {
+ return null;
+ }
+ return DurableAuthSession(token: token, currentUserJson: currentUserJson);
+ } catch (_) {
+ return null;
+ }
+ }
+
+ static Future clearSession() async {
+ if (!Platform.isIOS) {
+ return;
+ }
+ await _invokeSafely('delete', {'key': _sessionKey});
+ }
+
+ static Future _invokeSafely(
+ String method,
+ Map arguments,
+ ) async {
+ try {
+ await _channel.invokeMethod(method, arguments);
+ } catch (_) {}
+ }
+}
+
+class DurableAuthSession {
+ const DurableAuthSession({
+ required this.token,
+ required this.currentUserJson,
+ });
+
+ final String token;
+ final String currentUserJson;
+}
diff --git a/lib/shared/data_sources/sources/local/user_manager.dart b/lib/shared/data_sources/sources/local/user_manager.dart
index 27dfae9..b02420e 100644
--- a/lib/shared/data_sources/sources/local/user_manager.dart
+++ b/lib/shared/data_sources/sources/local/user_manager.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
import 'dart:convert';
import 'package:flutter/cupertino.dart';
@@ -5,6 +6,7 @@ import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/tools/sc_message_utils.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
+import 'package:yumi/shared/data_sources/sources/local/durable_auth_storage.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
@@ -48,6 +50,12 @@ class AccountStorage {
String userJson = jsonEncode(_currentUser?.toJson());
if (userJson.isNotEmpty) {
DataPersistence.setCurrentUser(userJson);
+ unawaited(
+ DurableAuthStorage.saveSession(
+ token: _currentUser!.token ?? "",
+ currentUserJson: userJson,
+ ),
+ );
}
}
@@ -119,6 +127,30 @@ class AccountStorage {
return token;
}
+ Future restoreDurableSessionIfNeeded() async {
+ final localToken = DataPersistence.getToken().trim();
+ final localUserJson = DataPersistence.getCurrentUser().trim();
+ if (localToken.isNotEmpty && localUserJson.isNotEmpty) {
+ return;
+ }
+
+ final session = await DurableAuthStorage.readSession();
+ if (session == null) {
+ return;
+ }
+
+ token = session.token;
+ await DataPersistence.setToken(session.token);
+ await DataPersistence.setCurrentUser(session.currentUserJson);
+ try {
+ _currentUser = SocialChatLoginRes.fromJson(
+ jsonDecode(session.currentUserJson),
+ );
+ } catch (_) {
+ _currentUser = null;
+ }
+ }
+
void setToken(String tk) {
token = tk;
DataPersistence.setToken(token);
@@ -129,6 +161,7 @@ class AccountStorage {
_currentUser = null;
DataPersistence.setToken("");
DataPersistence.setCurrentUser("");
+ unawaited(DurableAuthStorage.clearSession());
}
///退出登录
diff --git a/lib/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart
index f9eeea2..1466e96 100644
--- a/lib/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart
+++ b/lib/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart
@@ -1,7 +1,17 @@
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
import 'package:yumi/shared/business_logic/repositories/vip_repository.dart';
+import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
+const String _vipApiHost = String.fromEnvironment(
+ 'VIP_API_HOST',
+ defaultValue: '',
+);
+
class SCVipRepositoryImp implements SocialChatVipRepository {
static SCVipRepositoryImp? _instance;
@@ -13,20 +23,38 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
@override
Future vipStatus() async {
- final result = await http.get(
- '/app/vip/status',
- fromJson: (json) => SCVipStatusRes.fromJson(json),
- );
- return result;
+ final path = _vipPath('/status');
+ _logRequest('GET', path);
+ try {
+ final result = await http.get(
+ path,
+ extra: _vipExtra(),
+ fromJson: (json) => SCVipStatusRes.fromJson(json),
+ );
+ _logResponse('GET', path, _statusSummary(result));
+ return result;
+ } catch (error) {
+ _logError('GET', path, error);
+ rethrow;
+ }
}
@override
Future vipHome() async {
- final result = await http.get(
- '/app/vip/home',
- fromJson: (json) => SCVipHomeRes.fromJson(json),
- );
- return result;
+ final path = _vipPath('/home');
+ _logRequest('GET', path);
+ try {
+ final result = await http.get(
+ path,
+ extra: _vipExtra(),
+ fromJson: (json) => SCVipHomeRes.fromJson(json),
+ );
+ _logResponse('GET', path, _homeSummary(result));
+ return result;
+ } catch (error) {
+ _logError('GET', path, error);
+ rethrow;
+ }
}
@override
@@ -46,12 +74,21 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
params['targetLevelCode'] = targetLevelCode;
}
- final result = await http.post(
- '/app/vip/preview',
- data: params,
- fromJson: (json) => SCVipPreviewRes.fromJson(json),
- );
- return result;
+ final path = _vipPath('/preview');
+ _logRequest('POST', path, body: params);
+ try {
+ final result = await http.post(
+ path,
+ data: params,
+ extra: _vipExtra(),
+ fromJson: (json) => SCVipPreviewRes.fromJson(json),
+ );
+ _logResponse('POST', path, _previewSummary(result));
+ return result;
+ } catch (error) {
+ _logError('POST', path, error, body: params);
+ rethrow;
+ }
}
@override
@@ -59,21 +96,195 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
required int targetLevel,
required String bizIdempotentKey,
}) async {
- final result = await http.post(
- '/app/vip/purchase',
- data: {'targetLevel': targetLevel, 'bizIdempotentKey': bizIdempotentKey},
- fromJson: (json) => SCVipPurchaseRes.fromJson(json),
- );
- return result;
+ final path = _vipPath('/purchase');
+ final body = {
+ 'targetLevel': targetLevel,
+ 'bizIdempotentKey': bizIdempotentKey,
+ };
+ _logRequest('POST', path, body: body);
+ try {
+ final result = await http.post(
+ path,
+ data: body,
+ extra: _vipExtra(),
+ fromJson: (json) => SCVipPurchaseRes.fromJson(json),
+ );
+ _logResponse('POST', path, _purchaseSummary(result));
+ return result;
+ } catch (error) {
+ _logError('POST', path, error, body: body);
+ rethrow;
+ }
}
@override
Future vipOrders({int cursor = 1, int limit = 20}) async {
- final result = await http.get(
- '/app/vip/orders',
- queryParams: {'cursor': cursor, 'limit': limit},
- fromJson: (json) => SCVipOrdersRes.fromJson(json),
+ final path = _vipPath('/orders');
+ final query = {'cursor': cursor, 'limit': limit};
+ _logRequest('GET', path, query: query);
+ try {
+ final result = await http.get(
+ path,
+ queryParams: query,
+ extra: _vipExtra(),
+ fromJson: (json) => SCVipOrdersRes.fromJson(json),
+ );
+ _logResponse('GET', path, _ordersSummary(result));
+ return result;
+ } catch (error) {
+ _logError('GET', path, error, query: query);
+ rethrow;
+ }
+ }
+
+ static bool get _hasExplicitVipApiHost => _vipApiHost.trim().isNotEmpty;
+
+ static String get _activeBaseUrl =>
+ _hasExplicitVipApiHost ? _vipApiHost.trim() : SCGlobalConfig.apiHost;
+
+ static String _vipPath(String suffix) {
+ final baseUrl = _activeBaseUrl.trim();
+ final parsed = Uri.tryParse(baseUrl);
+ final pathSegments =
+ parsed?.pathSegments.where((segment) => segment.isNotEmpty).toSet() ??
+ {};
+ final baseAlreadyContainsGo = pathSegments.contains('go');
+
+ if (_hasExplicitVipApiHost || baseAlreadyContainsGo) {
+ return '/app/vip$suffix';
+ }
+ return '/go/app/vip$suffix';
+ }
+
+ static Map? _vipExtra() {
+ if (!_hasExplicitVipApiHost) {
+ return null;
+ }
+ return {BaseNetworkClient.baseUrlOverrideKey: _vipApiHost.trim()};
+ }
+
+ static void _logRequest(
+ String method,
+ String path, {
+ Map? query,
+ Map? body,
+ }) {
+ debugPrint(
+ '[VIP][Request] $method $path '
+ 'baseUrl=$_activeBaseUrl query=${_json(query ?? const {})} '
+ 'body=${_json(body ?? const {})}',
);
- return result;
+ }
+
+ static void _logResponse(
+ String method,
+ String path,
+ Map summary,
+ ) {
+ debugPrint('[VIP][Response] $method $path summary=${_json(summary)}');
+ }
+
+ static void _logError(
+ String method,
+ String path,
+ Object error, {
+ Map? query,
+ Map? body,
+ }) {
+ debugPrint(
+ '[VIP][Error] $method $path query=${_json(query ?? const {})} '
+ 'body=${_json(body ?? const {})} error=$error',
+ );
+ if (error is DioException) {
+ debugPrint(
+ '[VIP][DioError] $method $path '
+ 'uri=${error.requestOptions.uri} '
+ 'statusCode=${error.response?.statusCode} '
+ 'message=${error.message} '
+ 'response=${_json(error.response?.data)}',
+ );
+ }
+ }
+
+ static Map _statusSummary(SCVipStatusRes result) {
+ return {
+ 'active': result.active,
+ 'status': result.status,
+ 'level': result.level,
+ 'levelCode': result.levelCode,
+ 'expireAt': result.expireAt,
+ 'hasBadge': result.badge?.url?.isNotEmpty == true,
+ 'hasAvatarFrame': result.avatarFrame?.url?.isNotEmpty == true,
+ 'hasEntryEffect': result.entryEffect?.url?.isNotEmpty == true,
+ 'hasChatBubble': result.chatBubble?.url?.isNotEmpty == true,
+ 'hasFloatPicture': result.floatPicture?.url?.isNotEmpty == true,
+ };
+ }
+
+ static Map _homeSummary(SCVipHomeRes result) {
+ return {
+ 'configured': result.configured,
+ 'state': result.state == null ? null : _statusSummary(result.state!),
+ 'levelsCount': result.levels.length,
+ 'levels':
+ result.levels
+ .map(
+ (level) => {
+ 'level': level.level,
+ 'levelCode': level.levelCode,
+ 'enabled': level.enabled,
+ 'durationDays': level.durationDays,
+ 'priceGold': level.priceGold,
+ 'canPurchase': level.canPurchase,
+ 'payableGold': level.payableGold,
+ 'hasBadge': level.badge?.url?.isNotEmpty == true,
+ },
+ )
+ .toList(),
+ };
+ }
+
+ static Map _previewSummary(SCVipPreviewRes result) {
+ return {
+ 'orderType': result.orderType,
+ 'currentLevel': result.currentLevel,
+ 'targetLevel': result.targetLevel,
+ 'durationDays': result.durationDays,
+ 'originalPriceGold': result.originalPriceGold,
+ 'payableGold': result.payableGold,
+ 'expireAt': result.expireAt,
+ };
+ }
+
+ static Map _purchaseSummary(SCVipPurchaseRes result) {
+ final order = result.order;
+ return {
+ 'success': result.success,
+ 'order':
+ order == null
+ ? null
+ : {
+ 'id': order.id,
+ 'orderType': order.orderType,
+ 'fromLevel': order.fromLevel,
+ 'toLevel': order.toLevel,
+ 'paidGold': order.paidGold,
+ 'status': order.status,
+ 'expireAt': order.expireAt,
+ },
+ 'state': result.state == null ? null : _statusSummary(result.state!),
+ };
+ }
+
+ static Map _ordersSummary(SCVipOrdersRes result) {
+ return {'recordsCount': result.records.length, 'total': result.total};
+ }
+
+ static String _json(dynamic value) {
+ try {
+ return jsonEncode(value);
+ } catch (_) {
+ return value.toString();
+ }
}
}