From f9b7c53142f8c5ee6e308ca82ab36002dbcee6ac Mon Sep 17 00:00:00 2001 From: zhx Date: Wed, 10 Jun 2026 15:50:36 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E8=B0=B7=E6=AD=8C=E5=85=85=E5=80=BC?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 20 +++ .../payment/google_payment_manager.dart | 141 +++++++++++++++--- .../room_red_packet_send_panel.dart | 3 +- .../room/room_recharge_bottom_sheet.dart | 3 +- 4 files changed, 141 insertions(+), 26 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index fa642c2..e6a7b06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:fluro/fluro.dart' as fluro; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -299,6 +300,7 @@ class _YumiApplicationState extends State WidgetsBinding.instance.addPostFrameCallback((_) { _initRouter(); unawaited(_initLink()); + unawaited(_reconcilePendingGooglePurchases(force: true)); }); } @@ -325,6 +327,24 @@ class _YumiApplicationState extends State context.read().releaseRtcEngineForAppTermination(), ); } + if (state == AppLifecycleState.resumed) { + unawaited(_reconcilePendingGooglePurchases()); + } + } + + Future _reconcilePendingGooglePurchases({bool force = false}) async { + if (!Platform.isAndroid) { + return; + } + try { + await context + .read() + .reconcilePendingGooglePurchases(context: context, force: force); + } catch (error) { + if (kDebugMode) { + debugPrint('reconcile pending google purchases failed: $error'); + } + } } @override diff --git a/lib/services/payment/google_payment_manager.dart b/lib/services/payment/google_payment_manager.dart index 22fc38e..fc097df 100644 --- a/lib/services/payment/google_payment_manager.dart +++ b/lib/services/payment/google_payment_manager.dart @@ -33,6 +33,9 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 维护商品类型映射 final Map _productTypeMap = {}; + final Set _verifyingPurchaseKeys = {}; + bool _isReconcilingPastPurchases = false; + DateTime? _lastPastPurchaseReconcileAt; // 公开访问器 bool get isAvailable => _isAvailable; @@ -60,12 +63,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { SCTts.show("Google Play payment service is unavailable"); return; } - _subscription?.cancel(); - // 设置购买流监听 - _subscription = iap.purchaseStream.listen( - _handlePurchase, - onError: (error, stackTrace) => _handleError(error, stackTrace), - ); + _ensurePurchaseStreamListener(); // 先获取商品配置 await fetchProductConfiguration(); @@ -75,6 +73,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 最后恢复购买,处理未完成交易 await recoverTransactions(); + await reconcilePendingGooglePurchases(force: true); } catch (e) { SCTts.show("init fail: $e"); _errorMessage = 'init fail: ${e.toString()}'; @@ -84,6 +83,13 @@ class AndroidPaymentProcessor extends ChangeNotifier { } } + void _ensurePurchaseStreamListener() { + _subscription ??= iap.purchaseStream.listen( + _handlePurchase, + onError: (error, stackTrace) => _handleError(error, stackTrace), + ); + } + ///用户获取商店配置的商品列表 Future fetchProductConfiguration() async { productMap.clear(); @@ -155,6 +161,9 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 获取商品类型 String _getProductType(String productId) { + if (productId.startsWith('coins.')) { + return 'consumable'; + } return _productTypeMap[productId] ?? 'nonConsumable'; } @@ -283,7 +292,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { case PurchaseStatus.restored: // 处理恢复的购买 - _verifyPayment(purchase); + _verifyPayment(purchase, forceSubmit: true); break; default: break; @@ -304,21 +313,20 @@ class AndroidPaymentProcessor extends ChangeNotifier { listen: false, ); try { - // 如果是消耗型商品,尝试消耗 - if (_getProductType(purchase.productID) == 'consumable') { - await consumePurchase(purchase); - // 消耗后重新获取余额 - profileManager.balance(); - _refreshFirstRechargeRewardState(firstRechargeManager); - SCTts.show('outstanding purchases have been reinstated'); - } + await reconcilePendingGooglePurchases(force: true, showToast: true); + profileManager.balance(); + _refreshFirstRechargeRewardState(firstRechargeManager); } catch (e) { debugPrint('handle already-owned purchase failed: $e'); } } // 验证支付凭证 - Future _verifyPayment(PurchaseDetails purchase) async { + Future _verifyPayment( + PurchaseDetails purchase, { + bool forceSubmit = false, + bool showToast = true, + }) async { final profileManager = Provider.of( context, listen: false, @@ -331,12 +339,22 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 1. 基本验证 if (purchase.verificationData.serverVerificationData.isEmpty) { _errorMessage = '无效的支付凭证'; - SCTts.show("Invalid payment voucher"); + if (showToast) { + SCTts.show("Invalid payment voucher"); + } return; } - // 2. 检查是否已经处理过这个购买 - if (!purchase.pendingCompletePurchase) { + // 2. 正常购买流会依赖 pendingCompletePurchase;启动/回前台补偿流必须允许 + // 已确认但仍未消费的一次性商品再次上报后端,否则用户付款后退出 App 会漏单。 + if (!forceSubmit && !purchase.pendingCompletePurchase) { + return; + } + + final purchaseKey = + purchase.purchaseID ?? + purchase.verificationData.serverVerificationData; + if (purchaseKey.isNotEmpty && !_verifyingPurchaseKeys.add(purchaseKey)) { return; } @@ -371,16 +389,93 @@ class AndroidPaymentProcessor extends ChangeNotifier { profileManager.balance(); _refreshFirstRechargeRewardState(firstRechargeManager); - SCTts.show('purchase successful'); + if (showToast) { + SCTts.show('purchase successful'); + } } catch (e) { - SCTts.show("verification failed: $e"); + if (showToast) { + SCTts.show("verification failed: $e"); + } _errorMessage = '验证失败: ${e.toString()}'; } finally { + final purchaseKey = + purchase.purchaseID ?? + purchase.verificationData.serverVerificationData; + if (purchaseKey.isNotEmpty) { + _verifyingPurchaseKeys.remove(purchaseKey); + } SCLoadingManager.hide(); notifyListeners(); } } + Future reconcilePendingGooglePurchases({ + BuildContext? context, + bool force = false, + bool showToast = false, + }) async { + if (!Platform.isAndroid) { + return; + } + if (context != null) { + this.context = context; + } + if (_isReconcilingPastPurchases) { + return; + } + final lastCheck = _lastPastPurchaseReconcileAt; + if (!force && + lastCheck != null && + DateTime.now().difference(lastCheck) < const Duration(seconds: 20)) { + return; + } + + _isReconcilingPastPurchases = true; + _lastPastPurchaseReconcileAt = DateTime.now(); + try { + _ensurePurchaseStreamListener(); + _isAvailable = await iap.isAvailable(); + if (!_isAvailable) { + return; + } + + final InAppPurchaseAndroidPlatformAddition androidAddition = + iap.getPlatformAddition(); + final response = await androidAddition.queryPastPurchases(); + if (response.error != null) { + debugPrint('query past google purchases failed: ${response.error}'); + return; + } + + final purchases = + response.pastPurchases.where((purchase) { + final isPurchased = + purchase.status == PurchaseStatus.purchased || + purchase.status == PurchaseStatus.restored; + final productType = _getProductType(purchase.productID); + final isUnacknowledged = + purchase.pendingCompletePurchase || + !purchase.billingClientPurchase.isAcknowledged; + // Google 只会返回未消费的一次性商品;即使它已被确认,也说明仍可补上报后端。 + return isPurchased && + (isUnacknowledged || productType == 'consumable'); + }).toList(); + + if (purchases.isEmpty) { + return; + } + _purchases = purchases; + for (final purchase in purchases) { + await _verifyPayment(purchase, forceSubmit: true, showToast: showToast); + } + } catch (e) { + debugPrint('reconcile pending google purchases failed: $e'); + } finally { + _isReconcilingPastPurchases = false; + notifyListeners(); + } + } + // 交付商品 Future _deliverProduct(String productId) async { // 实现您的业务逻辑 @@ -506,7 +601,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { if (productType == 'consumable') { success = await iap.buyConsumable( purchaseParam: purchaseParam, - autoConsume: kReleaseMode, // 生产环境自动消耗,调试时手动控制 + autoConsume: false, ); } else { success = await iap.buyNonConsumable(purchaseParam: purchaseParam); @@ -528,6 +623,8 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 新增:检查未完成的购买 Future _checkPendingPurchases() async { try { + await reconcilePendingGooglePurchases(); + // 恢复购买以获取所有未完成交易 await iap.restorePurchases(); diff --git a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart index bfa3d2d..7ab02a7 100644 --- a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart +++ b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -1336,7 +1335,7 @@ class _RoomRedPacketRechargeSheetState SCLoadingManager.show(); final success = await processor.iap.buyConsumable( purchaseParam: PurchaseParam(productDetails: product.produc), - autoConsume: kReleaseMode, + autoConsume: false, ); if (!success) { SCTts.show('Purchase failed, please check your network connection'); diff --git a/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart b/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart index cd1fdb4..29eaa32 100644 --- a/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart +++ b/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -367,7 +366,7 @@ class _RoomRechargeBottomSheetState extends State { SCLoadingManager.show(); final success = await processor.iap.buyConsumable( purchaseParam: PurchaseParam(productDetails: product.produc), - autoConsume: kReleaseMode, + autoConsume: false, ); if (!success) { SCTts.show('Purchase failed, please check your network connection'); From 2de2aa789126a5f222ee8aff2c2a97ac8e8db297 Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 12 Jun 2026 11:55:13 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room_game/bridge/hotgame_js_bridge.dart | 303 +++++++++ .../data/models/room_game_models.dart | 4 + .../room_game/views/hotgame_game_page.dart | 640 ++++++++++++++++++ .../room_game/views/room_game_list_sheet.dart | 15 +- .../user/edit/edit_user_info_page2.dart | 118 +++- test/hotgame_bridge_test.dart | 62 ++ 6 files changed, 1138 insertions(+), 4 deletions(-) create mode 100644 lib/modules/room_game/bridge/hotgame_js_bridge.dart create mode 100644 lib/modules/room_game/views/hotgame_game_page.dart create mode 100644 test/hotgame_bridge_test.dart diff --git a/lib/modules/room_game/bridge/hotgame_js_bridge.dart b/lib/modules/room_game/bridge/hotgame_js_bridge.dart new file mode 100644 index 0000000..b72b1b8 --- /dev/null +++ b/lib/modules/room_game/bridge/hotgame_js_bridge.dart @@ -0,0 +1,303 @@ +import 'dart:convert'; + +class HotgameBridgeActions { + static const String recharge = 'recharge'; + static const String quit = 'quit'; + static const String loadComplete = 'loadComplete'; + static const String debugLog = 'debugLog'; + + static String normalize(String action) { + final trimmed = action.trim(); + final lower = trimmed.toLowerCase(); + switch (lower) { + case 'recharge': + case 'pay': + case 'gamepay': + case 'gamerecharge': + case 'insufficient': + case 'insufficientbalance': + return recharge; + case 'quit': + case 'exit': + case 'close': + case 'closegame': + case 'destroy': + return quit; + case 'loadcomplete': + case 'gameloaded': + case 'loaded': + case 'ready': + return loadComplete; + case 'debuglog': + return debugLog; + default: + return trimmed; + } + } +} + +class HotgameBridgeMessage { + const HotgameBridgeMessage({ + required this.action, + this.payload = const {}, + }); + + final String action; + final Map payload; + + static HotgameBridgeMessage fromAction(String action, String rawMessage) { + return HotgameBridgeMessage( + action: HotgameBridgeActions.normalize(action), + payload: _parsePayload(rawMessage), + ); + } + + static HotgameBridgeMessage parse(String rawMessage) { + try { + final dynamic decoded = jsonDecode(rawMessage); + if (decoded is Map) { + return HotgameBridgeMessage( + action: HotgameBridgeActions.normalize( + (decoded['action'] ?? + decoded['cmd'] ?? + decoded['event'] ?? + decoded['method'] ?? + decoded['type'] ?? + '') + .toString(), + ), + payload: _parsePayload( + decoded['payload'] ?? decoded['data'] ?? decoded['params'], + ), + ); + } + } catch (_) {} + return HotgameBridgeMessage( + action: HotgameBridgeActions.normalize(rawMessage), + ); + } + + static Map _parsePayload(dynamic rawPayload) { + if (rawPayload is Map) { + return rawPayload; + } + if (rawPayload is String) { + final trimmed = rawPayload.trim(); + if (trimmed.isEmpty) { + return const {}; + } + try { + final dynamic decoded = jsonDecode(trimmed); + if (decoded is Map) { + return decoded; + } + } catch (_) {} + return {'raw': trimmed}; + } + return const {}; + } +} + +class HotgameJsBridge { + static const String channelName = 'HotgameBridgeChannel'; + + static String bootstrapScript({Map? launchPayload}) { + final safeLaunchPayload = jsonEncode( + launchPayload ?? const {}, + ); + return ''' + (function() { + const launchPayload = $safeLaunchPayload; + + function toPayload(input) { + if (typeof input === 'string') { + try { + return JSON.parse(input); + } catch (_) { + return input.trim() ? { raw: input.trim() } : {}; + } + } + if (input && typeof input === 'object') { + return input; + } + return {}; + } + + function stringifyForLog(value) { + if (value == null) { + return ''; + } + if (typeof value === 'string') { + return value.length > 320 ? value.slice(0, 320) + '...' : value; + } + try { + const text = JSON.stringify(value); + return text.length > 320 ? text.slice(0, 320) + '...' : text; + } catch (_) { + return String(value); + } + } + + function postMessage(action, input) { + const payload = toPayload(input); + $channelName.postMessage(JSON.stringify({ + action: action, + payload: payload + })); + return payload; + } + + function postDebug(tag, payload) { + postMessage('${HotgameBridgeActions.debugLog}', { + tag: tag, + message: stringifyForLog(payload) + }); + } + + function dispatchLaunchPayload(payload) { + const normalizedPayload = + payload && typeof payload === 'object' ? payload : {}; + const launchConfig = + normalizedPayload.launchConfig && + typeof normalizedPayload.launchConfig === 'object' + ? normalizedPayload.launchConfig + : {}; + const entry = + normalizedPayload.entry && typeof normalizedPayload.entry === 'object' + ? normalizedPayload.entry + : {}; + window.__hotgameLaunchPayload = normalizedPayload; + window.hotgameLaunchPayload = normalizedPayload; + window.__hotgameLaunchConfig = launchConfig; + window.hotgameLaunchConfig = launchConfig; + window.__hotgameLaunchEntry = entry; + window.hotgameLaunchEntry = entry; + if (typeof window.onHotgameLaunchConfig === 'function') { + window.onHotgameLaunchConfig(launchConfig, normalizedPayload); + } + if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') { + window.dispatchEvent( + new CustomEvent('hotgameLaunchConfig', { detail: launchConfig }) + ); + } + } + + function installPostMessageObject(name, action) { + const existing = window[name]; + if (!existing || typeof existing.postMessage !== 'function') { + window[name] = { + postMessage: function(body) { + return postMessage(action, body); + } + }; + } + } + + function installWebkitHandler(name, action) { + window.webkit = window.webkit || {}; + window.webkit.messageHandlers = window.webkit.messageHandlers || {}; + if (!window.webkit.messageHandlers[name] || + typeof window.webkit.messageHandlers[name].postMessage !== 'function') { + window.webkit.messageHandlers[name] = { + postMessage: function(body) { + return postMessage(action, body); + } + }; + } + } + + function installJsBridgeFunction(name, action) { + window.JsBridge = window.JsBridge || {}; + window.JsBridge[name] = function(body) { + return postMessage(action, body || {}); + }; + } + + if (!window.__hotgameBridgeReady) { + window.__hotgameBridgeReady = true; + installJsBridgeFunction('recharge', '${HotgameBridgeActions.recharge}'); + installJsBridgeFunction('quit', '${HotgameBridgeActions.quit}'); + installJsBridgeFunction('exit', '${HotgameBridgeActions.quit}'); + installJsBridgeFunction('closeGame', '${HotgameBridgeActions.quit}'); + installPostMessageObject('recharge', '${HotgameBridgeActions.recharge}'); + installPostMessageObject('quit', '${HotgameBridgeActions.quit}'); + installPostMessageObject('loadComplete', '${HotgameBridgeActions.loadComplete}'); + installWebkitHandler('recharge', '${HotgameBridgeActions.recharge}'); + installWebkitHandler('quit', '${HotgameBridgeActions.quit}'); + installWebkitHandler('loadComplete', '${HotgameBridgeActions.loadComplete}'); + window.NativeBridge = window.NativeBridge || {}; + window.NativeBridge.recharge = function(body) { + return postMessage('${HotgameBridgeActions.recharge}', body || {}); + }; + window.NativeBridge.quit = function(body) { + return postMessage('${HotgameBridgeActions.quit}', body || {}); + }; + window.NativeBridge.closeGame = function(body) { + return postMessage('${HotgameBridgeActions.quit}', body || {}); + }; + window.NativeBridge.rechargeSuccess = function(body) { + return notifyRechargeSuccess(body || {}); + }; + window.__hotgameBridgeDebug = postDebug; + postDebug('bridge.ready', { + href: window.location && window.location.href, + hasJsBridge: !!window.JsBridge, + hasRechargeSuccess: typeof window.rechargeSuccess === 'function' + }); + } + + function notifyRechargeSuccess(body) { + const payload = body || {}; + let notified = false; + if (typeof window.rechargeSuccess === 'function') { + window.rechargeSuccess(); + notified = true; + } + if (typeof window.onRechargeSuccess === 'function') { + window.onRechargeSuccess(payload); + notified = true; + } + if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') { + window.dispatchEvent( + new CustomEvent('rechargeSuccess', { detail: payload }) + ); + } + return notified; + } + + window.__hotgameNotifyRechargeSuccess = notifyRechargeSuccess; + dispatchLaunchPayload(launchPayload); + })(); + '''; + } + + static String buildRechargeSuccessScript({Map? payload}) { + final safePayload = jsonEncode( + payload ?? + {'timestamp': DateTime.now().millisecondsSinceEpoch}, + ); + return ''' + (function() { + const payload = $safePayload; + if (typeof window.__hotgameBridgeDebug === 'function') { + window.__hotgameBridgeDebug('recharge.success', payload); + } + if (typeof window.__hotgameNotifyRechargeSuccess === 'function') { + window.__hotgameNotifyRechargeSuccess(payload); + return; + } + if (typeof window.rechargeSuccess === 'function') { + window.rechargeSuccess(); + } + if (typeof window.onRechargeSuccess === 'function') { + window.onRechargeSuccess(payload); + } + if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') { + window.dispatchEvent( + new CustomEvent('rechargeSuccess', { detail: payload }) + ); + } + })(); + '''; + } +} diff --git a/lib/modules/room_game/data/models/room_game_models.dart b/lib/modules/room_game/data/models/room_game_models.dart index a763d88..de459d2 100644 --- a/lib/modules/room_game/data/models/room_game_models.dart +++ b/lib/modules/room_game/data/models/room_game_models.dart @@ -127,6 +127,10 @@ class RoomGameListItemModel { bool get isLeader => provider.toUpperCase() == 'LEADER' || gameType.toUpperCase() == 'LEADER'; + bool get isHotgame => + provider.toUpperCase() == 'HOTGAME' || + gameType.toUpperCase() == 'HOTGAME'; + factory RoomGameListItemModel.fromJson(Map json) { return RoomGameListItemModel( gameId: _asString(json['gameId']), diff --git a/lib/modules/room_game/views/hotgame_game_page.dart b/lib/modules/room_game/views/hotgame_game_page.dart new file mode 100644 index 0000000..e104943 --- /dev/null +++ b/lib/modules/room_game/views/hotgame_game_page.dart @@ -0,0 +1,640 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/modules/room_game/bridge/hotgame_js_bridge.dart'; +import 'package:yumi/modules/room_game/data/models/room_game_models.dart'; +import 'package:yumi/modules/room_game/data/room_game_repository.dart'; +import 'package:yumi/modules/room_game/utils/room_game_viewport.dart'; +import 'package:yumi/modules/room_game/views/baishun_loading_view.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/shared/tools/sc_h5_url_utils.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; + +class HotgameGamePage extends StatefulWidget { + const HotgameGamePage({ + super.key, + required this.roomId, + required this.game, + required this.launchModel, + }); + + final String roomId; + final RoomGameListItemModel game; + final BaishunLaunchModel launchModel; + + @override + State createState() => _HotgameGamePageState(); +} + +class _HotgameGamePageState extends State { + static const double _sevenScreenRatio = 0.72; + + final RoomGameRepository _repository = RoomGameRepository(); + final Set> _webGestureRecognizers = + >{ + Factory(() => EagerGestureRecognizer()), + }; + late final WebViewController _controller; + + Timer? _bridgeBootstrapTimer; + Timer? _loadingFallbackTimer; + + bool _isLoading = true; + bool _isClosing = false; + bool _didReceiveBridgeMessage = false; + bool _didFinishPageLoad = false; + bool _didInjectProgressBridge = false; + int _bridgeInjectCount = 0; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.black) + ..addJavaScriptChannel( + HotgameJsBridge.channelName, + onMessageReceived: _handleBridgeMessage, + ) + ..addJavaScriptChannel( + HotgameBridgeActions.recharge, + onMessageReceived: + (JavaScriptMessage message) => _handleNamedChannelMessage( + HotgameBridgeActions.recharge, + message, + ), + ) + ..addJavaScriptChannel( + HotgameBridgeActions.quit, + onMessageReceived: + (JavaScriptMessage message) => _handleNamedChannelMessage( + HotgameBridgeActions.quit, + message, + ), + ) + ..addJavaScriptChannel( + HotgameBridgeActions.loadComplete, + onMessageReceived: + (JavaScriptMessage message) => _handleNamedChannelMessage( + HotgameBridgeActions.loadComplete, + message, + ), + ) + ..setNavigationDelegate( + NavigationDelegate( + onPageStarted: (String url) { + _log('page_started url=${_clip(url, 240)}'); + _prepareForPageLoad(); + }, + onPageFinished: (String url) async { + _didFinishPageLoad = true; + _log('page_finished url=${_clip(url, 240)}'); + await _injectBridge(reason: 'page_finished'); + if (!mounted || _errorMessage != null) { + return; + } + setState(() { + _isLoading = false; + }); + }, + onProgress: (int progress) { + if (_didInjectProgressBridge || + progress < 10 || + _errorMessage != null) { + return; + } + _didInjectProgressBridge = true; + unawaited(_injectBridge(reason: 'progress_$progress')); + }, + onWebResourceError: (WebResourceError error) { + _log( + 'web_resource_error code=${error.errorCode} ' + 'type=${error.errorType} desc=${_clip(error.description, 300)}', + ); + _stopBridgeBootstrap(reason: 'web_resource_error'); + if (!mounted) { + return; + } + setState(() { + _errorMessage = error.description; + _isLoading = false; + }); + }, + ), + ); + _log('init launch=${_stringifyForLog(_buildLaunchSummary())}'); + unawaited(_loadGameEntry()); + } + + @override + void dispose() { + _stopBridgeBootstrap(reason: 'dispose'); + super.dispose(); + } + + void _prepareForPageLoad() { + _didReceiveBridgeMessage = false; + _didFinishPageLoad = false; + _didInjectProgressBridge = false; + _bridgeInjectCount = 0; + _stopBridgeBootstrap(reason: 'prepare_page_load'); + _bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), ( + Timer timer, + ) { + if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) { + timer.cancel(); + return; + } + unawaited(_injectBridge(reason: 'bootstrap')); + if (_didFinishPageLoad && timer.tick >= 12) { + timer.cancel(); + } + }); + _loadingFallbackTimer = Timer(const Duration(seconds: 6), () { + if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) { + return; + } + setState(() { + _isLoading = false; + }); + }); + if (!mounted) { + return; + } + setState(() { + _isLoading = true; + _errorMessage = null; + }); + } + + void _stopBridgeBootstrap({String reason = 'manual'}) { + if (_bridgeBootstrapTimer != null || _loadingFallbackTimer != null) { + _log('stop_bootstrap reason=$reason'); + } + _bridgeBootstrapTimer?.cancel(); + _bridgeBootstrapTimer = null; + _loadingFallbackTimer?.cancel(); + _loadingFallbackTimer = null; + } + + Future _loadGameEntry() async { + try { + _prepareForPageLoad(); + final entryUrl = widget.launchModel.entry.entryUrl.trim(); + _log( + 'load_entry launchMode=${widget.launchModel.entry.launchMode} ' + 'entryUrl=${_clip(entryUrl, 240)}', + ); + if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) { + await _controller.loadHtmlString(_buildMockHtml()); + return; + } + final h5Url = SCH5UrlUtils.appendToken(entryUrl); + final uri = Uri.tryParse(h5Url); + if (uri == null) { + throw Exception('Invalid hotgame entry url: $entryUrl'); + } + await _controller.loadRequest(uri); + } catch (error) { + _log('load_entry_error error=${_clip(error.toString(), 400)}'); + if (!mounted) { + return; + } + setState(() { + _errorMessage = error.toString(); + _isLoading = false; + }); + } + } + + Future _injectBridge({String reason = 'manual'}) async { + _bridgeInjectCount += 1; + if (reason != 'bootstrap' || + _bridgeInjectCount <= 3 || + _bridgeInjectCount % 5 == 0) { + _log('inject_bridge reason=$reason count=$_bridgeInjectCount'); + } + try { + await _controller.runJavaScript( + HotgameJsBridge.bootstrapScript(launchPayload: _buildLaunchPayload()), + ); + } catch (error) { + _log( + 'inject_bridge_error reason=$reason error=${_clip(error.toString(), 300)}', + ); + } + } + + Future _handleBridgeMessage(JavaScriptMessage message) async { + _log('channel_message raw=${_clip(message.message, 600)}'); + final bridgeMessage = HotgameBridgeMessage.parse(message.message); + await _dispatchBridgeMessage(bridgeMessage); + } + + Future _handleNamedChannelMessage( + String action, + JavaScriptMessage message, + ) async { + final bridgeMessage = HotgameBridgeMessage.fromAction( + action, + message.message, + ); + _log('named_channel action=$action raw=${_clip(message.message, 600)}'); + await _dispatchBridgeMessage(bridgeMessage); + } + + Future _dispatchBridgeMessage( + HotgameBridgeMessage bridgeMessage, + ) async { + if (bridgeMessage.action == HotgameBridgeActions.debugLog) { + final tag = bridgeMessage.payload['tag']?.toString().trim(); + final message = bridgeMessage.payload['message']?.toString() ?? ''; + _log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}'); + return; + } + + _didReceiveBridgeMessage = true; + _stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}'); + _log( + 'bridge_action action=${bridgeMessage.action} ' + 'payload=${_stringifyForLog(_sanitizeForLog(bridgeMessage.payload))}', + ); + + switch (bridgeMessage.action) { + case HotgameBridgeActions.recharge: + await _openRechargeAndNotifyGame(); + break; + case HotgameBridgeActions.quit: + await _closeAndExit(reason: 'h5_quit'); + break; + case HotgameBridgeActions.loadComplete: + if (!mounted) { + return; + } + setState(() { + _isLoading = false; + _errorMessage = null; + }); + break; + default: + _log('bridge_action_unhandled action=${bridgeMessage.action}'); + break; + } + } + + Future _openRechargeAndNotifyGame() async { + _log('hotgame_recharge open_wallet'); + await SCNavigatorUtils.push(context, WalletRoute.recharge); + try { + await _controller.runJavaScript( + HotgameJsBridge.buildRechargeSuccessScript(), + ); + } catch (error) { + _log( + 'recharge_success_notify_error error=${_clip(error.toString(), 300)}', + ); + } + } + + Future _reload() async { + if (!mounted) { + return; + } + setState(() { + _errorMessage = null; + _isLoading = true; + }); + await _loadGameEntry(); + } + + Future _closeAndExit({String reason = 'user_exit'}) async { + if (_isClosing) { + return; + } + _isClosing = true; + try { + final sessionId = widget.launchModel.gameSessionId.trim(); + if (sessionId.isNotEmpty && !sessionId.startsWith('mock')) { + await _repository.closeGame( + provider: widget.game.provider, + roomId: widget.roomId, + gameSessionId: sessionId, + reason: reason, + params: const {}, + ); + } + } catch (error) { + _log('close_error error=${_clip(error.toString(), 300)}'); + } + if (mounted) { + Navigator.of(context).pop(); + } + } + + String _resolveScreenMode() { + return resolveRoomGameScreenMode( + widget.game.launchParams, + fullScreen: widget.game.fullScreen, + ); + } + + int _resolveSafeHeight() { + if (widget.launchModel.entry.safeHeight > 0) { + return widget.launchModel.entry.safeHeight; + } + if (widget.game.safeHeight > 0) { + return widget.game.safeHeight; + } + return 0; + } + + double _calculateBodyHeight(BuildContext context) { + final screenMode = _resolveScreenMode(); + final maxDialogHeight = resolveRoomGameAvailableHeight( + context, + reservedTop: 12.w, + ); + if (maxDialogHeight <= 0) { + return 0; + } + + if (screenMode == 'full') { + return maxDialogHeight; + } + if (screenMode == 'seven') { + return maxDialogHeight * _sevenScreenRatio; + } + + return clampRoomGameHeight( + _calculatePreferredBodyHeight(context), + maxDialogHeight, + ); + } + + double _calculatePreferredBodyHeight(BuildContext context) { + final mediaQuery = MediaQuery.maybeOf(context); + final screenSize = mediaQuery?.size ?? Size.zero; + final safeHeight = _resolveSafeHeight(); + final fallback = screenSize.width; + + if (safeHeight > 0 && screenSize.width > 0) { + final ratio = roomGameDesignWidth / safeHeight; + return screenSize.width / ratio; + } + + return fallback; + } + + void _log(String message) {} + + String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) { + final trimmed = value.trim(); + if (trimmed.isEmpty) { + return ''; + } + if (trimmed.length <= keepStart + keepEnd) { + return trimmed; + } + return '${trimmed.substring(0, keepStart)}***${trimmed.substring(trimmed.length - keepEnd)}'; + } + + Object? _sanitizeForLog(Object? value, {String key = ''}) { + final lowerKey = key.toLowerCase(); + final shouldMask = + lowerKey.contains('code') || + lowerKey.contains('token') || + lowerKey.contains('authorization'); + if (value is Map) { + final result = {}; + for (final MapEntry entry in value.entries) { + result[entry.key.toString()] = _sanitizeForLog( + entry.value, + key: entry.key.toString(), + ); + } + return result; + } + if (value is List) { + return value.map((Object? item) => _sanitizeForLog(item)).toList(); + } + if (value is String) { + final normalized = shouldMask ? _maskValue(value) : value; + return _clip(normalized, 600); + } + return value; + } + + String _clip(String value, [int limit = 600]) { + final trimmed = value.trim(); + if (trimmed.length <= limit) { + return trimmed; + } + return '${trimmed.substring(0, limit)}...'; + } + + String _stringifyForLog(Object? value) { + if (value == null) { + return ''; + } + try { + return _clip(jsonEncode(value), 800); + } catch (_) { + return _clip(value.toString(), 800); + } + } + + Map _buildLaunchPayload() { + return { + 'provider': widget.launchModel.provider, + 'gameId': widget.launchModel.gameId, + 'providerGameId': widget.launchModel.providerGameId, + 'gameSessionId': widget.launchModel.gameSessionId, + 'entry': widget.launchModel.entry.toJson(), + 'launchConfig': widget.launchModel.launchConfig.toJson(), + 'roomState': widget.launchModel.roomState.toJson(), + 'game': { + 'gameId': widget.game.gameId, + 'provider': widget.game.provider, + 'gameType': widget.game.gameType, + 'name': widget.game.name, + 'launchMode': widget.game.launchMode, + 'launchParams': widget.game.launchParams, + }, + }; + } + + Map _buildLaunchSummary() { + final payload = _buildLaunchPayload(); + return { + 'roomId': widget.roomId, + 'provider': widget.game.provider, + 'gameId': widget.game.gameId, + 'providerGameId': widget.launchModel.providerGameId, + 'gameSessionId': widget.launchModel.gameSessionId, + 'entry': _sanitizeForLog(payload['entry']), + 'launchConfig': _sanitizeForLog(payload['launchConfig']), + 'roomState': _sanitizeForLog(payload['roomState']), + }; + } + + String _buildMockHtml() { + return ''' + + + + + + + + +
+

Hotgame Mock

+ + + + + + +
+
+ + + + '''; + } + + @override + Widget build(BuildContext context) { + final bodyHeight = _calculateBodyHeight(context); + final bottomFrameHeight = resolveRoomGameCompatibilityBottomHeight(context); + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) { + return; + } + await _closeAndExit(); + }, + child: Material( + color: Colors.transparent, + child: Align( + alignment: Alignment.bottomCenter, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24.w), + topRight: Radius.circular(24.w), + ), + child: Container( + width: ScreenUtil().screenWidth, + height: bodyHeight + bottomFrameHeight, + color: const Color(0xFF081915), + child: Stack( + children: [ + Positioned( + left: 0, + right: 0, + top: 0, + bottom: bottomFrameHeight, + child: WebViewWidget( + controller: _controller, + gestureRecognizers: _webGestureRecognizers, + ), + ), + if (bottomFrameHeight > 0) + Positioned( + left: 0, + right: 0, + bottom: 0, + height: bottomFrameHeight, + child: RoomGameCompatibilityBottomFrame( + height: bottomFrameHeight, + ), + ), + if (_errorMessage != null) _buildErrorState(), + if (_isLoading && _errorMessage == null) + const IgnorePointer( + ignoring: true, + child: BaishunLoadingView( + message: 'Waiting for Hotgame...', + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildErrorState() { + return Positioned.fill( + child: Container( + color: Colors.black.withValues(alpha: 0.88), + padding: EdgeInsets.symmetric(horizontal: 28.w), + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, color: Colors.white70, size: 34.w), + SizedBox(height: 12.w), + Text( + 'Hotgame failed to load', + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 8.w), + Text( + _errorMessage ?? '', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70, fontSize: 12.sp), + ), + SizedBox(height: 16.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton(onPressed: _reload, child: const Text('Retry')), + SizedBox(width: 10.w), + TextButton( + onPressed: () { + SCTts.show('Please verify the Hotgame entry url'); + }, + child: const Text('Tips'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/room_game/views/room_game_list_sheet.dart b/lib/modules/room_game/views/room_game_list_sheet.dart index 4961a61..076d34b 100644 --- a/lib/modules/room_game/views/room_game_list_sheet.dart +++ b/lib/modules/room_game/views/room_game_list_sheet.dart @@ -9,6 +9,7 @@ import 'package:yumi/modules/room_game/data/models/room_game_models.dart'; import 'package:yumi/modules/room_game/data/room_game_repository.dart'; import 'package:yumi/modules/room_game/utils/room_game_viewport.dart'; import 'package:yumi/modules/room_game/views/baishun_game_page.dart'; +import 'package:yumi/modules/room_game/views/hotgame_game_page.dart'; import 'package:yumi/modules/room_game/views/leader_game_page.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; @@ -32,7 +33,6 @@ class RoomGameListSheet extends StatefulWidget { } class _RoomGameListSheetState extends State { - static const String _logPrefix = '[RoomGameLaunch]'; static const int _itemsPerRow = 4; static const String _sheetFrameAsset = 'sc_images/room/sc_room_game_sheet_frame.png'; @@ -91,7 +91,9 @@ class _RoomGameListSheetState extends State { String targetGameId, ) { final supportedItems = - items.where((item) => item.isBaishun || item.isLeader).toList(); + items + .where((item) => item.isBaishun || item.isLeader || item.isHotgame) + .toList(); final candidates = supportedItems.isNotEmpty ? supportedItems : items; if (targetGameId == '0') { return candidates[math.Random().nextInt(candidates.length)]; @@ -118,7 +120,7 @@ class _RoomGameListSheetState extends State { SCTts.show('roomId is empty'); return; } - if (!game.isBaishun && !game.isLeader) { + if (!game.isBaishun && !game.isLeader && !game.isHotgame) { SCTts.show('This game provider is not wired yet'); return; } @@ -268,6 +270,13 @@ class _RoomGameListSheetState extends State { launchModel: launchModel, ); } + if (game.isHotgame) { + return HotgameGamePage( + roomId: _roomId, + game: game, + launchModel: launchModel, + ); + } return null; } diff --git a/lib/modules/user/edit/edit_user_info_page2.dart b/lib/modules/user/edit/edit_user_info_page2.dart index de8adae..4e01768 100644 --- a/lib/modules/user/edit/edit_user_info_page2.dart +++ b/lib/modules/user/edit/edit_user_info_page2.dart @@ -8,6 +8,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:provider/provider.dart'; import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; @@ -17,8 +18,11 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/modules/country/country_route.dart'; import '../../../shared/tools/sc_pick_utils.dart'; +import '../../../shared/business_logic/models/res/country_res.dart'; import '../../../shared/business_logic/models/res/login_res.dart'; +import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart'; class EditUserInfoPage2 extends StatefulWidget { @@ -37,6 +41,9 @@ class _EditUserInfoPage2State extends State String? bornMonth; String? autograph; String? hobby; + String? countryId; + String? countryCode; + String? countryName; String? bornDay; String? bornYear; String nickName = ""; @@ -53,6 +60,9 @@ class _EditUserInfoPage2State extends State nickName = currentProfile?.userNickname ?? ""; autograph = currentProfile?.autograph ?? ""; hobby = currentProfile?.hobby ?? ""; + countryId = currentProfile?.countryId ?? ""; + countryCode = currentProfile?.countryCode ?? ""; + countryName = currentProfile?.countryName ?? ""; birthdayDate = _birthdayFromProfile(currentProfile) ?? _birthdayFromAge(currentProfile?.age); @@ -63,6 +73,19 @@ class _EditUserInfoPage2State extends State bornYear = _formatYear(currentProfile?.bornYear ?? birthdayDate?.year); age = currentProfile?.age ?? _ageFromBirthday(birthdayDate); sex = currentProfile?.userSex; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + Provider.of( + context, + listen: false, + ).getUserIdentity(); + Provider.of( + context, + listen: false, + ).fetchCountryList(); + }); } String? _preferNonEmpty(String? primary, String? fallback) { @@ -157,11 +180,42 @@ class _EditUserInfoPage2State extends State return values.any((value) => !identical(value, _noChange)); } + bool _hasIdentity(SCUserIdentityRes? identity) { + if (identity == null) { + return true; + } + return (identity.anchor ?? false) || + (identity.bd ?? false) || + (identity.agent ?? false) || + (identity.bdLeader ?? false) || + (identity.freightAgent ?? false) || + (identity.superFreightAgent ?? false) || + (identity.admin ?? false) || + (identity.superAdmin ?? false) || + (identity.manager ?? false) || + (identity.yumiManager ?? false); + } + + bool _canChangeCountry(SCUserIdentityRes? identity) { + return !_hasIdentity(identity); + } + + String _countryDisplayText() { + final displayName = (countryName ?? "").trim(); + if (displayName.isNotEmpty) { + return displayName; + } + return (countryCode ?? "").trim(); + } + void _syncLocalProfileState(SocialChatUserProfile profile) { userCover = _preferUsableAvatar(profile.userAvatar, userCover); nickName = profile.userNickname ?? nickName; autograph = profile.autograph ?? autograph; hobby = profile.hobby ?? hobby; + countryId = profile.countryId ?? countryId; + countryCode = profile.countryCode ?? countryCode; + countryName = profile.countryName ?? countryName; sex = profile.userSex ?? sex; birthdayDate = _birthdayFromProfile(profile) ?? @@ -284,6 +338,12 @@ class _EditUserInfoPage2State extends State _showInputBioHobby(hobby ?? "", 2); }, ), + if (_canChangeCountry(ref.userIdentity)) + _buildItem( + "${SCAppLocalizations.of(context)!.countryRegion}:", + _countryDisplayText(), + _openCountryPicker, + ), ], ); }, @@ -421,6 +481,7 @@ class _EditUserInfoPage2State extends State Object? bornDayValue = _noChange, Object? hobbyValue = _noChange, Object? autographValue = _noChange, + Object? countryValue = _noChange, }) async { if (!_hasChanges([ userAvatarValue, @@ -432,6 +493,7 @@ class _EditUserInfoPage2State extends State bornDayValue, hobbyValue, autographValue, + countryValue, ])) { return; } @@ -446,6 +508,8 @@ class _EditUserInfoPage2State extends State _isSubmitting = true; SCLoadingManager.show(); try { + final selectedCountry = + identical(countryValue, _noChange) ? null : countryValue as Country?; final updatedProfile = await SCAccountRepository().updateUserInfo( userAvatar: identical(userAvatarValue, _noChange) @@ -471,6 +535,7 @@ class _EditUserInfoPage2State extends State identical(autographValue, _noChange) ? null : autographValue as String?, + countryId: selectedCountry?.id, ); final mergedProfile = updatedProfile.copyWith( userAvatar: _preferUsableAvatar( @@ -516,6 +581,21 @@ class _EditUserInfoPage2State extends State (identical(bornYearValue, _noChange) ? birthdayDate?.year : bornYearValue as num?), + countryId: _preferNonEmpty( + updatedProfile.countryId, + selectedCountry?.id, + ), + countryCode: _preferNonEmpty( + updatedProfile.countryCode, + selectedCountry?.alphaTwo, + ), + countryName: _preferNonEmpty( + updatedProfile.countryName, + _preferNonEmpty( + selectedCountry?.countryName, + selectedCountry?.aliasName, + ), + ), ); _syncLocalProfileState(mergedProfile); if (!mounted) { @@ -529,13 +609,49 @@ class _EditUserInfoPage2State extends State true; setState(() {}); SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); - } catch (e) { + if (selectedCountry != null) { + AccountStorage().logout(context); + return; + } + } catch (_) { + // The repository/network layer already shows request errors, so keep the + // current edit page state and only release the submit/loading guards here. } finally { _isSubmitting = false; SCLoadingManager.hide(); } } + Future _openCountryPicker() async { + final userIdentity = + Provider.of( + context, + listen: false, + ).userIdentity; + if (!_canChangeCountry(userIdentity)) { + return; + } + final generalManager = Provider.of( + context, + listen: false, + ); + generalManager.clearCountrySelection(); + await SCNavigatorUtils.push(context, CountryRoute.country, replace: false); + if (!mounted) { + return; + } + final selectedCountry = generalManager.selectCountryInfo; + if (selectedCountry == null) { + return; + } + final selectedCountryId = (selectedCountry.id ?? "").trim(); + if (selectedCountryId.isEmpty || + selectedCountryId == (countryId ?? "").trim()) { + return; + } + submit(countryValue: selectedCountry); + } + // ignore: unused_element void _showSexDialog() { showCenterDialog( diff --git a/test/hotgame_bridge_test.dart b/test/hotgame_bridge_test.dart new file mode 100644 index 0000000..428ba1d --- /dev/null +++ b/test/hotgame_bridge_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/modules/room_game/bridge/hotgame_js_bridge.dart'; +import 'package:yumi/modules/room_game/data/models/room_game_models.dart'; + +void main() { + test('normalizes documented hotgame recharge and quit bridge actions', () { + expect(HotgameBridgeActions.normalize('recharge'), 'recharge'); + expect(HotgameBridgeActions.normalize('pay'), 'recharge'); + expect(HotgameBridgeActions.normalize('gameRecharge'), 'recharge'); + expect(HotgameBridgeActions.normalize('quit'), 'quit'); + expect(HotgameBridgeActions.normalize('closeGame'), 'quit'); + expect(HotgameBridgeActions.normalize('destroy'), 'quit'); + }); + + test('parses hotgame bridge payload maps', () { + final message = HotgameBridgeMessage.parse( + '{"action":"gameRecharge","payload":{"source":"button"}}', + ); + + expect(message.action, HotgameBridgeActions.recharge); + expect(message.payload['source'], 'button'); + }); + + test( + 'bootstrap script contains documented hotgame client bridge methods', + () { + final script = HotgameJsBridge.bootstrapScript(); + + expect(script, contains('window.JsBridge')); + expect(script, contains("installJsBridgeFunction('recharge'")); + expect(script, contains("installJsBridgeFunction('quit'")); + expect(script, contains("installWebkitHandler('recharge'")); + expect(script, contains("installWebkitHandler('quit'")); + expect(script, contains('window.rechargeSuccess')); + }, + ); + + test('detects hotgame room game list items', () { + const item = RoomGameListItemModel( + gameId: 'hg_1', + provider: 'HOTGAME', + gameType: 'HOTGAME', + providerGameId: 'Seven7', + name: 'Lucky77', + cover: '', + category: 'CHAT_ROOM', + sort: 1, + launchMode: 'H5_REMOTE', + fullScreen: false, + gameMode: 3, + safeHeight: 0, + orientation: 0, + packageVersion: '2026', + status: 'ENABLED', + launchParams: {'screenMode': 'seven'}, + ); + + expect(item.isHotgame, isTrue); + expect(item.isBaishun, isFalse); + expect(item.isLeader, isFalse); + }); +} From e2a488cdf86a64f7313ac2e18adefeec80865f5e Mon Sep 17 00:00:00 2001 From: roxy Date: Wed, 24 Jun 2026 11:35:43 +0800 Subject: [PATCH 3/7] iPhone only --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d03fab7..e670fa1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -510,7 +510,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; name = Profile; @@ -647,7 +647,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -701,7 +701,7 @@ SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; name = Release; From 3a6010001db48390b9049a5f1386a2ebac5be5df Mon Sep 17 00:00:00 2001 From: roxy Date: Wed, 24 Jun 2026 11:58:20 +0800 Subject: [PATCH 4/7] iOS 1.0.0+101 --- ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ ios/Runner/Info.plist | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e670fa1..b7bf204 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -524,7 +524,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1631; + CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -533,7 +533,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.3; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1631; + CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -724,7 +724,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.3; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -744,7 +744,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1631; + CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -753,7 +753,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.3; + MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 76689b7..8fab214 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -57,7 +57,7 @@ CFBundleVersion - 100 + 101 LSRequiresIPhoneOS NSCameraUsageDescription From bae5ee44af6c6932ccc2c82e569d83fd89c7ae33 Mon Sep 17 00:00:00 2001 From: roxy Date: Fri, 26 Jun 2026 16:40:25 +0800 Subject: [PATCH 5/7] =?UTF-8?q?iOS=E9=9A=90=E8=97=8F=E5=85=85=E5=80=BC?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- lib/modules/user/me_page2.dart | 25 ++++++++ .../wallet/recharge/recharge_page.dart | 58 +++++++----------- sc_images/index/sc_icon_cp_entry.png | Bin 0 -> 6340 bytes 需求进度.md | 22 +++++++ 6 files changed, 72 insertions(+), 41 deletions(-) create mode 100644 sc_images/index/sc_icon_cp_entry.png diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b7bf204..0b12671 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -524,7 +524,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 101; + CURRENT_PROJECT_VERSION = 102; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 101; + CURRENT_PROJECT_VERSION = 102; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -744,7 +744,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 101; + CURRENT_PROJECT_VERSION = 102; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8fab214..ff3b0e0 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -57,7 +57,7 @@ CFBundleVersion - 101 + 102 LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index a9b42e2..795d265 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -37,6 +37,8 @@ class MePage2 extends StatefulWidget { class _MePage2State extends State with WidgetsBindingObserver { static const Duration _counterStaleDuration = Duration(seconds: 30); + static const String _cpActivityUrl = + 'https://h5.global-interaction.com/activity/cp-space/yumi.html'; static const String _inviteActivityUrl = 'https://h5.haiyihy.com/app-invite/index.html'; static const double _inviteActivityHorizontalScale = 750 / 662; @@ -299,6 +301,21 @@ class _MePage2State extends State with WidgetsBindingObserver { ); } + void _openCpActivity() { + final cpActivityUrl = _cpActivityUrl.trim(); + if (cpActivityUrl.isEmpty) { + if (kDebugMode) { + debugPrint('[MePage2] cp activity url is empty'); + } + return; + } + + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(cpActivityUrl)}&showTitle=false", + ); + } + String _appendToken(String url) { final token = AccountStorage().getToken(); if (token.isEmpty) return url; @@ -480,6 +497,14 @@ class _MePage2State extends State with WidgetsBindingObserver { iconPath: 'sc_images/index/sc_icon_bag.png', onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags), ), + SizedBox(width: 10.w), + _buildEntryItem( + title: 'cp', + iconPath: 'sc_images/index/sc_icon_cp_entry.png', + iconWidth: 47, + iconHeight: 50, + onTap: _openCpActivity, + ), ], ); } diff --git a/lib/modules/wallet/recharge/recharge_page.dart b/lib/modules/wallet/recharge/recharge_page.dart index eaa3c30..1eb18a3 100644 --- a/lib/modules/wallet/recharge/recharge_page.dart +++ b/lib/modules/wallet/recharge/recharge_page.dart @@ -37,11 +37,6 @@ class _RechargePageState extends State { context, listen: false, ).initializePaymentProcessor(context); - } else if (Platform.isIOS) { - Provider.of( - context, - listen: false, - ).initializePaymentProcessor(context); } } @@ -60,7 +55,7 @@ class _RechargePageState extends State { .getRechargePageScaffoldBackgroundColor(), resizeToAvoidBottomInset: false, appBar: SocialChatStandardAppBar( - title: SCAppLocalizations.of(context)!.recharge, + title: SCAppLocalizations.of(context)!.wallet, actions: [ GestureDetector( onTap: _showRecordDialog, @@ -83,38 +78,27 @@ class _RechargePageState extends State { _buildWalletSection(), SizedBox(height: 36.w), Expanded( - child: Padding( - padding: EdgeInsets.fromLTRB(12.w, 8.w, 12.w, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildMethodSection(), - SizedBox(height: 14.w), - Expanded(child: _buildNativeProductList()), - if (Platform.isIOS) ...[ - SizedBox(height: 12.w), - _buildFooterButton( - title: - SCAppLocalizations.of( - context, - )!.restorePurchases, - onTap: () { - Provider.of( - context, - listen: false, - ).recoverTransactions(); - }, + child: + Platform.isIOS + ? const SizedBox.shrink() + : Padding( + padding: EdgeInsets.fromLTRB(12.w, 8.w, 12.w, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMethodSection(), + SizedBox(height: 14.w), + Expanded(child: _buildNativeProductList()), + SizedBox(height: 12.w), + _buildFooterButton( + title: + SCAppLocalizations.of(context)!.recharge, + onTap: _processNativePurchase, + ), + SizedBox(height: 14.w), + ], + ), ), - ], - SizedBox(height: 12.w), - _buildFooterButton( - title: SCAppLocalizations.of(context)!.recharge, - onTap: _processNativePurchase, - ), - SizedBox(height: 14.w), - ], - ), - ), ), ], ), diff --git a/sc_images/index/sc_icon_cp_entry.png b/sc_images/index/sc_icon_cp_entry.png new file mode 100644 index 0000000000000000000000000000000000000000..9b2c19d72845b8a1ca60795ce35fe21fba394f95 GIT binary patch literal 6340 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 4 -> 1 -> 4` 这类来回闪动。 - 已按 2026-04-20 最新调整撤掉 `Wallet -> Recharge` 中新增的 MiFaPay 第三方支付 UI 与页面逻辑:当前充值页仅保留原生 `Google Pay / Apple Pay` 入口与商品列表,`Recharge methods` 区域也已收敛为单一原生支付卡片;此前接入的 MiFaPay 方法选择、底部弹窗、H5 收银台页以及对应页面级状态管理已从现有充值链路移除,避免继续对当前版本产生影响。 +- [x] 2026-06-26 App Review 收口:已调整 `Wallet -> Recharge` 页面,iOS 仅展示钱包余额,不再初始化/展示 Recharge methods、商品空态、恢复购买和充值按钮;顶部标题改为 Wallet,页面静态检查通过。 +- [x] 2026-06-26 iOS 提审版本号调整:Runner 的 `MARKETING_VERSION` 保持 `1.0.0`,`CFBundleVersion` / `CURRENT_PROJECT_VERSION` 已调整为 `102`,对应 App Store Connect 显示 `1.0.0 (102)`。 - 已按 2026-04-18 联调要求继续收口幸运礼物链路:项目默认 API host 已临时切到 `http://192.168.110.43:1100/` 方便直连测试环境;`/gift/give/lucky-gift` 请求体也已补齐为最新结构,除原有 `giftId/quantity/roomId/acceptUserIds/checkCombo` 外,会稳定携带 `gameId: null`、`accepts: []`、`dynamicContentId: null`、`songId: null` 这几组字段,便于和当前后端参数定义保持一致。 - 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。 - 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `>= 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。 From a44bdd7287511964568098986c19e62293e53139 Mon Sep 17 00:00:00 2001 From: roxy Date: Thu, 2 Jul 2026 19:13:13 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=96=99?= =?UTF-8?q?=E5=8D=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/room/online/room_online_page.dart | 3 +- .../room/rank/room_gift_rank_tab_page.dart | 59 +++++---- lib/services/audio/rtc_manager.dart | 10 +- lib/services/auth/user_profile_manager.dart | 78 +++++------ lib/ui_kit/widgets/room/empty_mai_select.dart | 6 +- .../widgets/room/room_user_info_card.dart | 123 +++++++++++++++--- 6 files changed, 165 insertions(+), 114 deletions(-) diff --git a/lib/modules/room/online/room_online_page.dart b/lib/modules/room/online/room_online_page.dart index 0c989b6..af9b51d 100644 --- a/lib/modules/room/online/room_online_page.dart +++ b/lib/modules/room/online/room_online_page.dart @@ -13,7 +13,6 @@ import '../../../ui_kit/components/sc_page_list.dart'; import '../../../ui_kit/components/sc_tts.dart'; import '../../../ui_kit/components/text/sc_text.dart'; import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; -import '../../../shared/tools/sc_lk_dialog_util.dart'; import '../../../ui_kit/widgets/badge/sc_user_badge_strip.dart'; import '../../../ui_kit/widgets/room/room_user_info_card.dart'; @@ -44,7 +43,7 @@ class _RoomOnlinePageState if (dialogContext == null) { return; } - showBottomInBottomDialog(dialogContext, RoomUserInfoCard(userId: userId)); + RoomUserInfoCard.showPreloaded(dialogContext, userId: userId); }); } diff --git a/lib/modules/room/rank/room_gift_rank_tab_page.dart b/lib/modules/room/rank/room_gift_rank_tab_page.dart index 36e9f2a..49a644c 100644 --- a/lib/modules/room/rank/room_gift_rank_tab_page.dart +++ b/lib/modules/room/rank/room_gift_rank_tab_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/main.dart'; import 'package:yumi/app_localizations.dart'; @@ -13,7 +13,6 @@ import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_page_list.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart'; import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart'; import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; @@ -98,11 +97,11 @@ class _RoomGiftRankTabPageState // headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl, ), onTap: () { - showBottomInBottomDialog( - navigatorKey.currentState!.context, - RoomUserInfoCard(userId: userProfile?.id), - ); SmartDialog.dismiss(tag: "showRoomGiftRankPage"); + RoomUserInfoCard.showPreloaded( + navigatorKey.currentState!.context, + userId: userProfile?.id, + ); }, ), SizedBox(width: 3.w), @@ -119,7 +118,7 @@ class _RoomGiftRankTabPageState needScroll: (userProfile?.userNickname?.characters.length ?? 0) > - 14, + 14, ), ], ), @@ -137,7 +136,7 @@ class _RoomGiftRankTabPageState animationHeight: 24.w, showTextBesideAnimated: true, animatedTextSpacing: 0, - showAnimatedGradientText: + showAnimatedGradientText: userProfile ?.shouldShowColoredSpecialIdText() ?? false, @@ -195,24 +194,24 @@ class _RoomGiftRankTabPageState SocialChatUserProfile? _mergeCurrentUserVip( SocialChatUserProfile? profile, ) { - if (profile == null || profile.vipLevelForColoredId > 0) { - return profile; - } - final currentProfile = AccountStorage().getCurrentUser()?.userProfile; - if (currentProfile == null || - currentProfile.vipLevelForColoredId <= 0 || - currentProfile.id != profile.id) { - return profile; - } - return profile.copyWith( - vipLevel: - currentProfile.vipLevel ?? - currentProfile.vipLevelForColoredId.toString(), - ); - } - - @override - empty() { + if (profile == null || profile.vipLevelForColoredId > 0) { + return profile; + } + final currentProfile = AccountStorage().getCurrentUser()?.userProfile; + if (currentProfile == null || + currentProfile.vipLevelForColoredId <= 0 || + currentProfile.id != profile.id) { + return profile; + } + return profile.copyWith( + vipLevel: + currentProfile.vipLevel ?? + currentProfile.vipLevelForColoredId.toString(), + ); + } + + @override + empty() { return mainEmpty( image: Image.asset( 'sc_images/general/sc_icon_loading.png', diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index a8450a6..c6272da 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -5197,10 +5197,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (clickUser.id == AccountStorage().getCurrentUser()?.userProfile?.id) { ///是自己,直接打开资料卡 - showBottomInBottomDialog( - context!, - RoomUserInfoCard(userId: clickUser.id), - ); + RoomUserInfoCard.showPreloaded(context!, userId: clickUser.id); } else { showBottomInBottomDialog( context!, @@ -5441,10 +5438,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (normalizedUserId.isEmpty) { return; } - showBottomInBottomDialog( - context!, - RoomUserInfoCard(userId: normalizedUserId), - ); + RoomUserInfoCard.showPreloaded(context!, userId: normalizedUserId); } addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { diff --git a/lib/services/auth/user_profile_manager.dart b/lib/services/auth/user_profile_manager.dart index d5adb4e..6b640ee 100644 --- a/lib/services/auth/user_profile_manager.dart +++ b/lib/services/auth/user_profile_manager.dart @@ -250,7 +250,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { return sessionSerial; } - Future roomUserCard( + Future roomUserCard( String roomId, String userId, { int? sessionSerial, @@ -272,36 +272,31 @@ class SocialChatUserProfileManager extends ChangeNotifier { isUserCardLoading = false; notifyListeners(); } - return; + return null; } try { - RoomUserCardRes? card = await SCChatRoomRepository().roomUserCard( - normalizedRoomId, - normalizedUserId, + final card = await _loadHydratedRoomUserCard( + roomId: normalizedRoomId, + userId: normalizedUserId, ); - if (requestSerial != _userCardRequestSerial) { - return; + if (requestSerial != _userCardRequestSerial || + activeSessionSerial != _activeUserCardSessionSerial) { + return null; } userCardInfo = card; userCardLoadError = null; isUserCardLoading = false; - isUserCardRelationLoading = card.userProfile != null; + isUserCardRelationLoading = false; notifyListeners(); - unawaited( - _hydrateRoomUserCard( - card: card, - userId: normalizedUserId, - requestSerial: requestSerial, - sessionSerial: activeSessionSerial, - ), - ); + return card; } catch (error, stackTrace) { if (requestSerial != _userCardRequestSerial) { - return; + return null; } userCardInfo = null; userCardLoadError = error; + isUserCardRelationLoading = false; _debugProfileLoadFailure( scope: 'roomCard', userId: normalizedUserId, @@ -314,6 +309,23 @@ class SocialChatUserProfileManager extends ChangeNotifier { notifyListeners(); } } + return null; + } + + Future preloadRoomUserCardForOpen( + String roomId, + String userId, { + int? sessionSerial, + }) { + return roomUserCard(roomId, userId, sessionSerial: sessionSerial); + } + + Future _loadHydratedRoomUserCard({ + required String roomId, + required String userId, + }) async { + final card = await SCChatRoomRepository().roomUserCard(roomId, userId); + return _withCurrentVipLevelForCard(card, userId); } Future _hydrateUserInfoById({ @@ -340,38 +352,6 @@ class SocialChatUserProfileManager extends ChangeNotifier { } } - Future _hydrateRoomUserCard({ - required RoomUserCardRes? card, - required String userId, - required int requestSerial, - required int sessionSerial, - }) async { - try { - final hydratedCard = await _withCurrentVipLevelForCard(card, userId); - if (requestSerial != _userCardRequestSerial || - sessionSerial != _activeUserCardSessionSerial || - hydratedCard == null) { - return; - } - userCardInfo = hydratedCard; - userCardLoadError = null; - isUserCardRelationLoading = false; - notifyListeners(); - } catch (error, stackTrace) { - if (requestSerial == _userCardRequestSerial && - sessionSerial == _activeUserCardSessionSerial) { - isUserCardRelationLoading = false; - notifyListeners(); - } - _debugProfileLoadFailure( - scope: 'roomCardHydrate', - userId: userId, - error: error, - stackTrace: stackTrace, - ); - } - } - void clearRoomUserCard({int? sessionSerial}) { if (sessionSerial != null && sessionSerial != _activeUserCardSessionSerial) { diff --git a/lib/ui_kit/widgets/room/empty_mai_select.dart b/lib/ui_kit/widgets/room/empty_mai_select.dart index 1b192f4..f83ecfa 100644 --- a/lib/ui_kit/widgets/room/empty_mai_select.dart +++ b/lib/ui_kit/widgets/room/empty_mai_select.dart @@ -9,7 +9,6 @@ import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_screen.dart'; -import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; @@ -266,10 +265,7 @@ class EmptyMaiSelect extends StatelessWidget { context, () { Navigator.of(context).pop(); - showBottomInBottomDialog( - context, - RoomUserInfoCard(userId: clickUser?.id), - ); + RoomUserInfoCard.showPreloaded(context, userId: clickUser?.id); }, ), ); diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index dd04730..baecda2 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -48,8 +48,91 @@ import '../../../modules/chat/chat_route.dart'; class RoomUserInfoCard extends StatefulWidget { final String? userId; + final String? roomId; + final int? sessionSerial; + final bool preloaded; - const RoomUserInfoCard({super.key, this.userId}); + const RoomUserInfoCard({ + super.key, + this.userId, + this.roomId, + this.sessionSerial, + this.preloaded = false, + }); + + static int _openRequestSerial = 0; + + static Future showPreloaded( + BuildContext context, { + required String? userId, + }) async { + final normalizedUserId = (userId ?? '').trim(); + if (normalizedUserId.isEmpty) { + return; + } + final openSerial = ++_openRequestSerial; + final sourceRoomId = _resolveCurrentRoomId(context); + final dialogContext = navigatorKey.currentState?.context ?? context; + final userProvider = Provider.of( + dialogContext, + listen: false, + ); + final roomId = + sourceRoomId.isNotEmpty + ? sourceRoomId + : _resolveCurrentRoomId(dialogContext); + final sessionSerial = userProvider.beginRoomUserCardSession(); + SCLoadingManager.show(); + room_card.RoomUserCardRes? card; + try { + card = await userProvider.preloadRoomUserCardForOpen( + roomId, + normalizedUserId, + sessionSerial: sessionSerial, + ); + } finally { + if (openSerial == _openRequestSerial) { + SCLoadingManager.hide(); + } + } + if (openSerial != _openRequestSerial) { + return; + } + final latestDialogContext = navigatorKey.currentState?.context ?? context; + if (!latestDialogContext.mounted) { + userProvider.clearRoomUserCard(sessionSerial: sessionSerial); + return; + } + if (card == null) { + userProvider.clearRoomUserCard(sessionSerial: sessionSerial); + SCTts.show( + SCAppLocalizations.of(latestDialogContext)?.loadingFailedClickToRetry ?? + 'Loading failed, click to retry', + ); + return; + } + showBottomInBottomDialog( + latestDialogContext, + RoomUserInfoCard( + userId: normalizedUserId, + roomId: roomId, + sessionSerial: sessionSerial, + preloaded: true, + ), + ); + } + + static String _resolveCurrentRoomId(BuildContext context) { + try { + return Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ""; + } catch (_) { + return ""; + } + } @override State createState() => _RoomUserInfoCardState(); @@ -76,12 +159,16 @@ class _RoomUserInfoCardState extends State { context, listen: false, ); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) { - return; - } - _loadRoomUserCard(); - }); + roomId = widget.roomId?.trim() ?? ""; + _userCardSessionSerial = widget.sessionSerial; + if (!widget.preloaded) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _loadRoomUserCard(); + }); + } SCAccountRepository().userIdentity(userId: widget.userId).then((v) { if (!mounted) { return; @@ -96,12 +183,7 @@ class _RoomUserInfoCardState extends State { context, listen: false, ); - roomId = - Provider.of( - context, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id ?? - ""; + roomId = RoomUserInfoCard._resolveCurrentRoomId(context); final sessionSerial = userProvider?.beginRoomUserCardSession(); _userCardSessionSerial = sessionSerial; userProvider?.roomUserCard( @@ -244,12 +326,13 @@ class _RoomUserInfoCardState extends State { required bool relationLoading, }) { final defaultSheetHeight = screenHeight * _profileSheetHeightFactor; + final compactBadgeHeightDelta = 10.w; if (relationLoading) { - return defaultSheetHeight; + return defaultSheetHeight + compactBadgeHeightDelta; } final hasCpRelation = _hasCpRelation(profile); final hasCloseFriends = _closeFriendProfileData(profile).isNotEmpty; - var resolvedHeight = defaultSheetHeight; + var resolvedHeight = defaultSheetHeight + compactBadgeHeightDelta; if (!hasCpRelation) { resolvedHeight -= 126.w; } @@ -602,16 +685,16 @@ class _RoomUserInfoCardState extends State { ) { return SizedBox( width: double.infinity, - height: 20.w, + height: 30.w, child: Center( child: SCUserBadgeStrip( userId: userId, fallbackBadges: _roomCardFallbackBadges(ref, profile), displayScope: SCUserBadgeDisplayScope.short, - height: 20.w, - badgeHeight: 20.w, - longBadgeWidth: 44.w, - spacing: 5.w, + height: 30.w, + badgeHeight: 28.w, + longBadgeWidth: 62.w, + spacing: 6.w, maxBadges: 3, wrap: false, reserveSpace: false, From 02b4fafb3ee518497d73bf75e30e705d9969a6d6 Mon Sep 17 00:00:00 2001 From: roxy Date: Tue, 7 Jul 2026 19:13:27 +0800 Subject: [PATCH 7/7] Fix room bottom mic visibility after feature sync --- lib/services/audio/rtc_manager.dart | 63 ++++++++++++++++++++++------ test/rtc_self_mic_snapshot_test.dart | 29 +++++++++++++ 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 6e00c7d..f1fe40d 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -523,6 +523,30 @@ class RealTimeCommunicationManager extends ChangeNotifier { return currentUserId.isNotEmpty && currentUserId == userId.trim(); } + Set _currentUserIdentityKeys() { + return _userProfileIdentityKeys( + AccountStorage().getCurrentUser()?.userProfile, + ); + } + + Set _userProfileIdentityKeys(SocialChatUserProfile? user) { + return { + user?.id?.trim() ?? "", + user?.account?.trim() ?? "", + user?.getID().trim() ?? "", + }..removeWhere((item) => item.isEmpty); + } + + bool _userProfileMatchesAnyIdentity( + SocialChatUserProfile? user, + Set identityKeys, + ) { + if (identityKeys.isEmpty) { + return false; + } + return _userProfileIdentityKeys(user).any(identityKeys.contains); + } + ///房间红包列表 List redPacketList = []; @@ -5418,14 +5442,15 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///自己是否在麦上 bool isOnMai() { - final currentUserId = - (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); - if (currentUserId.isEmpty) { + final currentUserKeys = _currentUserIdentityKeys(); + if (currentUserKeys.isEmpty) { return false; } for (final entry in roomWheatMap.entries) { - if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() == - currentUserId) { + if (_userProfileMatchesAnyIdentity( + micAtIndexForDisplay(entry.key)?.user, + currentUserKeys, + )) { return true; } } @@ -5434,25 +5459,39 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///自己是否在指定的麦上 bool isOnMaiInIndex(num index) { - return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() == - (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); + return _userProfileMatchesAnyIdentity( + micAtIndexForDisplay(index)?.user, + _currentUserIdentityKeys(), + ); } ///点击的用户在哪个麦上 num userOnMaiInIndex(String userId) { + return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true); + } + + num _findUserOnMaiInIndex( + String userId, { + required bool useDisplaySeatForCurrentUser, + }) { final normalizedUserId = userId.trim(); if (normalizedUserId.isEmpty) { return -1; } num index = -1; + final currentUserKeys = _currentUserIdentityKeys(); + final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId); + final lookupKeys = {normalizedUserId}; + if (isCurrentUserQuery) { + lookupKeys.addAll(currentUserKeys); + } + lookupKeys.removeWhere((item) => item.isEmpty); roomWheatMap.forEach((k, value) { - final visibleSeat = - normalizedUserId == - (AccountStorage().getCurrentUser()?.userProfile?.id ?? "") - .trim() + final candidateSeat = + useDisplaySeatForCurrentUser && isCurrentUserQuery ? micAtIndexForDisplay(k) : value; - if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) { + if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) { index = k; } }); diff --git a/test/rtc_self_mic_snapshot_test.dart b/test/rtc_self_mic_snapshot_test.dart index e767f29..0f473af 100644 --- a/test/rtc_self_mic_snapshot_test.dart +++ b/test/rtc_self_mic_snapshot_test.dart @@ -30,4 +30,33 @@ void main() { expect(rtcProvider.roomWheatMap[3]?.user?.id, 'user-1'); expect(rtcProvider.isOnMai(), isTrue); }); + + test('self mic lookup matches account when mic user id differs', () { + SharedPreferences.setMockInitialValues({}); + AccountStorage().setCurrentUser( + SocialChatLoginRes( + userProfile: SocialChatUserProfile( + id: 'local-user-id', + account: 'account-1', + userNickname: 'Me', + ), + ), + ); + final rtcProvider = + RealTimeCommunicationManager() + ..roomWheatMap = { + 2: MicRes( + micIndex: 2, + user: SocialChatUserProfile( + id: 'mic-user-id', + account: 'account-1', + userNickname: 'Me', + ), + ), + }; + + expect(rtcProvider.isOnMai(), isTrue); + expect(rtcProvider.isOnMaiInIndex(2), isTrue); + expect(rtcProvider.userOnMaiInIndex('local-user-id'), 2); + }); }