critical priority medium complexity infrastructure pending frontend specialist Tier 1

Acceptance Criteria

BLoC emits BadgeLoading state on initial load event before any repository call completes
BLoC emits BadgeLoaded state with a list of EarnedBadge objects after successful fetch from badge-repository
BLoC emits BadgeError state with a human-readable message if badge-repository throws; error state includes a retry trigger
BLoC emits BadgeEmpty state (distinct from BadgeLoaded with empty list) when the mentor has zero earned badges
BLoC transitions to BadgeCelebration state when a BadgeEarnedEvent domain event is received; celebration state includes the newly earned badge
After celebration acknowledgement event, BLoC transitions back to BadgeLoaded with the updated badge list (new badge included)
BLoC manages badge-detail-modal visibility: emits BadgeDetailOpen(badgeId) and BadgeDetailClosed states correctly
In-memory cache is used on subsequent LoadBadges events within the same BLoC lifecycle; repository is not called again unless invalidated
Cache is invalidated and repository re-fetched on BadgeEarnedEvent
BLoC is properly closed and all stream subscriptions cancelled in close() method

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
flutter_test
bloc_test
apis
BadgeRepository.getEarnedBadges(mentorId)
BadgeDefinitionRepository.getAllDefinitions()
DomainEventBus.on<BadgeEarnedEvent>()
data models
EarnedBadge
BadgeDefinition
BadgeEarnedEvent
performance requirements
Initial load must complete within 2 seconds on a 4G connection
In-memory cache hit must return synchronously (no async gap after first load)
BLoC must not hold strong references to UI widgets
security requirements
Mentor ID sourced from authenticated session only — never from UI input
Domain event subscription must verify event's mentorId matches current session before transitioning to celebration state

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Extend Bloc (not Cubit) because the domain event subscription requires event-driven architecture. Subscribe to the DomainEventBus in the BLoC constructor using a StreamSubscription stored as a field; cancel it in close(). Use an in-memory List? field as the cache — null means unloaded, non-null means cached.

Merge badge definitions from BadgeDefinitionRepository into the earned list at load time so the UI always receives fully resolved BadgeCardModels without further repository calls. Avoid emitting intermediate states during cache-hit loads to prevent UI flicker. The celebration state should carry the full BadgeDefinition (name, image, description) to allow the overlay widget to render without additional async calls.

Testing Requirements

Use bloc_test package for all BLoC tests. Test sequences: (1) LoadBadges → BadgeLoading → BadgeLoaded; (2) LoadBadges with empty result → BadgeEmpty; (3) LoadBadges with repository error → BadgeError; (4) BadgeEarnedEvent received → BadgeCelebration → (acknowledge) → BadgeLoaded with updated list; (5) second LoadBadges within lifecycle → cache hit, repository not called. Mock both repositories with Mockito or mocktail. Assert stream subscriptions are cancelled in tearDown.

Minimum 95% line coverage.

Component
Badge BLoC
infrastructure medium
Epic Risks (2)
medium impact medium prob integration

The badge-earned-celebration overlay must appear within 2 seconds of the triggering activity being saved, but badge evaluation runs server-side in an edge function triggered by a database webhook. Network latency, edge function cold start, and Supabase Realtime delivery delays could cause the overlay to appear late or not at all, breaking the motivational loop.

Mitigation & Contingency

Mitigation: Implement an optimistic UI path: after activity save, badge-bloc immediately checks whether any badge thresholds are crossed client-side using cached stats and badge definitions, showing the overlay speculatively before server confirmation. The server result then reconciles. Subscribe to Supabase Realtime on the earned_badges table for authoritative confirmation.

Contingency: If Realtime delivery is unreliable in production, add a polling fallback: badge-bloc polls for new earned badges 3 seconds after an activity save and shows the overlay if a new record is detected, accepting up to 5-second latency as a fallback SLA.

high impact low prob technical

The celebration overlay uses animation for positive reinforcement, but motion sensitivity (prefers-reduced-motion) and screen reader users require a non-animated or text-only alternative. Failing to handle this risks excluding Blindeforbundet users or triggering vestibular discomfort for motion-sensitive volunteers.

Mitigation & Contingency

Mitigation: Check MediaQuery.disableAnimations in badge-earned-celebration-overlay and skip animation entirely when true, showing a static card instead. Add an ExcludeSemantics wrapper around the decorative animation widget and a separate Semantics node with a live region announcement of the badge name and congratulatory message.

Contingency: If accessibility issues are identified in TestFlight testing with Blindeforbundet's test group, fast-track a patch that defaults to the static card path and gates the animation behind a user preference setting in notification preferences.