high priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Bottom sheet opens from a badge-card-widget tap and displays the badge-card-widget as the modal header without duplication
Earned badges show formatted earned timestamp (e.g., 'Earned on 12 March 2025') in the modal body
Locked badges show a progress indicator (e.g., '3 of 5 sessions completed') derived from badge-bloc state
Full criteria description text is displayed with no truncation, scrollable if content overflows
Focus is automatically moved to the modal on open; closing the modal returns focus to the triggering element
Dismissing via back button, swipe down, or scrim tap all close the modal correctly
All text elements meet WCAG 2.2 AA contrast ratio (minimum 4.5:1 for body text, 3:1 for large text) against the modal background
VoiceOver and TalkBack can navigate all modal content in logical reading order
A Semantics label announces 'Badge detail: [badge name], [earned/locked status]' when the modal opens
Modal renders correctly on screen sizes from 320px width upward without horizontal overflow
No visual or functional regression on badge-card-widget when embedded in the modal header

Technical Requirements

frameworks
Flutter
BLoC
data models
Badge
BadgeProgress
BadgeCriteria
performance requirements
Modal open animation completes within 300ms
No jank during bottom sheet drag gesture (maintain 60fps)
security requirements
Modal must not expose badge IDs or internal criteria logic in widget keys or debug labels accessible to end users
ui components
badge-card-widget (reused as header)
DraggableScrollableSheet or showModalBottomSheet
Progress indicator widget (LinearProgressIndicator or custom)
Semantics widget wrapping modal root
Design token typography and spacing

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use showModalBottomSheet with isScrollControlled: true and a DraggableScrollableSheet for content that may overflow. Embed the existing badge-card-widget at the top of the sheet by passing the BadgeState directly — do not recreate the card's internal structure. Use BlocBuilder inside the modal to read progress/earned data reactively. For focus management, wrap the modal root with a FocusScope and call FocusScope.of(context).requestFocus() in the sheet's initState.

Announce modal open via SemanticsService.announce() with AssertiveQueue to trigger immediate screen reader announcement. Apply design tokens exclusively for colors, spacing, and typography — no hardcoded values. Avoid using Navigator.pop inside the modal for the drag-to-dismiss path; rely on Flutter's built-in DraggableScrollableSheet dismiss behaviour.

Testing Requirements

Write widget tests (flutter_test) verifying: (1) modal renders with badge-card-widget header for both earned and locked badge states, (2) earned timestamp is displayed when badge state is earned, (3) progress text is displayed when badge state is locked, (4) modal closes on back-button press and returns focus. Add golden tests for earned and locked states. Perform manual accessibility audit with VoiceOver (iOS) and TalkBack (Android) to confirm focus order and announcements. Use flutter_test Semantics matchers to assert label content.

Minimum 80% widget test coverage for badge-detail-modal.

Component
Badge Detail Modal
ui low
Epic Risks (2)
medium impact medium prob integration

The badge-earned-celebration overlay must appear within 2 seconds of the triggering activity being saved, but badge evaluation runs server-side in an edge function triggered by a database webhook. Network latency, edge function cold start, and Supabase Realtime delivery delays could cause the overlay to appear late or not at all, breaking the motivational loop.

Mitigation & Contingency

Mitigation: Implement an optimistic UI path: after activity save, badge-bloc immediately checks whether any badge thresholds are crossed client-side using cached stats and badge definitions, showing the overlay speculatively before server confirmation. The server result then reconciles. Subscribe to Supabase Realtime on the earned_badges table for authoritative confirmation.

Contingency: If Realtime delivery is unreliable in production, add a polling fallback: badge-bloc polls for new earned badges 3 seconds after an activity save and shows the overlay if a new record is detected, accepting up to 5-second latency as a fallback SLA.

high impact low prob technical

The celebration overlay uses animation for positive reinforcement, but motion sensitivity (prefers-reduced-motion) and screen reader users require a non-animated or text-only alternative. Failing to handle this risks excluding Blindeforbundet users or triggering vestibular discomfort for motion-sensitive volunteers.

Mitigation & Contingency

Mitigation: Check MediaQuery.disableAnimations in badge-earned-celebration-overlay and skip animation entirely when true, showing a static card instead. Add an ExcludeSemantics wrapper around the decorative animation widget and a separate Semantics node with a live region announcement of the badge name and congratulatory message.

Contingency: If accessibility issues are identified in TestFlight testing with Blindeforbundet's test group, fast-track a patch that defaults to the static card path and gates the animation behind a user preference setting in notification preferences.