feat(app): session restore on startup — no forced re-login after app restart
用户反馈:关闭 App 重开每次都要登录。根因:会话已持久化(安全存储, 服务端 TTL 30 天),但启动固定落在登录页——无人消费已存会话。 - _StartupGate(home):watch sessionAccount——在场直进 HomePage,离场 LoginPage;状态驱动(401/登出清会话后 gate 自动重建,不依赖导航时序) - onUnauthorized 保留 push 兜底(home 路由被显式导航替换后的 401 场景) - 测试适配(boot 401 两项重写通过;397/397 全绿) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,7 @@ import 'locale_controller.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// 根导航键:[ApiClient.onUnauthorized](非 UI 上下文触发)跳转 /login 用。
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
/// App 根组件:主题 + l10n 委托 + 命名路由 + 401 驱逐接线 + 引擎生命周期。
|
||||
///
|
||||
@@ -36,9 +35,12 @@ class _PhotoLiveAppState extends ConsumerState<PhotoLiveApp> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.read(apiClientProvider).onUnauthorized = () {
|
||||
rootNavigatorKey.currentState
|
||||
?.pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
// 401 已清会话(ApiClient):刷新账号观察 → 引擎 stop。
|
||||
rootNavigatorKey.currentState?.pushNamedAndRemoveUntil(
|
||||
'/login',
|
||||
(route) => false,
|
||||
);
|
||||
// 401 已清会话(ApiClient):刷新账号观察 → 引擎 stop;启动门
|
||||
// (home 路由仍在栈上时)随会话离场重建为登录页。
|
||||
ref.read(sessionAccountProvider.notifier).refresh();
|
||||
};
|
||||
}
|
||||
@@ -84,7 +86,39 @@ class _PhotoLiveAppState extends ConsumerState<PhotoLiveApp> {
|
||||
'/password-change': (context) => const PasswordChangePage(),
|
||||
'/server-history': (context) => const ServerHistoryPage(),
|
||||
},
|
||||
home: const LoginPage(),
|
||||
// 启动门(UI06 用户反馈 2026-09-23:重启 App 不应强制重登):有未过期
|
||||
// 会话直接进主页,否则登录页。
|
||||
home: const _StartupGate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动门:读本地会话——未过期直接进 HomePage,否则 LoginPage。
|
||||
///
|
||||
/// 会话已持久化(SecureSessionStore,服务端 TTL 默认 30 天);此前的缺口
|
||||
/// 是启动固定落在登录页。token 实际失效(服务端已撤销)时由 ApiClient 401
|
||||
/// 兜底:清会话并整栈回登录页(onUnauthorized),过期本地判断只用于避免
|
||||
/// 「必 401 的闪跳」。
|
||||
class _StartupGate extends ConsumerStatefulWidget {
|
||||
const _StartupGate();
|
||||
|
||||
@override
|
||||
ConsumerState<_StartupGate> createState() => _StartupGateState();
|
||||
}
|
||||
|
||||
class _StartupGateState extends ConsumerState<_StartupGate> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 状态驱动(声明式):watch 会话账号——在场进 HomePage,离场回
|
||||
// LoginPage。401/登出清会话后 gate 自动重建,不依赖导航时序;home
|
||||
// 路由已被显式导航替换(登录成功 pushReplacement '/home' 等)时,
|
||||
// 401 兜底走 onUnauthorized 的 push。
|
||||
final account = ref.watch(sessionAccountProvider);
|
||||
return account.maybeWhen(
|
||||
skipLoadingOnReload: false,
|
||||
orElse: () =>
|
||||
const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
data: (account) => account != null ? const HomePage() : const LoginPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user