critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Overlay appears as a full-screen layer above all other content when badge-bloc emits a badge-earned event
Overlay auto-dismisses after the animation sequence completes without any user interaction required
User can manually dismiss the overlay early via a tap or accessible close button
Badge name and icon are prominently displayed with WCAG 2.2 AA contrast (4.5:1 text, 3:1 large/icon) against the overlay background
SemanticsService.announce() is called with '[Badge name] badge earned!' using TextDirection.ltr immediately on overlay display
Overlay is a live region: VoiceOver and TalkBack announce the badge name without the user navigating to the overlay
When 'Reduce Motion' is enabled, the animation is replaced with a simple fade rather than particles/confetti
Overlay does not block navigation stack — underlying routes remain intact for post-dismiss navigation
Multiple rapid badge-earned events are queued and played sequentially, not simultaneously
Overlay disposes all AnimationControllers and listeners on dismount to prevent memory leaks
No horizontal or vertical overflow of overlay content on any screen size

Technical Requirements

frameworks
Flutter
BLoC
data models
Badge
EarnedBadge
performance requirements
Celebration animation runs at 60fps; particle/confetti effects capped at 100 particles to avoid GPU overhead
Overlay appears within one frame of badge-earned event emission
ui components
OverlayEntry or showGeneralDialog for full-screen layer
AnimationController + multiple Tween animations for entrance, hold, and exit phases
Confetti or custom particle widget (conditional on Reduce Motion flag)
Semantics widget with liveRegion: true
SemanticsService.announce() call
Design token color and typography for badge name text

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement overlay display using Navigator.of(context).overlay?.insert(OverlayEntry(...)) rather than pushing a new route — this preserves the navigation stack. Manage a queue (List) inside a stateful widget or BLoC listener; process one overlay at a time, popping the queue on AnimationStatus.completed. Use three-phase AnimationController: entrance (scale + fade in), hold (static display), exit (fade out). Check MediaQuery.of(context).disableAnimations to conditionally replace particles with AnimatedOpacity fade.

Call SemanticsService.announce() in initState or the BLoC listener callback, not inside build(). Wrap badge name text in a Semantics widget with liveRegion: true for persistent announcement. All animation durations should be defined as constants in a shared animations.dart file for easy tuning. Dispose AnimationController in dispose() using a try/finally guard.

Testing Requirements

Write widget tests (flutter_test): (1) overlay renders with correct badge name when bloc emits badge-earned, (2) overlay auto-dismisses after animation duration (use fake async / tester.pump(duration)), (3) overlay can be manually dismissed via close gesture, (4) Reduce Motion suppresses particle animation and uses fade instead, (5) queuing logic shows overlays sequentially for two rapid badge-earned events. Assert SemanticsService.announce() is called with the correct badge name using a test observer. Manual test on iOS with VoiceOver to confirm live region announcement fires without focus navigation. Integration test: confirm underlying route remains navigable after overlay dismiss.

Minimum 75% coverage for overlay widget.

Component
Badge Earned Celebration Overlay
ui medium
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.