Add tappable admin portal link with url_launcher
epic-no-access-screen-ui-task-004 — Implement a tappable admin portal link widget inside the no-access-screen-widget that opens the admin portal URL in the device browser via the url-launcher-util (url_launcher package). The link must meet the 44×44 dp minimum touch target requirement, have a visible underline or button style, and include a semantic label for screen readers.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use the project's existing url-launcher-util wrapper if one exists (check utils/ or shared/ for a UrlLauncherUtil class) rather than calling url_launcher directly, to keep the dependency injectable for testing. The admin portal URL should be stored in AppConstants or read from a Riverpod provider so it can vary per organisation or environment. Implement a simple boolean _isLaunching state variable to prevent double-tap issues. Use LaunchMode.externalApplication.
Apply a link-specific colour token (e.g. AppColors.linkDefault) which should already exist in the design token system. Keep the ARB key something like noAccessScreenAdminPortalLinkLabel.
Testing Requirements
Write widget and unit tests using flutter_test. Test 1: mock the url-launcher-util and assert launchUrl is called with the correct URL when the link is tapped. Test 2: mock canLaunchUrl to return false and assert a user-facing error message is displayed. Test 3: verify the Semantics label is present in the semantics tree.
Test 4: verify the touch target size is at least 44×44 dp by inspecting the RenderBox size in the widget test. Test 5: verify rapid double-tap does not call launchUrl twice (debounce). Use mockito or mocktail to mock the url_launcher dependency.
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.