Implement Badge Card Widget
epic-achievement-badges-ui-task-005 — Build the reusable badge-card-widget Flutter UI component that renders a single badge in both earned (full colour, timestamp) and locked (greyscale, progress indicator) visual states. Ensure WCAG 2.2 AA contrast compliance, include Semantics label for VoiceOver/TalkBack, and support tap callback for badge detail navigation.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Use ColorFilter.matrix with a luminance-preserving greyscale matrix for the locked state — do not maintain a separate greyscale asset. Design token colours must be consumed from the project's token system (e.g., AppColors.badgeEarned, AppColors.badgeLocked) to support future theming. The widget should be purely presentational (no BLoC dependency); state is passed in via a BadgeCardModel. For progress, accept a double (0.0–1.0) rather than raw counts to keep the widget reusable.
Wrap the entire card in a Semantics node with `button: true` when onTap is non-null so screen readers announce it as interactive. Use ExcludeSemantics on the progress bar to avoid double-announcing progress — include it in the parent Semantics label string instead.
Testing Requirements
Unit tests: verify BadgeCardModel mapping for earned vs locked states; verify Semantics label string construction. Widget tests: render both states and assert key widget presence (Image, Text, LinearProgressIndicator, Semantics). Golden tests: capture screenshots for earned and locked at two breakpoints. Accessibility test: use flutter_test SemanticsHandle to assert no critical a11y violations.
Minimum 90% branch coverage on widget logic.
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.
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.