Implement live region announcement on screen entry
epic-no-access-screen-ui-task-007 — Add a live region that fires when the no-access screen is first rendered so screen-reader users receive the access denial message without needing to manually navigate. Use SemanticsService.announce or a post-frame callback with an announceForAccessibility call. The announcement must include the denial reason text.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Convert the widget to a StatefulWidget (or ConsumerStatefulWidget if using Riverpod). In initState, register a single addPostFrameCallback that calls SemanticsService.announce(context.l10n.noAccessScreenDenialMessage, TextDirection.ltr). Use a boolean flag _hasAnnounced to guard against re-announcement on subsequent builds — set it to true immediately after scheduling the callback. Do not call SemanticsService.announce inside the build method.
Note that SemanticsService.announce is a best-effort API — it will be silently ignored if no accessibility service is active, which is the desired degradation behaviour. If the project already has a utility function for live region announcements (check utils/ or accessibility_utils.dart), use that instead of calling SemanticsService directly.
Testing Requirements
Write widget tests using flutter_test with semanticsEnabled: true. Test 1: pump the widget and use tester.pump() to flush post-frame callbacks, then verify SemanticsService.announce was called with the expected denial message string (mock SemanticsService or capture via test binding). Test 2: rebuild the widget (setState or provider update) and assert the announcement is NOT fired a second time. Test 3: verify the announcement text matches the localised denial message for the active locale.
Test 4: assert no exception is thrown when semantics is disabled in the test environment. Perform a manual device test with VoiceOver on iOS and document that the announcement fires on screen entry.
Flutter's live region (SemanticsProperties.liveRegion) announcement may be delayed or swallowed by the OS accessibility engine if the Semantics tree is not fully built when the screen mounts, causing screen-reader users to miss the denial announcement.
Mitigation & Contingency
Mitigation: Trigger the live region announcement from a post-frame callback (WidgetsBinding.addPostFrameCallback) to ensure the Semantics tree is committed before the announcement fires. Test on both VoiceOver (iOS) and TalkBack (Android) physical devices.
Contingency: If live region timing is unreliable, fall back to using SemanticsService.announce() directly in the initState post-frame callback, which provides more deterministic announcement timing.
The organisation logo may fail to load (network error, missing asset) leaving a broken image in an otherwise functional screen, degrading the professional appearance and potentially confusing users.
Mitigation & Contingency
Mitigation: Wrap the logo widget in an error builder that renders a styled fallback (organisation name text or a generic icon) when the logo asset or network image fails to load.
Contingency: If logo loading is persistently unreliable across organisations, remove the logo from the no-access screen entirely in favour of a text-only header using the organisation's display name from the design token system.