high priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

'Record Manual Renewal' button navigates to RecordRenewalScreen via GoRouter; if RecordRenewalScreen does not yet exist, navigate to a placeholder screen with the correct route path so the route is registered
'Enrol in Course' button launches an external URL (the HLF course enrolment URL) using url_launcher; if the URL cannot be launched, show a SnackBar with a user-friendly error message using design token error color
Both buttons use the project's AppButton widget with the correct variant (primary for 'Record Manual Renewal', secondary/outlined for 'Enrol in Course' — confirm with design system conventions)
Each button's touch target is at minimum 44x44 logical pixels as required by WCAG 2.2 AA — verified by reading the AppButton widget implementation; if AppButton does not meet this, wrap it in a SizedBox with minWidth/minHeight constraints
Buttons are arranged in a Column (stacked vertically) with design token spacing between them, pinned to the bottom of the screen using a bottomNavigationBar slot or a sticky bottom padding pattern so they remain visible without scrolling
Both buttons have tooltipMessage or semanticsLabel set for screen reader support: e.g., 'Record a manual certification renewal', 'Enrol in an HLF certification course'
Widget test verifies: tapping 'Record Manual Renewal' triggers the correct GoRouter navigation event; tapping 'Enrol in Course' invokes url_launcher with the expected URL
'Record Manual Renewal' button is disabled (greyed out) and shows a tooltip 'Certification is expired — contact your coordinator' when the certification status is expired, preventing invalid renewal recording
Buttons are only shown when CertificationBLoC is in the loaded state — not during loading or error states

Technical Requirements

frameworks
Flutter
GoRouter for internal navigation
url_launcher for external course URL
BLoC (conditional rendering based on CertificationLoadedState)
apis
url_launcher (canLaunchUrl / launchUrl)
HLF course enrolment URL (retrieve from app config or Supabase remote config — do not hardcode in widget)
data models
CertificationLoadedState (status field to determine if renewal button should be disabled)
CertificationStatus enum
performance requirements
Button row widget should be const-constructible where certification data is not needed for layout
url_launcher call must be async with proper await — do not fire-and-forget
security requirements
External URL must be validated against an allowlist of HLF domains before launching to prevent open redirect; use Uri.parse and check host == 'hlf.no' or equivalent
Do not embed API keys or tokens in the course enrolment URL — use a backend-generated short-lived link if authentication is required for course access
ui components
AppButton (primary variant)
AppButton (secondary/outlined variant)
SnackBar (error feedback for url_launcher failure)
Tooltip (for disabled button state)
SafeArea + bottom padding container for sticky button placement

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Retrieve the course enrolment URL from a config/constants file or Supabase remote config — never hardcode it in the widget. Add a CourseEnrolmentConfig class or extend the existing AppConfig with the URL. For the sticky bottom placement, check how other screens in the app handle bottom action bars — use the same pattern for consistency (likely a Padding wrapping a Column at the bottom of a CustomScrollView, or using the Scaffold's bottomNavigationBar if the screen does not use the global bottom nav). The disabled state for the expired button should use AppButton's onPressed: null pattern (if AppButton supports it) or a custom disabled style — check AppButton implementation first.

Use a BlocBuilder or pass the certification status down as a parameter to the action row widget to keep it reactive. The url_launcher domain allowlist check is a one-liner before launchUrl — implement it as a private bool _isAllowedUrl(Uri uri) method.

Testing Requirements

Widget tests: (1) pump CertificationStatusScreen with CertificationLoadedState (status=valid) — assert both buttons are present and enabled; (2) pump with status=expired — assert 'Record Manual Renewal' button is disabled; (3) mock GoRouter and tap 'Record Manual Renewal' — assert correct route is pushed; (4) mock url_launcher and tap 'Enrol in Course' — assert launchUrl is called with the correct URL; (5) mock url_launcher to return false (cannot launch) — assert SnackBar with error message appears. Use mocktail to mock url_launcher. Integration test on a real device (TestFlight build) to confirm url_launcher opens the HLF course page in the device browser.

Component
Certification Status Screen
ui medium
Epic Risks (3)
high impact medium prob technical

Flutter date pickers have historically poor screen reader support (VoiceOver/TalkBack), which is especially critical for this feature given that HLF peer mentors may have hearing impairment and the broader user base includes people with visual impairments. An inaccessible date picker on RecordRenewalScreen could block coordinator workflows entirely.

Mitigation & Contingency

Mitigation: Evaluate and adopt a third-party accessible date picker widget with verified WCAG 2.2 AA support, or build a custom picker using Flutter Semantics wrappers following the pattern established by the accessibility epic. Test all date pickers against VoiceOver on iOS and TalkBack on Android before UI sign-off.

Contingency: If no accessible date picker is available in time, provide a manual text field fallback for date entry (ISO format with clear labelling) alongside the picker, ensuring keyboard and screen reader users are never blocked.

medium impact high prob scope

Course enrolment initiation may redirect the user to the external HLF course portal (deep link or browser), which breaks the in-app flow and may confuse users expecting a seamless experience. The course data structure from Dynamics may also not be available in a machine-readable format in time for the initial release.

Mitigation & Contingency

Mitigation: Agree with HLF on whether enrolment is in-app or via deep link before UI design begins. If course data is not available from Dynamics at launch, design the enrolment prompt as a placeholder CTA that links to the HLF course portal homepage with a clear label indicating the user is leaving the app.

Contingency: Ship the course enrolment prompt as a configurable deep link per org. If Dynamics integration is delayed, the feature flag for the enrolment section can be disabled without affecting the rest of the certification status screen.

medium impact medium prob scope

Coordinators may attempt to record a renewal with an expiry date earlier than the previous certification's expiry (e.g., data entry error), or attempt to back-date a renewal. Without strict validation, the renewal history timeline could become chronologically inconsistent and mislead peer mentors about their coverage.

Mitigation & Contingency

Mitigation: CertificationManagementService validates that new expiry_date > current date and new issue_date >= previous renewal's issue_date before persisting. Surface validation errors as plain-language messages on RecordRenewalScreen using the error_message_registry pattern.

Contingency: If invalid renewal entries are discovered in production (from pre-validation data), provide a coordinator-only correction flow (edit renewal entry) behind an admin feature flag to fix historical records without requiring a full reset.