high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Badge renders as a circular overlay in the top-right corner of the Notifications tab icon using a Stack widget
Badge displays the exact unread count when count is between 1 and 99 inclusive
Badge displays '99+' string when unread count exceeds 99
Badge is fully hidden (not merely transparent) when unread count is zero — no empty circle visible
Badge background color uses the design token red-accent color constant, not a hardcoded hex value
Badge text uses the design token white color constant with a readable font size (minimum 10sp)
BlocBuilder or StreamBuilder reacts to NotificationBLoC state changes and updates the badge count without requiring widget rebuild of the full navigation bar
Semantics widget wraps the badge and announces 'N unread notifications' where N is the current count
When count changes, the semantics live region triggers a screen-reader announcement (liveRegion: true or equivalent Flutter semantics flag)
Widget is self-contained and accepts no mutable state externally — count is derived entirely from BLoC
Widget test confirms: badge hidden at count=0, shows '1' at count=1, shows '99' at count=99, shows '99+' at count=100
No overflow or clipping of the badge circle on any screen density (ldpi through xxxhdpi)

Technical Requirements

frameworks
Flutter
BLoC
data models
NotificationBLoC state (unread count field)
NotificationState
performance requirements
Badge update must be reflected within one rendered frame of the BLoC state emission
Widget must not trigger a full navigation bar rebuild — use targeted BlocBuilder scope
No unnecessary widget allocations on each state update (use const constructors where possible)
security requirements
Badge count must be sourced exclusively from authenticated BLoC state — never from unauthenticated or cached-only data
ui components
Stack (Flutter built-in)
Positioned (Flutter built-in)
Container with BoxDecoration (circular shape)
Semantics (Flutter built-in)
BlocBuilder<NotificationBLoC, NotificationState>

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use BlocBuilder scoped tightly to only the unread count field — avoid rebuilding the entire nav bar on every notification event. The badge should be a stateless widget that receives nothing from its parent; all data flows from BLoC. Use design token constants for colors (e.g., AppColors.badgeBackground, AppColors.badgeText) — do not hardcode #FF0000. For the Semantics node, set `liveRegion: true` to ensure VoiceOver/TalkBack announces count changes when the user is elsewhere on screen.

Cap logic: `count > 99 ? '99+' : '$count'`. The Stack should use `clipBehavior: Clip.none` so the badge can overflow the icon boundary naturally. Position the badge at top-right using `Positioned(top: 0, right: 0)`.

Testing Requirements

Unit test NotificationTabBadge widget in isolation using flutter_test. Use a mock NotificationBLoC to emit states with varying unread counts (0, 1, 50, 99, 100, 200). Assert: (1) badge widget is absent from tree when count=0, (2) Text widget shows correct string for all count ranges, (3) Semantics label matches expected string. Test on standard phone size (375×812) and tablet (768×1024) to ensure no overflow.

Aim for 100% branch coverage on the count display logic (0, 1–99, 100+).

Component
Notification Tab Badge
ui low
Epic Risks (3)
medium impact medium prob integration

If a referenced entity (contact, certification, activity) has been deleted or its RLS policy now excludes the current user, the deep link handler may navigate to a screen that renders in an error state or throws an unhandled exception.

Mitigation & Contingency

Mitigation: The deep link handler must perform a lightweight existence check (HEAD request or minimal SELECT) before pushing the route. Define a contract with each destination screen for how to handle a not-found entity ID passed as a route parameter.

Contingency: If the existence check itself fails (network error), navigate to the destination screen anyway and let it handle the error gracefully with its own error state; do not block navigation for network timeouts.

medium impact low prob technical

If the tab badge widget triggers a full rebuild of the bottom navigation bar on every unread count change, it will cause visible jank on devices with many active Realtime events (e.g., org admins receiving org-wide alerts).

Mitigation & Contingency

Mitigation: Scope the badge widget to a dedicated BlocSelector that rebuilds only when the unread count value changes, not on any BLoC state emission. Use RepaintBoundary to isolate the badge from the rest of the nav bar.

Contingency: If performance issues persist, debounce badge updates to a maximum of one rebuild per 500ms and display the last known count during the debounce window.

high impact medium prob scope

Complex swipe-to-mark-read gestures and dynamic list updates may conflict with VoiceOver/TalkBack navigation patterns, particularly for Blindeforbundet users who rely exclusively on screen readers.

Mitigation & Contingency

Mitigation: Provide a dedicated accessibility action (Semantics.onTap / CustomSemanticsAction) for mark-as-read on each list item so screen reader users do not need the swipe gesture. Test with VoiceOver on iOS and TalkBack on Android before each release.

Contingency: If the swipe gesture proves incompatible with assistive technologies, disable it when a screen reader is detected (via ScreenReaderDetectionService) and rely solely on the tap-to-read and accessible action pathways.