high priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

MilestoneBadgeWidget accepts a MilestoneModel (containing id, title, description, artworkAssetPath) and a MilestoneBadgeState enum (locked / unlocked) as required constructor parameters
Unlocked state renders badge artwork at full opacity with the token-defined primary colour scheme and no overlay
Locked state renders badge artwork at reduced opacity (token-defined, e.g. 0.35) with a padlock icon centred as an overlay using a Stack
Locked state applies a greyscale colour filter to the badge artwork so colour is not the sole differentiator (WCAG 1.4.1 Use of Color)
All colour, opacity, border-radius, shadow, and spacing values reference design token constants — zero inline numeric literals
Padlock icon uses the token-defined icon size and colour for the muted/locked semantic
Widget renders without overflow at badge sizes 56 dp, 80 dp, and 120 dp (controlled by a single size parameter defaulting to the token-defined standard badge size)
Switching MilestoneBadgeState from locked to unlocked (widget rebuild) produces the correct visual state without retaining stale locked-state styling
Golden tests capture both locked and unlocked states at the default token size

Technical Requirements

frameworks
Flutter
flutter_test
apis
ColorFiltered (greyscale matrix)
Stack (overlay composition)
data models
MilestoneModel
MilestoneBadgeState (enum: locked, unlocked)
performance requirements
Widget must be const-constructible when MilestoneModel and state are final
ColorFiltered greyscale must not be applied in unlocked state — conditional wrapping only
ui components
MilestoneBadgeWidget
Padlock icon overlay
Badge artwork image (asset)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define a MilestoneBadgeState enum with two values: locked and unlocked. Implement the widget as a StatelessWidget accepting the enum. Use a Stack to layer: (1) the badge artwork image (Image.asset from MilestoneModel.artworkAssetPath); (2) conditionally, a ColorFiltered wrapping the artwork when state == MilestoneBadgeState.locked, using the standard greyscale matrix (ColorFilter.matrix([0.2126,0.7152,0.0722,0,0, ...])); (3) conditionally, a centred padlock Icon when locked. Wrap the entire Stack in a SizedBox using the token-defined badge size.

All opacity, border-radius, and colour values must be read from the design token extension on ThemeData. Avoid AnimatedSwitcher in this base layout task — state transition animation, if required, belongs in a later task. This widget does not include Semantics nodes — accessibility wiring is scoped to a subsequent task matching the pattern used for StatCardWidget.

Testing Requirements

Write widget tests verifying: (1) unlocked state renders artwork without ColorFiltered wrapper and without padlock icon; (2) locked state renders artwork inside ColorFiltered with greyscale matrix and padlock icon is present in the widget tree; (3) switching state from locked to unlocked (pumpWidget with new state) removes the greyscale filter and padlock; (4) no overflow at 56 dp, 80 dp, and 120 dp sizes. Write golden tests for both states at the default token size. Use tester.widget and tester.widgetList to make assertions without relying on pixel comparisons alone.

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.