critical priority high complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

MapViewScreen is a full-screen StatefulWidget that uses BlocBuilder/BlocListener to react to MentorLocationState variants: loading, loaded, error
Screen layout is a Stack with three named zones: map canvas (bottom), overlay controls (middle), FAB area (top/front)
Loading state renders a centered CircularProgressIndicator with a Semantics label readable by VoiceOver and TalkBack
Loaded state renders the map canvas placeholder widget (or the MapProviderIntegration widget when wired in a subsequent task)
Error state renders a user-facing error message with a retry button, both accessible via screen reader
On screen mount, a screen-reader announcement is made with the page title (e.g., 'Peer mentor map') using SemanticsService.announce or equivalent
All interactive elements (FAB, controls) have Semantics(label: ...) wrappers with descriptive Norwegian or English labels matching the organisation labels system
Screen passes flutter accessibility audit: no missing semantic labels on tappable elements, no color-only information, sufficient contrast (WCAG 2.2 AA)
BLoC is provided via BlocProvider at the screen level or injected via Riverpod, not instantiated inside the widget build method
Screen is registered in the app router (GoRouter or equivalent) and is reachable via a named route

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
data models
assignment
performance requirements
Screen must reach first meaningful paint (loading indicator visible) within 100ms of navigation
BlocBuilder rebuild must not cause full widget tree rebuild — use buildWhen to limit rebuilds to state type changes
Stack layout must not cause RenderFlex overflow on any screen size from 320px to 428px width
security requirements
Screen must not log mentor location data to the console or analytics in any state
Error messages shown to users must not expose internal Supabase error codes or stack traces
ui components
MapViewScreen (Scaffold + Stack)
LoadingOverlay (CircularProgressIndicator + Semantics)
ErrorBanner (error text + retry button + Semantics)
MapCanvasPlaceholder (Container, replaced by map widget in later task)
MapOverlayControlsZone (positioned overlay)
MapFabArea (FAB placeholder)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use BlocConsumer (combines BlocBuilder + BlocListener) to handle both UI rebuilds and one-off side effects (e.g., showing a SnackBar on error). Define MentorLocationState as a sealed class with MentorLocationInitial, MentorLocationLoading, MentorLocationLoaded, and MentorLocationError subclasses. In the Stack, use Positioned.fill for the map canvas so it occupies the full screen behind overlays. For the screen-reader announcement, call SemanticsService.announce('Peer mentor map', TextDirection.ltr) in initState after the first frame using WidgetsBinding.instance.addPostFrameCallback.

Use the organisation labels system for any user-facing strings — avoid hardcoding. Apply design tokens for spacing, typography, and color rather than hardcoded values. The FAB zone and overlay controls zone should be Positioned widgets with sufficient touch target size (minimum 48×48dp per WCAG 2.2 AA). Add an ExcludeSemantics wrapper around the map canvas itself so VoiceOver does not attempt to read map tile labels — provide a separate Semantics description of the map region instead.

Testing Requirements

Widget tests using flutter_test: (1) pump MapViewScreen with BlocProvider wrapping a mock BLoC emitting MentorLocationLoading — assert CircularProgressIndicator is present and has a Semantics label; (2) emit MentorLocationLoaded — assert loading indicator is gone and map canvas placeholder is present; (3) emit MentorLocationError — assert error message and retry button are visible and have Semantics labels; (4) verify retry button triggers the expected BLoC event. Accessibility test: use tester.getSemantics() to assert all interactive elements have non-empty labels. Golden test optional but recommended for the Stack layout structure. All tests must pass with tester.pumpAndSettle() without requiring real BLoC events — use MockBloc from bloc_test package.

Component
Map View Screen
ui high
Epic Risks (3)
high impact high prob technical

Flutter's map canvas (flutter_map) does not natively support semantic focus traversal for screen readers, meaning map markers may be entirely invisible to VoiceOver/TalkBack users. If the accessible list fallback is not treated as a first-class view, screen reader users will have no access to the feature.

Mitigation & Contingency

Mitigation: From sprint 1, treat mentor-list-fallback as a fully featured primary view, not an afterthought. Implement and test it in parallel with the map canvas. Make the view-toggle-button keyboard focusable and announced on every screen state. Conduct VoiceOver testing on device before submitting each PR touching UI components.

Contingency: If map canvas accessibility cannot be achieved for marker focus traversal, make the view-toggle-button the default focus target on screen load for VoiceOver users (detected via screen-reader-detection-service) so they are immediately directed to the list fallback without needing to discover the toggle.

medium impact medium prob technical

The mentor-info-popup must occupy no more than 40% of visible map area on small screens. On devices with screen heights under 667px (iPhone SE), overlapping with the filter panel or obscuring most of the map could severely degrade usability.

Mitigation & Contingency

Mitigation: Implement the popup as a bottom sheet capped at 40% of screen height with a ScrollView for overflow content. Test on iPhone SE (375x667pt) and the smallest commonly used Android form factor in the device lab. Define max-height as a percentage constant in location-privacy-config or design tokens.

Contingency: If the popup cannot fit all required fields within 40% height on smallest targets, truncate assigned contact count and certification badge to icons-only in the compact view, with a 'View Profile' button always visible at the bottom of the popup regardless of scroll position.

medium impact medium prob integration

Filter state must remain perfectly synchronised between the map view and the list fallback. If the filter panel emits state that is not consumed identically by both views, coordinators switching between views will see inconsistent mentor sets, eroding trust in the feature.

Mitigation & Contingency

Mitigation: Store active filter criteria in a single shared Riverpod provider owned by the map-view-screen and consumed by both map-marker-widget (via mentor-location-service) and mentor-list-fallback. Write integration tests that apply a filter, switch views, and assert identical mentor counts in both views.

Contingency: If filter sync proves brittle, simplify to a single filter state object passed explicitly as a constructor argument to both views on each rebuild, eliminating indirect state sharing.