From 9f4df82c42b7b3e06d51ac5b771528339bd8cebb Mon Sep 17 00:00:00 2001 From: roxy Date: Wed, 29 Apr 2026 19:23:51 +0800 Subject: [PATCH] =?UTF-8?q?vip=20=E6=8E=A5=E5=8F=A3=20=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E5=8F=8A=20=E9=87=8D=E6=96=B0=E5=AE=89=E8=A3=85=E7=AD=89?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 14 +- android/app/src/main/res/xml/backup_rules.xml | 6 + .../main/res/xml/data_extraction_rules.xml | 13 + ios/Runner/AppDelegate.swift | 92 +++++- lib/main.dart | 2 + .../room_game/views/leader_game_page.dart | 53 +--- lib/modules/splash/splash_page.dart | 36 +++ lib/modules/user/vip/vip_detail_page.dart | 35 +++ .../sources/local/durable_auth_storage.dart | 83 ++++++ .../sources/local/user_manager.dart | 33 +++ .../repositories/sc_vip_repository_imp.dart | 265 ++++++++++++++++-- 11 files changed, 547 insertions(+), 85 deletions(-) create mode 100644 android/app/src/main/res/xml/backup_rules.xml create mode 100644 android/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 lib/shared/data_sources/sources/local/durable_auth_storage.dart 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(); + } } }