diff --git a/client/lib/l10n/app_text.dart b/client/lib/l10n/app_text.dart index 41ee9d6..53dc9cb 100644 --- a/client/lib/l10n/app_text.dart +++ b/client/lib/l10n/app_text.dart @@ -108,6 +108,10 @@ abstract class AppText { String get devForceLogoutConfirm; // 弹窗正文(含设备名占位 %s) String get devClearLoginConfirm; String get devCancel; + // 本设备被其他设备强制下线时的提示框。 + String get sessionRevokedTitle; + String get sessionRevokedBody; + String get sessionRevokedOk; String get redeemEntry; String get contactEntry; String get signOut; diff --git a/client/lib/l10n/strings_en.dart b/client/lib/l10n/strings_en.dart index b932090..109a3b9 100644 --- a/client/lib/l10n/strings_en.dart +++ b/client/lib/l10n/strings_en.dart @@ -162,6 +162,12 @@ class StringsEn extends AppText { @override String get devCancel => 'Cancel'; @override + String get sessionRevokedTitle => 'This device was signed out'; + @override + String get sessionRevokedBody => 'Your account signed this device out from another device. Please sign in again.'; + @override + String get sessionRevokedOk => 'Sign in again'; + @override String get redeemEntry => 'Redeem & buy'; @override String get contactEntry => 'Contact us'; diff --git a/client/lib/l10n/strings_zh.dart b/client/lib/l10n/strings_zh.dart index e4bb09c..2defd44 100644 --- a/client/lib/l10n/strings_zh.dart +++ b/client/lib/l10n/strings_zh.dart @@ -161,6 +161,12 @@ class StringsZh extends AppText { @override String get devCancel => '取消'; @override + String get sessionRevokedTitle => '此设备已被强制下线'; + @override + String get sessionRevokedBody => '你的账号在其他设备上将此设备强制退出,需要重新登录。'; + @override + String get sessionRevokedOk => '重新登录'; + @override String get redeemEntry => '兑换 & 购买'; @override String get contactEntry => '联系我们'; diff --git a/client/lib/main.dart b/client/lib/main.dart index 064b3b8..72c3ca8 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -132,6 +132,7 @@ class _RootFlowState extends ConsumerState { bool _autoConnectArmed = false; // 自动连接每进程只尝试一次 bool _homeLogged = false; // 仅日志:HomeShell 首次显示打一行 bool _sessionWatchdogArmed = false; // 会话有效性轮询每进程只起一次 + bool _forcedLogoutHandling = false; // 正在弹「被强制下线」框 + 登出,避免重复 Timer? _sessionPoll; @override @@ -149,19 +150,46 @@ class _RootFlowState extends ConsumerState { } Future _checkSession() async { - if (!mounted || !ref.read(authProvider).isLoggedIn) return; + if (!mounted || _forcedLogoutHandling || !ref.read(authProvider).isLoggedIn) return; try { final deviceId = await ref.read(deviceIdentityProvider).deviceId(); final active = await ref.read(accountApiProvider).sessionActive(deviceId); - if (!active && mounted && ref.read(authProvider).isLoggedIn) { - logLine('Session', 'server revoked this device → logout'); - await ref.read(authProvider.notifier).logout(); - } + if (active || !mounted || !ref.read(authProvider).isLoggedIn) return; + // 被其他设备强制下线:先弹对话框告知原因,用户确认后再登出跳登录页。 + _forcedLogoutHandling = true; + logLine('Session', 'server revoked this device → notify + logout'); + await _showForcedLogoutDialog(); + if (mounted) await ref.read(authProvider.notifier).logout(); + _forcedLogoutHandling = false; } catch (_) { // 网络/鉴权异常不据此登出(fail-safe):只在服务端明确 active=false 才登出。 } } + Future _showForcedLogoutDialog() async { + if (!mounted) return; + final c = context.pangolin; + final t = ref.read(appTextProvider); + await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + backgroundColor: c.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.xl)), + title: Text(t.sessionRevokedTitle, + style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), + content: Text(t.sessionRevokedBody, style: PangolinText.sm.copyWith(color: c.fg2, height: 1.5)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(t.sessionRevokedOk, + style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + // 进入登录态主界面后:开了「自动连接」且当前未连接,就自动连一次。 // 关键:必须同时等「设置已从持久化加载完(settings.loaded)」与「节点已就绪」—— // 二者都是异步且完成时刻接近,早读 autoConnect 会撞上还没加载完的默认 false(实测差 1ms)。