critical priority medium complexity frontend pending backend specialist Tier 5

Acceptance Criteria

PeerMentorStatsScreen calls a roleGuard() helper on initState or via GoRouter redirect; non-peer-mentor roles are immediately redirected to /no-access
The role check reads the role from the Supabase session JWT claims or the app's authStateProvider — never from a locally mutable variable
userId passed to the stats provider is sourced exclusively from Supabase auth.currentUser.id — route parameters and query strings cannot override it
A coordinator or admin visiting the peer mentor stats URL is redirected, not shown an empty screen or an error
Riverpod provider for peer mentor stats is a family provider keyed by userId — two different userId values produce two independent provider instances with no data leakage between them
Provider override in tests can inject a scoped userId without touching the auth layer (testable isolation)
If the auth session expires mid-screen, the provider emits an AsyncError and the screen shows a session-expired message with a login redirect button
No userId, role, or session token is written to widget state, only read via ref.watch on the auth provider
Role guard logic is extracted into a reusable requireRole(Role.peerMentor) guard (not inlined in the widget)

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Auth — currentUser, session JWT role claim
data models
UserSession
AppRole (enum)
PeerMentorStatsData
performance requirements
Role check must resolve synchronously from cached session — no async gap that would briefly show the screen to the wrong role
security requirements
Role must be verified against the Supabase JWT server claim, not a client-side role field that can be tampered with
Supabase Row Level Security (RLS) policies must enforce userId scoping at the database level — client-side scoping is defence-in-depth only
Log a security warning (non-blocking) if a non-peer-mentor role attempts to access this route
ui components
No new UI components — guard is logic-only
Existing NoAccessScreen for redirect target
SessionExpiredBanner or full-screen SessionExpiredWidget

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Implement requireRole as a GoRouter redirect callback on the route definition, not as a runtime check inside the widget — this prevents any flash of content for unauthorised users. Example: redirect: (context, state) { final role = ref.read(currentRoleProvider); return role == AppRole.peerMentor ? null : '/no-access'; }. For userId isolation, use peerMentorStatsProvider(userId) family where userId = ref.watch(authStateProvider).requireValue.userId.

The .requireValue call will throw if session is null, which AsyncNotifier will catch and surface as AsyncError — wire the error state to show the session-expired widget. Never call ref.read(authStateProvider).userId in a button callback to pass to a provider; always use ref.watch in the provider itself.

Testing Requirements

Unit tests for requireRole() guard: (1) peer-mentor role returns allowed; (2) coordinator role returns redirect; (3) admin role returns redirect; (4) null session returns redirect to login. Widget tests: (5) non-peer-mentor auth override redirects via GoRouter — verify tester finds NoAccessScreen; (6) valid peer-mentor session renders PeerMentorStatsScreen; (7) expired session triggers session-expired UI. Use ProviderScope overrides for authStateProvider; do not call real Supabase auth.

Component
Peer Mentor Stats Screen
ui medium
Epic Risks (4)
high impact high prob technical

fl_chart renders chart elements on a Canvas, making individual bars and data points invisible to the Flutter Semantics tree by default. Without explicit Semantics wrappers, VoiceOver and TalkBack users receive no chart information, violating the WCAG 2.2 AA requirement mandated by all three partner organizations.

Mitigation & Contingency

Mitigation: Wrap the fl_chart widget in a Semantics node with a dynamically generated textual description of the chart data (e.g., 'Bar chart: January 12, February 8, March 15 sessions'). Implement a collapsible data table alternative beneath the chart that screen readers can navigate row by row. Validate with VoiceOver on iOS and TalkBack on Android before the epic is marked complete.

Contingency: If fl_chart's Canvas rendering cannot be made accessible within the epic timeline, ship the chart hidden from the Semantics tree with ExcludeSemantics and promote the data table alternative to first-class UI so screen reader users have full access to the information. Log a tech-debt item to revisit native chart accessibility in a future sprint.

medium impact medium prob scope

Coordinators managing up to 5 chapters (NHF requirement) require the PeerMentorStatsList to display chapter affiliation labels for each row. With large chapter lists and many peer mentors, the list could become overwhelming and cause layout overflow or scrolling performance issues on lower-end Android devices.

Mitigation & Contingency

Mitigation: Implement chapter filtering as a segmented control above the list so coordinators can scope the list to one chapter at a time. Use ListView.builder (lazy rendering) rather than a Column of all rows. Profile scroll performance on a low-end Android device (Pixel 4a equivalent) with 50 peer mentors in scope during development.

Contingency: If multi-chapter display causes unacceptable performance, ship with single-chapter scope as the default view and a chapter switcher dropdown, deferring the combined cross-chapter list to a follow-up sprint.

low impact low prob technical

Summary cards and the chart widget rebuilding simultaneously on provider state change could cause a visible jank frame on slower devices, degrading perceived quality especially since this screen is intended to feel motivating and polished for gamification purposes.

Mitigation & Contingency

Mitigation: Use AnimatedSwitcher with a short fade transition (150ms) on the stats cards and chart so that data replacement feels intentional rather than jarring. Profile with Flutter DevTools on a mid-range device and ensure no frame exceeds 16ms during a time-window switch.

Contingency: If animation introduces complexity that delays delivery, ship without animation and use a loading skeleton (shimmer effect) during re-fetch instead, which is simpler and equally effective at masking the data swap.

high impact low prob security

If the role-based screen dispatch is misconfigured, a peer mentor could navigate to the coordinator stats screen and see aggregated chapter data for all peer mentors, which is a data privacy violation and a compliance risk for all three organizations.

Mitigation & Contingency

Mitigation: Implement role guard at the router level using an existing role-route-guard component so the coordinator screen route is unreachable for peer mentor roles. Add a widget test that mounts the coordinator screen with a peer mentor session token and asserts that the guard redirects to the no-access screen.

Contingency: If a bypass is found in QA, add a secondary in-screen role assertion in the coordinator screen's initState that throws an AuthorizationException and navigates to the error screen, ensuring defence in depth regardless of router configuration.