high priority low complexity infrastructure pending frontend specialist Tier 0

Acceptance Criteria

BadgeIconAssetManager is a Dart service class registered as a Riverpod provider
resolveIconPath(String iconRef, {required bool locked}) returns a String asset path — e.g., 'assets/badges/volunteer_bronze_unlocked.png' or 'assets/badges/volunteer_bronze_locked.png'
resolveIconPath returns the placeholder asset path ('assets/badges/placeholder.png') when the resolved path does not exist in the asset bundle
Runtime asset existence validation uses Flutter's rootBundle.load() wrapped in a try-catch; missing assets fall back to placeholder without throwing
validateContrast(Color foreground, Color background) returns a ContrastValidationResult with fields: ratio (double), passesAA (bool), passesAAA (bool)
Contrast ratio computed using the WCAG 2.2 relative luminance formula: L = 0.2126*R + 0.7152*G + 0.0722*B where R, G, B are linearised sRGB values
AA pass threshold is ratio >= 4.5:1 for normal text / 3.0:1 for large text; AAA pass threshold is ratio >= 7.0:1 — both checked against the 4.5 threshold as the default
validateContrast correctly handles white on white (ratio 1.0, both false) and black on white (ratio 21.0, both true)
Design token background colours are resolved via the existing project token system — BadgeIconAssetManager accepts a DesignTokens dependency
Locked icons must visually indicate inactivity — asset naming convention documented (e.g., suffix '_locked') and enforced in resolveIconPath
Service includes a static method computeLuminance(Color color) that is independently testable
Placeholder icon asset exists at 'assets/badges/placeholder.png' and is declared in pubspec.yaml

Technical Requirements

frameworks
Flutter
Riverpod
apis
Flutter AssetBundle (rootBundle)
Flutter dart:ui Color
WCAG 2.2 contrast algorithm
data models
ContrastValidationResult
DesignTokens
performance requirements
Asset existence validation should be cached after first resolution to avoid repeated rootBundle.load() calls for the same iconRef
ContrastValidationResult computation is pure/synchronous — no async operations in validateContrast
security requirements
Asset paths must be constructed from an allowlist of known iconRef values or validated against a safe pattern (alphanumeric + underscore only) to prevent path traversal
Do not expose internal asset directory structure through error messages
ui components
Placeholder badge icon widget (StatelessWidget wrapping Image.asset with the fallback path)
ContrastBadge debug widget (dev-only overlay showing contrast ratio — removable for production)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

The WCAG 2.2 relative luminance formula requires linearising sRGB values: for a channel value c (0–1), if c <= 0.04045 then linear = c/12.92, else linear = ((c + 0.055)/1.055)^2.4. Implement this as a private static method `_linearise(double c)`. The contrast ratio formula is (L1 + 0.05) / (L2 + 0.05) where L1 is the lighter luminance. Cache resolved asset paths in a `Map` keyed by '{iconRef}_{locked}' to avoid repeated async bundle lookups.

For the asset existence check, wrap `rootBundle.load(path)` in a try-catch returning the placeholder path on FlutterError — this check should be async and called lazily on first resolution. Make the asset base path configurable via a constructor parameter (default 'assets/badges/') to facilitate testing with a mock asset directory. Ensure the locked/unlocked naming convention is consistent with the visual design: locked icons should be desaturated/greyed versions of unlocked icons — document this expectation for the design team even if the Flutter code just resolves paths by naming convention.

Testing Requirements

Unit tests using flutter_test: (1) resolveIconPath returns correct unlocked path for a known iconRef, (2) resolveIconPath returns correct locked path when locked=true, (3) resolveIconPath returns placeholder path when asset does not exist (mock rootBundle to throw FlutterError), (4) computeLuminance(Colors.white) returns 1.0, computeLuminance(Colors.black) returns 0.0, (5) validateContrast(Colors.black, Colors.white) returns ratio ~21.0 and passesAA=true, passesAAA=true, (6) validateContrast(Colors.white, Colors.white) returns ratio 1.0 and both false, (7) validateContrast with a mid-grey pair returns ratio ~4.6 and passesAA=true, passesAAA=false. Use testWidgets for the placeholder widget render test. 95%+ coverage on the WCAG computation logic.

Component
Badge Icon Asset Manager
infrastructure low
Epic Risks (2)
high impact medium prob scope

Badge criteria are stored as structured JSON in badge_definitions. If the JSON schema for criteria (threshold counts, streak lengths, training completion flags) is not well-defined upfront, the evaluation service will be built against a moving target, requiring costly migrations and refactors.

Mitigation & Contingency

Mitigation: Define and document the criteria JSON schema in a shared type file before any repository code is written. Review the schema with all three organisations' badge requirements — especially Blindeforbundet's honorar thresholds — and version the JSON schema using a 'criteria_version' field from day one.

Contingency: If the criteria schema must change after services are built, write a Supabase migration to backfill existing rows and add a migration version column. Keep the evaluation service criteria parser isolated behind an interface so only one function needs updating.

medium impact medium prob dependency

Badge icon assets may not yet exist or may fail WCAG 2.2 AA contrast validation (minimum 3:1 for graphical objects) when rendered over design-token backgrounds. Missing or non-compliant icons could block UI epic delivery for Blindeforbundet, for whom screen reader and visual accessibility is non-negotiable.

Mitigation & Contingency

Mitigation: During this epic, implement the contrast-ratio validator in badge-icon-asset-manager and run it as a Flutter test against all candidate icon assets early. Coordinate with the design team to provide WCAG-compliant SVG icons in both locked and unlocked variants before the UI epic begins.

Contingency: If assets are late or fail contrast checks, ship placeholder icons that are guaranteed compliant (solid design-token colour fills with text labels) and swap in final assets post-QA without requiring a code change.