critical priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

StatCardWidget wraps its content in a Semantics widget whose label is the string generated by SummaryAccessibilityProvider.generateStatCardAnnouncement(finalValue, label)
The Semantics label uses the final target value, not the current animated intermediate value — screen readers never announce an intermediate number
A SemanticsService.announce call is made exactly once after animation completion (or immediately on mount when reduced-motion is true) to trigger the live-region announcement
During count-up animation, the Semantics node's label remains static (final value) — it does not update on each animation frame
The Semantics node has excludeSemantics: true for child nodes inside the card so child Text widgets do not create duplicate announcements
Accessibility audit (flutter_test's SemanticsHandle + tester.getSemantics) confirms: one node per card, correct label, no duplicate children
VoiceOver on iOS and TalkBack on Android both read the card as a single unit with the announcement string (manual verification checklist)
Widget tests using flutter_test semantics assertions verify the label equals the expected announcement string for a given value/label pair

Technical Requirements

frameworks
Flutter
flutter_test
apis
Semantics widget
SemanticsService.announce
SemanticsHandle (testing)
SummaryAccessibilityProvider
WrappedAnimationController (animation completion callback)
performance requirements
Semantics node update must not trigger a full widget rebuild — use a dedicated Semantics wrapper outside the AnimatedBuilder subtree
security requirements
Announcement strings must not contain PII — verify the string generator (task-003) does not leak sensitive data
ui components
StatCardWidget
Semantics wrapper

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Wrap the entire StatCardWidget content in a single Semantics(label: announcementString, excludeSemantics: true, child: ...) node. Compute announcementString once at build time using the final target value — never derive it from the animation's current value. To trigger the post-animation live-region announcement, add an addStatusListener to the AnimationController and call SemanticsService.announce(announcementString, TextDirection.ltr) when status == AnimationStatus.completed. When reduced-motion is true (no animation), call SemanticsService.announce in initState / after the first frame via WidgetsBinding.instance.addPostFrameCallback.

Avoid calling announce repeatedly — use a bool _hasAnnounced guard. The Semantics label must be the full human-readable sentence from the generator (e.g. '47 activities logged'), not a raw number.

Testing Requirements

Write widget tests using tester.semantics (or ensureSemantics()) to assert: (1) each card has exactly one top-level Semantics node; (2) the node's label matches generateStatCardAnnouncement output for the given inputs; (3) the label does not change during animation — pump to 50% duration and assert label is still the final-value string; (4) SemanticsService.announce is called once on animation completion and once on mount when reduced-motion is true. Mock SemanticsService.announce using a test double to count invocations.

Component
Animated Stat Card 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.