high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

Badge appears as a small filled circle (design token color: error/accent) overlaid on the top-right corner of the Notifications tab icon
Badge displays the numeric unread count when count is 1–99
Badge displays '99+' when unread count exceeds 99
Badge is hidden (not merely transparent) when unread count is 0
Badge animates in with a scale transition (0 → 1) when count goes from 0 to ≥ 1, and animates out (1 → 0) when count returns to 0
After mark-all-read action fires, badge disappears within one render frame (no stale count shown)
Badge subscribes to the unread count stream from NotificationRepository and reflects any new notification received via Supabase Realtime within 2 seconds
The bottom nav tab item's Semantics label dynamically reads 'Notifications, N unread' (or 'Notifications, no unread messages' when 0) for VoiceOver/TalkBack
Widget disposes its stream subscription when removed from the widget tree with no memory leaks
Badge renders correctly at all system font scale settings (0.8x–2.0x) without clipping or overlapping adjacent tab icons

Technical Requirements

frameworks
Flutter
Riverpod or BLoC (match existing bottom nav state management)
apis
Supabase Realtime stream via NotificationRepository.watchUnreadCount()
data models
Notification (unread count derived field)
performance requirements
Stream subscription must not cause unnecessary widget rebuilds outside the badge widget subtree
AnimatedContainer or ScaleTransition duration ≤ 200ms
security requirements
Unread count stream must be scoped to the authenticated user via Supabase RLS — never expose another user's count
ui components
Stack widget to overlay badge on tab icon
AnimatedSwitcher or ScaleTransition for badge appear/disappear animation
Container with BoxDecoration (circular) for badge background
Text widget for count label
Semantics widget wrapping the tab item

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement as a stateful widget (or ConsumerWidget/BlocBuilder) that wraps a StreamBuilder listening to NotificationRepository.watchUnreadCount(). Use an AnimatedSwitcher with a ScaleTransition child keyed on count > 0 to handle animate-in/out. Format count with a helper: int → String where value > 99 returns '99+'. Wrap the entire bottom nav tab in a Stack, placing the NotificationBadgeWidget at Alignment.topRight with a small positive offset.

For Semantics, override the tab item's semanticsLabel at the NavigationDestination level rather than inside the badge itself to avoid duplicate announcements. Ensure the Supabase Realtime channel is opened once at the repository layer and shared — do not open a new channel per widget instance.

Testing Requirements

Unit tests: test badge label formatting — 0 returns empty/hidden, 1 returns '1', 99 returns '99', 100 returns '99+', 999 returns '99+'. Widget tests: render NotificationBadgeWidget with a StreamController; push values 0, 5, 100 and assert rendered text and visibility. Verify ScaleTransition completes (use tester.pumpAndSettle). Verify Semantics label updates correctly for each count value.

Integration test: simulate a Supabase Realtime event incrementing the unread count and verify badge updates. Memory leak test: mount and unmount widget, assert StreamSubscription is cancelled. Target 85% line coverage.

Component
Notification Badge Widget
ui low
Epic Risks (2)
medium impact medium prob technical

The notification badge widget depends on a persistent Supabase Realtime websocket subscription for live unread count updates. On mobile, network transitions (WiFi to cellular, background app state) can silently drop the websocket, resulting in a stale badge count that does not update until the next app foreground — reducing trust in the notification system.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management in the badge widget's BLoC that re-subscribes on app foreground and on network reconnection events. Add a fallback polling query (every 60 seconds when app is foregrounded) to reconcile the badge count if the Realtime subscription is interrupted.

Contingency: If Realtime reliability proves insufficient in production, replace the live subscription with a polling approach using a configurable interval, accepting slightly delayed badge updates in exchange for reliability.

medium impact medium prob technical

The notification list item widget requires merged semantics combining title, body, timestamp, read state, and role-context icon into a single VoiceOver/TalkBack announcement. Getting the merged semantics structure right for both iOS (VoiceOver) and Android (TalkBack) simultaneously is non-trivial and common to break silently when widgets are refactored.

Mitigation & Contingency

Mitigation: Use the project's existing semantics-wrapper-widget pattern with explicit Semantics widgets and excludeSemantics on decorative children. Write accessibility widget tests using Flutter's SemanticsController to assert the exact announcement string. Test on physical devices with VoiceOver and TalkBack enabled before release.

Contingency: If merged semantics cannot be achieved cleanly on both platforms, implement platform-specific semantic trees using defaultTargetPlatform branching, ensuring each platform receives an optimal announcement even if the implementation differs.