high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

When a MilestoneBadgeWidget transitions from locked to unlocked state, a scale animation starts at 0.0 and ends at 1.0 combined with a fade from 0.0 to 1.0 opacity, driven exclusively by WrappedAnimationController
The pop animation duration is between 300ms and 500ms with an easeOutBack or equivalent overshoot curve to produce a natural 'pop' feel
When SummaryAccessibilityProvider.reducedMotion is true, the badge renders the unlocked state immediately via a crossfade (opacity tween only, ≤150ms) with no scale transform
The animation triggers only once per unlock event — re-rendering the widget after unlock does not replay the animation
If the widget is disposed before the animation completes, no exceptions are thrown and the AnimationController is properly disposed
The widget correctly passes animation state to WrappedAnimationController and does not manage its own AnimationController directly
Both animation paths (full pop and reduced-motion crossfade) are visually verified on iOS and Android at standard and 2× text scale

Technical Requirements

frameworks
Flutter
flutter_test
apis
WrappedAnimationController API
SummaryAccessibilityProvider (Riverpod provider)
data models
MilestoneBadge (locked/unlocked state, name, description, criterion)
performance requirements
Animation must maintain 60fps on mid-range devices (Pixel 4a equivalent)
No unnecessary widget rebuilds during animation — use AnimatedBuilder scoped to the animated subtree only
Widget build method must not call setState during animation; use AnimationController listener pattern
security requirements
No user data is logged or transmitted as part of animation lifecycle events
ui components
MilestoneBadgeWidget
WrappedAnimationController
SummaryAccessibilityProvider
ScaleTransition or AnimatedBuilder with Transform.scale
FadeTransition

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use an AnimatedBuilder wrapping only the badge content (not the entire shelf) to avoid unnecessary rebuilds of sibling widgets. The scale + fade combination should be composed as a single CurvedAnimation fed to both tweens so they stay in sync. For the reduced-motion path, use a simple AnimatedOpacity or AnimatedSwitcher with duration ≤150ms. Store the 'has animated' state in the widget's State object (a bool _hasPlayed) initialized from the badge's current unlock status at build time — if the badge is already unlocked when the widget first mounts (e.g.

after a hot reload or screen re-entry), skip the animation entirely. Avoid using GlobalKey-based hero animations here; the pop should be self-contained within the badge bounds. Use design token values for animation durations if they are defined in the token system rather than hardcoding millisecond values.

Testing Requirements

Unit tests using flutter_test: (1) verify that with reducedMotion=false the AnimationController drives a scale tween from 0.0→1.0 and opacity tween from 0.0→1.0; (2) verify that with reducedMotion=true no scale transform is applied and only opacity animates; (3) pump widget to end of animation and assert final state shows unlocked badge at full opacity and scale=1.0; (4) simulate dispose mid-animation and assert no errors thrown. Widget tests should use a fake SummaryAccessibilityProvider to toggle reducedMotion. Golden tests for both locked and unlocked final states. No e2e tests required for this task.

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.