high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

Full mode renders the badge label, a formatted expiry date string (e.g., 'Expires 15 Jun 2025'), and a state-appropriate icon (e.g., checkmark for active, warning triangle for expiring-soon, X for expired)
displayMode parameter (ExpiryDisplayMode.compact / ExpiryDisplayMode.full) controls rendering path; compact mode behavior from task-002 is unchanged
Full mode Semantics label combines state and date (e.g., 'Certificate status: expiring soon, expires 15 June 2025') for screen reader users
Expiry date string is passed as a nullable String? parameter — when null in full mode, the date portion is omitted gracefully
Icon uses the same foreground color token as the label — no separate color parameter needed
Full mode layout uses a Row with icon + label + date, does not overflow at system text scale 1.5
All color logic is shared with compact mode — no duplicated token lookup code
Widget remains stateless and const-constructible
Full mode is visually distinct from compact mode and appropriate for a detail card context (larger visual weight)

Technical Requirements

frameworks
Flutter
Dart
data models
CertificateExpiryStatus (enum)
CertificateExpiryTokens
ExpiryDisplayMode (enum: compact, full)
performance requirements
No additional performance overhead vs compact mode — same const token lookup
ui components
ExpiryStatusIndicator (extended)
Icon (Flutter built-in)
Row layout for full mode

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Introduce ExpiryDisplayMode enum (compact, full) in the same file or adjacent enums file. Refactor the existing build() method to delegate to _buildCompact() and _buildFull() private methods — this avoids a large conditional block and keeps each rendering path readable. Extract the shared color token selection into a private _tokensForStatus() method that both paths call. For the icon mapping, use a static const Map to keep it declarative.

The expiryDateLabel parameter should be a String? — the caller is responsible for formatting the date (do not accept a DateTime and format inside the widget, as this creates l10n complexity). In full mode, wrap the Row in a Semantics node with excludeSemantics: true on child text/icon nodes so the combined label is read once, not fragmented by the screen reader.

Testing Requirements

Extend test/widgets/expiry_status_indicator_test.dart with a new group for full mode. Additional test cases: (1) full mode renders icon widget for each state, (2) full mode renders the expiry date string when provided, (3) full mode omits date gracefully when expiryDateLabel is null, (4) full mode Semantics label includes the date string when provided, (5) full mode does not overflow at textScaleFactor 1.5 in a 320px-wide viewport, (6) switching displayMode from compact to full changes widget tree structure (verify with find.byType). Existing compact mode tests must still pass without modification.

Epic Risks (2)
medium impact medium prob technical

The persistent banner must remain visible across app sessions and only disappear when a specific backend condition is met (renewal or coordinator acknowledgement). If the BLoC state is not properly sourced from the notification record repository on every app launch, the banner may disappear prematurely or fail to reappear after a session restart.

Mitigation & Contingency

Mitigation: Drive the banner's visibility exclusively from a Supabase real-time subscription on the notification records table filtered by mentor_id and acknowledged_at IS NULL. Never persist banner visibility state locally. Write an integration test that restarts the BLoC and verifies the banner reappears from the database source.

Contingency: If real-time subscriptions introduce latency or connection reliability issues in offline-first scenarios, add a local cache flag that is only cleared when the repository confirms the acknowledgement write succeeded, with a cache TTL of 24 hours as a fallback.

high impact low prob security

The notification detail view must conditionally render coordinator-specific actions based on the authenticated user's role. Incorrect role resolution could expose the 'Acknowledge Lapse' action to peer mentors or hide it from coordinators, breaking the workflow and potentially allowing unauthorised state changes.

Mitigation & Contingency

Mitigation: Source the role check from the existing role_state_manager BLoC that is already authenticated against Supabase role claims. Do not rely on a local flag. The coordinator acknowledgement service backend also validates role server-side, providing defence in depth. Add widget tests that render the detail view with mentor and coordinator role fixtures and assert the presence or absence of coordinator actions.

Contingency: If a role resolution bug is found in production, immediately disable the acknowledge action via a feature flag and patch the role check in a hotfix release. The server-side validation in the coordinator acknowledgement service ensures no actual state change can occur even if the button is incorrectly rendered.