critical priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Each MilestoneBadgeWidget exposes a single Semantics node with a label matching the exact format: 'Badge locked: [name]. Requirement: [criterion]' when locked, or 'Badge earned: [name]. [description]' when unlocked
The Semantics label strings are sourced exclusively from SummaryAccessibilityProvider — no hardcoded strings in the widget itself
All decorative child widgets inside the badge (icon, colour overlay, lock graphic) are marked with excludeSemantics: true so screen readers do not announce redundant content
When badges appear in a horizontal shelf layout, the semantic focus order follows the visual left-to-right order (verified with Flutter's debugDumpSemanticsTree)
When a badge transitions from locked to unlocked, a liveRegion or SemanticsService.announce call triggers the unlocked announcement string so the change is communicated without the user re-focusing the element
The widget passes WCAG 2.2 AA criterion 1.3.1 (Info and Relationships) and 4.1.2 (Name, Role, Value)
TalkBack on Android and VoiceOver on iOS both read the correct label in manual spot-checks on physical or emulated devices

Technical Requirements

frameworks
Flutter
flutter_test
apis
SummaryAccessibilityProvider (Riverpod provider)
Flutter Semantics widget
SemanticsService.announce (for live region unlock announcement)
data models
MilestoneBadge (locked/unlocked state, name, description, criterion)
performance requirements
Semantics node construction must not trigger additional widget rebuilds beyond what state changes already cause
security requirements
Announcement strings must not include raw user identifiers or sensitive personal data beyond what is shown visually
ui components
MilestoneBadgeWidget
Semantics (Flutter built-in)
SummaryAccessibilityProvider
ExcludeSemantics (for decorative children)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Wrap the entire badge content in a single Semantics widget with label and excludeSemantics:false; wrap all internal decorative elements (image, icon, gradient overlay) with ExcludeSemantics. Pull announcement strings from SummaryAccessibilityProvider via a Riverpod Consumer or ref.watch — do not construct strings inside the widget. For the live announcement on unlock, use a didUpdateWidget lifecycle check: if oldWidget.badge.isUnlocked == false && widget.badge.isUnlocked == true, call SemanticsService.announce(label, TextDirection.ltr) after the frame. Do not use liveRegion:true on the Semantics node itself as this can cause double-announcements on some platforms; prefer the explicit announce call.

For shelf layout focus order, rely on the natural widget tree order (left-to-right rendering) — do not use sortKey unless the visual order genuinely differs from the tree order. This task is marked critical because Blindeforbundet users depend entirely on screen readers.

Testing Requirements

Widget tests using flutter_test and SemanticsController: (1) assert that tester.getSemantics(find.byType(MilestoneBadgeWidget)).label equals the locked format string when badge.isUnlocked=false; (2) assert the unlocked format string when badge.isUnlocked=true; (3) assert decorative child widgets have no semantics label (excludeSemantics); (4) simulate state change from locked→unlocked and assert SemanticsService.announce was called with the unlocked string. Use TestSemantics matchers. No golden tests required. Accessibility audit: run Flutter's SemanticsChecker in debug mode and confirm no warnings.

Component
Milestone Badge Widget
ui medium
Epic Risks (2)
medium impact medium prob technical

Simultaneous count-up animations across multiple stat cards and chart draw-in animations on lower-end Android devices may cause frame drops below 60fps, degrading the premium Wrapped experience and making the feature feel unpolished.

Mitigation & Contingency

Mitigation: Stagger animation starts using AnimationController with staggered intervals rather than starting all animations simultaneously. Use RepaintBoundary around each animated widget to isolate rasterisation. Profile on a mid-range Android device (e.g., equivalent to Pixel 4a) during development, not just at QA.

Contingency: If frame rate targets cannot be met on low-end devices, implement a device-capability check at startup and substitute simpler fade-in animations for the count-up and chart draw-in on devices below a CPU performance threshold.

medium impact low prob integration

The activity-type-breakdown-widget must render organisation-specific activity type labels sourced from the terminology system. If the terminology provider is not yet integrated at the time this widget is built, the widget will display hardcoded system labels, which is a regression risk for multi-org support.

Mitigation & Contingency

Mitigation: Accept activity type labels as a typed parameter in the widget constructor rather than reading from the terminology provider directly inside the widget. The BLoC or repository layer resolves labels before passing them to the widget, maintaining clean separation and testability.

Contingency: If terminology resolution is unavailable at widget integration time, display internal activity type keys as a temporary fallback with a localised suffix '(label pending)' visible only in non-production builds so QA can identify unresolved labels.