high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

PauseStatusBanner renders the text 'Paused' (or localized equivalent) as a prominent status label using the design token amber/warning color
The banner displays the ISO 8601 formatted timestamp of when the pause was applied (e.g., '2026-03-30T14:22:00Z'), formatted in a human-readable locale-aware string (e.g., '30 Mar 2026, 14:22')
When a non-null, non-empty reason string is provided, it is displayed below the timestamp
When reason is null or empty, the reason row is absent from the layout (no empty space left)
A 'Reactivate' shortcut button is present and calls the onReactivateTapped() callback when tapped
The 'Reactivate' button meets the 48×48 dp minimum touch target requirement
A dismiss icon button (X) is present and calls the onDismissed() callback when tapped
After onDismissed() is called, the banner is removed from the widget tree (parent controls visibility)
The banner can be re-shown by the parent setting isVisible: true (or equivalent) — it does not maintain internal dismissed state across rebuilds
All text in the banner meets WCAG 2.2 AA contrast ratio ≥4.5:1 against the banner background
Widget uses design tokens exclusively — no hardcoded colors, font sizes, or spacing values
Semantics: banner container has a semanticsLabel summarizing status, timestamp, and reason for screen readers

Technical Requirements

frameworks
Flutter
flutter_test
data models
PeerMentorStatus
PausedAt (DateTime)
PauseReason (String?)
performance requirements
Banner renders in a single frame — no async work inside the widget
No AnimationController unless a slide-in animation is explicitly required by design
security requirements
Reason text must be rendered as plain text (no HTML/rich-text parsing) to prevent injection
Timestamp is formatted client-side from a DateTime object, not from a raw string to prevent format injection
ui components
Container or Card with design token border/background
Row with status icon + 'Paused' label
Text widget for formatted timestamp
Conditional Text widget for reason
AppButton or TextButton for 'Reactivate' action
IconButton for dismiss (X icon)
Semantics wrapper for the whole banner

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Place the widget at lib/features/peer_mentor/widgets/pause_status_banner.dart. Accept pausedAt as a DateTime and format it with intl package's DateFormat('d MMM yyyy, HH:mm') using the device locale. Accept reason as String? and use a conditional expression in build to include or exclude the reason Text.

The banner should be a pure StatelessWidget — dismissibility is controlled by the parent via an isVisible flag or by simply not including the banner in the tree. This makes it trivially re-renderable without internal state. For the Reactivate button, reuse the existing AppButton widget. Keep the dismiss button as a plain IconButton(icon: Icon(Icons.close)).

Wrap the entire banner in a Semantics widget with a constructed label: 'Mentor paused on [formatted date]. Reason: [reason or none]. Tap Reactivate to restore.'. Use ExcludeSemantics on individual child Text widgets if the parent Semantics label is comprehensive enough, to prevent double-reading by screen readers.

Testing Requirements

Write widget tests covering: (1) banner renders status label, timestamp, and reason when all are provided; (2) reason row is absent when reason is null; (3) reason row is absent when reason is empty string; (4) onReactivateTapped callback fires on tap of reactivate button; (5) onDismissed callback fires on tap of dismiss button; (6) timestamp is displayed in human-readable format (not raw ISO string); (7) design token color is applied to the status label (resolve from test theme); (8) Semantics label is non-empty and contains 'paused'. Run meetsGuideline(textContrastGuideline) via ensureSemantics(). Golden snapshot test for the fully-populated state.

Component
Pause Status Banner
ui low
Epic Risks (3)
medium impact medium prob technical

PauseConfirmationDialog must meet WCAG 2.2 AA focus trap requirements: when the dialog opens, focus must move to the first interactive element; when it closes, focus must return to the triggering toggle. Flutter's default showDialog does not always guarantee correct focus restoration on Android, which could trap screen-reader users.

Mitigation & Contingency

Mitigation: Use a custom modal implementation wrapping Flutter's Dialog with explicit FocusScope management and test with TalkBack on Android and VoiceOver on iOS during development, not just in final QA. Reference the accessible-modal-sheet pattern already used elsewhere in the codebase.

Contingency: If full WCAG focus management cannot be achieved within the sprint, ship the feature with a documented accessibility defect and schedule a dedicated accessibility remediation task. Communicate the known gap to the accessibility stakeholder at HLF/NHF.

low impact medium prob technical

If the user taps the PauseReactivateToggle rapidly before the BLoC emits a loading state, multiple PauseMentorEvents could be added to the BLoC stream, resulting in duplicate service calls and inconsistent UI state (toggle flickering between states).

Mitigation & Contingency

Mitigation: Disable the toggle widget immediately on first tap by emitting MentorStatusLoading synchronously before the async service call begins. Use BLoC's event transformer with droppable() to discard subsequent events while a transition is in progress.

Contingency: If BLoC event deduplication is not sufficient, add a debounce at the widget level (300 ms) and a server-side idempotency check in MentorStatusService that no-ops if the requested transition is already in progress.

low impact low prob security

PauseStatusBanner must conditionally render the reactivate shortcut only for users with coordinator permission. If the permission check relies on a stale role state in the Riverpod provider, a peer mentor could briefly see a reactivate action they are not authorised to use, which could cause a confusing permission-denied error if tapped.

Mitigation & Contingency

Mitigation: Source the permission check directly from the role-resolution Riverpod provider (which is kept in sync with Supabase auth) rather than from local component state. Add a widget test asserting the shortcut is absent when the user role is peer_mentor.

Contingency: If the shortcut is accidentally shown to an unauthorised user, the underlying MentorStatusService enforces role validation server-side, so the worst outcome is a visible error message rather than an actual unauthorised state change.