critical priority medium complexity backend pending frontend specialist Tier 2

Acceptance Criteria

TabStateManager is implemented as a Riverpod StateNotifier<int> (or AsyncNotifier if persistence is async) in lib/navigation/tab_state_manager.dart
tabStateManagerProvider is a globally accessible Riverpod provider that exposes the TabStateManager
selectTab(int index) method validates index is in range 0–4; throws ArgumentError for out-of-range values
resetTab() method resets the active index to 0 (Home tab)
State change from selectTab() is immediately reflected in any widget watching tabStateManagerProvider.select()
NavigationStateRepository.persistTabIndex(int) is called after each selectTab() to survive app restart
On initialization, TabStateManager reads the last persisted tab index from NavigationStateRepository and restores it
The bottom nav bar widget rebuilds only when the tab index changes — no unnecessary rebuilds on unrelated state changes (verified with flutter_test's pumpAndSettle + build count assertion)
Tab selection driven by the GoRouter location (e.g., navigating directly via deep link) syncs the TabStateManager index to match
Unit test confirms selectTab(2) followed by app restart (simulated by re-creating the provider with the same repository) restores index 2

Technical Requirements

frameworks
Flutter
Riverpod (StateNotifier / AsyncNotifier)
go_router
data models
NavigationState (persisted tab index)
performance requirements
Tab index state reads must be O(1) — no database queries on the hot path
Persistence write must be fire-and-forget (async, not awaited in selectTab) to keep UI response instant
security requirements
Persisted tab index must not expose role-restricted routes — always validate the restored index against the current user's permitted routes on startup
ui components
BottomNavBar (consumer)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use Riverpod's StateNotifierProvider pattern. Keep the NavigationStateRepository behind an abstract interface so unit tests can inject a mock without hitting SharedPreferences or Supabase. Persistence can use SharedPreferences (local, synchronous-ish) since the tab index is a tiny scalar — no need for Supabase for this. The trickiest part is syncing GoRouter's current location back to the tab index when deep links are followed: use a GoRouter listener (router.routerDelegate.addListener) inside the notifier's constructor, compute which branch the new location falls under, and call an internal _syncIndex() method that updates state without triggering another navigation.

Avoid creating a circular dependency between GoRouter and TabStateManager by injecting the router as a parameter rather than reading it from a Riverpod ref inside the notifier. The five tab indices must map to named constants: kHomeTab = 0, kContactsTab = 1, kAddTab = 2, kWorkTab = 3, kNotificationsTab = 4.

Testing Requirements

Write unit tests with flutter_test and Riverpod's ProviderContainer for isolated testing. Test 1: Initial state is 0. Test 2: selectTab(3) sets state to 3. Test 3: selectTab(5) throws ArgumentError.

Test 4: resetTab() after selectTab(3) returns state to 0. Test 5: Mock NavigationStateRepository returning savedIndex=2 — provider initializes to 2. Test 6: Pump a widget with a Consumer watching tabStateManagerProvider — call selectTab(1), pumpAndSettle, confirm only one rebuild occurred. Test 7: Simulate app restart by creating a new ProviderContainer with the same mock repository that returns savedIndex=4 — confirm restored state is 4.

Target 100% branch coverage on TabStateManager logic.

Component
Tab State Manager
service medium
Epic Risks (3)
high impact high prob technical

Flutter's ModalBottomSheet and showDialog do not automatically confine VoiceOver or TalkBack focus to the modal's subtree on all platform versions. Background content may remain reachable by screen readers, confusing users and violating WCAG 2.2 criterion 1.3.1.

Mitigation & Contingency

Mitigation: Wrap modal content in an ExcludeSemantics or BlockSemantics widget for background content. Use a Semantics node with liveRegion on the modal container and manually request focus via FocusScope after the modal animation completes. Test on both iOS (VoiceOver) and Android (TalkBack) during widget development.

Contingency: If platform-level focus trapping is unreliable, implement a custom modal wrapper widget that uses a FocusTrap widget (available in Flutter's internal tooling) and an Overlay entry with semantics blocking on the dimmed background layer.

medium impact medium prob technical

On iOS, the system-level swipe-back gesture (UINavigationController) can bypass PopScope and GoRouter's gesture suppression, meaning users can still accidentally dismiss screens via swipe even after the component is implemented. This breaks the gesture-free contract for motor-impaired users.

Mitigation & Contingency

Mitigation: Set popGestureEnabled: false in GoRouter route configurations where swipe-back is suppressed. Test specifically against Flutter's CupertinoPageRoute, which respects this flag, and verify that GoRouter generates Cupertino routes on iOS rather than Material routes with gesture enabled.

Contingency: If go_router's popGestureEnabled flag does not propagate correctly, wrap affected routes in a WillPopScope replacement (PopScope with canPop: false) and file a bug with the go_router maintainers. Document the workaround in the navigation-route-config component for future maintainers.

medium impact medium prob scope

The feature description implies migrating all existing ModalBottomSheet and dialog call sites across the app to use the new accessible helpers, which is a cross-cutting change. Scope underestimation could mean the epic finishes the new components but leaves many call sites un-migrated, leaving the accessibility promise partially broken.

Mitigation & Contingency

Mitigation: Audit all existing modal call sites at the start of the epic (grep for showModalBottomSheet, showDialog, showCupertinoDialog) and add the count to the task list. Treat migration as explicit tasks, not an implied post-step.

Contingency: If migration scope grows beyond the epic's estimate, create a follow-up tech-debt epic scoped only to call-site migration, and gate the release on at minimum all flows used by the accessibility user-story acceptance criteria being migrated.