Implement session lifecycle hooks in TabStateManager
epic-navigation-and-gesture-accessibility-foundation-task-009 — Add lifecycle hooks to TabStateManager that respond to app resume, logout, and role-switch events. On resume: reload snapshots from repository if the in-memory state is stale. On logout and role-switch: call repository.clearAll() then reset in-memory state to default tab index (0) and empty snapshots. Subscribe to the AppLifecycleState stream and the auth state stream to trigger these hooks automatically.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use `ref.listen(authStateProvider, (previous, next) { ... })` inside `build()` to subscribe to auth changes — this is automatically disposed when the provider is disposed. For `AppLifecycleState`, use `AppLifecycleListener(onResume: _onResume)` registered in `build()` and disposed in `ref.onDispose(() => _lifecycleListener.dispose())`. Staleness check: store a `DateTime _lastMutatedAt` field in the notifier and compare on resume.
The 5-minute threshold should be a named constant `_staleThreshold = Duration(minutes: 5)`. For the logout/role-switch reset, use a single `_resetState()` private method that calls `clearAll` and then `state = AsyncData(_defaultTabState)` — reuse this for both triggers. The BankID and Vipps logout paths must go through the same auth provider so the stream emits correctly — coordinate with the auth epic to ensure this contract. Do not call `GoRouter.go()` from the notifier — let the shell widget observe `activeTabIndex` via `ref.watch` and call `shellNavigator.goBranch(0)` reactively.
Testing Requirements
Unit tests in `test/navigation/tab_state_manager_lifecycle_test.dart` using `ProviderContainer` with fake providers: (1) Emit `AppLifecycleState.resumed` with stale timestamp (> 5 min) — assert repository read was called and state was refreshed. (2) Emit `AppLifecycleState.resumed` with fresh timestamp (< 5 min) — assert repository read was NOT called. (3) Emit logout auth event — assert `repository.clearAll()` called and `state.activeTabIndex == 0` and all snapshots are empty. (4) Emit role-switch auth event — assert same clearAll + reset behavior as logout.
(5) Dispose the notifier — assert no further events are processed after disposal (subscription leak test using a broadcast stream controller). (6) Emit logout during active `updateStackSnapshot` call — assert final state is the reset state, not a race-condition hybrid. Target: 90%+ line coverage, all race conditions tested.
StatefulShellRoute branch navigator state can interact unexpectedly with GoRouter's imperative navigation (go, push, replace), causing state snapshots to desync from actual route stacks. This could manifest as a user returning to a tab and seeing a different screen than expected, breaking the core motor-fatigue promise.
Mitigation & Contingency
Mitigation: Write integration tests that simulate cross-tab navigation with nested pushes before any UI layer is built. Pin go_router to a tested minor version and review the StatefulShellRoute changelog before upgrading.
Contingency: If branch navigator state consistently desyncs, fall back to a manual stack snapshot strategy using a custom NavigatorObserver that records and replays navigation events independently of StatefulShellRoute internals.
Persisted navigation stacks in shared_preferences can become stale or corrupt if route paths are renamed during development, causing app crashes or infinite redirect loops on cold start for users who have an old snapshot.
Mitigation & Contingency
Mitigation: Version the persisted schema with a format key. On app start, validate that all stored route paths exist in the current route config before restoring; silently discard invalid entries rather than crashing.
Contingency: Implement a safe-mode cold start that skips state restoration after a detected crash (via a dirty-launch flag written at startup and cleared on successful first frame), falling back to the default root tab.