high priority medium complexity frontend pending frontend specialist Tier 8

Acceptance Criteria

VoiceOver (iOS) and TalkBack (Android) announce each slide's primary content (headline statistic, supporting label, call-to-action text) in a logical reading order that matches the visual layout
Focus traversal order on each slide follows: slide content area → progress indicator → action buttons (share, close) — no focus traps or skipped elements
All custom animated widgets (counters, chart animations, particle effects) have Semantics wrappers that provide a static accessible label so screen readers announce the final value rather than intermediate animation states
When a screen reader is active, the summary-accessibility-provider suppresses redundant animation-intermediate semantics announcements — no spurious intermediate number announcements during count-up animations
The progress indicator communicates current slide position as an accessible value (e.g. 'Slide 3 of 7') via Semantics.value or equivalent
Share button has a descriptive Semantics.label (e.g. 'Share your annual impact summary') rather than relying on icon inference
Close button has a descriptive Semantics.label (e.g. 'Close annual impact summary')
All colour combinations on slides meet WCAG 2.2 AA contrast ratio minimums: 4.5:1 for normal text, 3:1 for large text and UI components — verified using a contrast checking tool
Text scales correctly when system font size is set to maximum accessibility size (largest text setting) without layout overflow or clipping
All interactive elements have a minimum touch target size of 44x44 dp as per WCAG 2.5.8 and Flutter accessibility guidelines
Reduced-motion system preference is detected via the summary-accessibility-provider and all non-essential animations are disabled or reduced to simple fades when active
Audit findings are documented in a brief accessibility report (markdown or inline code comments) listing each issue found, its WCAG criterion, and the fix applied

Technical Requirements

frameworks
Flutter Semantics API (Semantics widget, SemanticsProperties, MergeSemantics, ExcludeSemantics)
Riverpod (summary-accessibility-provider — consumes MediaQuery.accessibleNavigation and MediaQuery.disableAnimations)
flutter_test (SemanticsController for automated semantics assertions in widget tests)
apis
MediaQuery (disableAnimations, accessibleNavigation, textScaleFactor — for reduced-motion and font-scale detection)
flutter_local_auth is not involved in this task
data models
accessibility_preferences (user_id, font_scale_factor, contrast_mode, haptic_feedback_enabled — read to apply user-specific accessibility overrides on top of system defaults)
performance requirements
Semantics tree construction must not introduce perceptible rendering overhead — ExcludeSemantics must be used on decorative elements to keep the semantics tree lean
Screen reader focus movement between slide elements must complete within one TalkBack/VoiceOver gesture cycle (under 500 ms response)
security requirements
No accessibility-related change should expose additional user PII through semantics labels — labels must use aggregated statistics only (counts, hours) never names or contact details
ui components
Semantics wrapper on each slide's statistic widget (animated counter, chart, badge)
Semantics wrapper on progress indicator with current/total slide values
Semantics label on share button
Semantics label on close button
ExcludeSemantics on all purely decorative background animations and particle effects

Execution Context

Execution Tier
Tier 8

Tier 8 - 48 tasks

Can start after Tier 7 completes

Implementation Notes

Start the audit by enabling accessibility on a physical device and navigating through the entire flow with only gesture-based navigation (no visual reference) to identify focus order issues before writing any code. For animated count-up widgets, the most reliable pattern is to wrap the entire AnimatedCounter widget in a Semantics widget with label set to the final value and liveRegion: true — this causes screen readers to announce only when the animation completes. Use the summary-accessibility-provider (to be defined in task epic-annual-impact-summary-ui-components-task-001) to expose a bool reduceMotion and bool isScreenReaderActive flag; conditionally set animation durations to Duration.zero when reduceMotion is true. For the progress indicator, use Semantics(value: 'Slide $current of $total', child: ...) rather than trying to make the visual dots individually focusable — this is cleaner and matches native carousel accessibility patterns.

Check the design token color palette against WCAG contrast requirements for each slide theme variant (some slides may use inverted or coloured backgrounds). Pay special attention to the Blindeforbundet user base requirement for full VoiceOver support — this feature's gamification intent must be fully accessible, not merely compliant.

Testing Requirements

Write widget tests using Flutter's SemanticsController (tester.semantics) to assert that each slide's semantics tree contains the expected labels, values, and hints. Test that ExcludeSemantics correctly removes decorative elements from the semantics tree. Test that the progress indicator's Semantics.value reflects the correct 'Slide N of M' string for at least three slide positions. Test the reduced-motion path by injecting a MediaQueryData with disableAnimations: true via a ProviderScope override and asserting that animation durations are zero or transitions are instant.

Run manual accessibility testing on physical devices: VoiceOver on iPhone (TestFlight) and TalkBack on Android — document results. Use the Flutter accessibility scanner (google_accessibility_scanner if available, or manual Xcode Accessibility Inspector) to verify no WCAG violations remain.

Component
Wrapped Summary Screen
ui high
Epic Risks (3)
medium impact medium prob technical

If the device transitions between online and offline states while the user is mid-session in the wrapped screen, the BLoC may emit conflicting state transitions (loaded → error → offline) that cause visual flickering or an inconsistent UI state such as showing the offline banner over an already-loaded summary.

Mitigation & Contingency

Mitigation: Implement a connectivity stream listener in the BLoC that only triggers a state re-evaluation when transitioning from online to offline, not on every connectivity event. Once a summary is in the Loaded state, the BLoC should not transition to error/offline unless the user explicitly requests a refresh. Store the last-loaded data in BLoC state so it survives connectivity changes.

Contingency: If state flickering is observed in testing, add a minimum 3-second debounce on connectivity state changes before the BLoC reacts, and display a non-blocking top banner rather than replacing the entire screen state.

high impact medium prob integration

The push notification deep-link to the wrapped-summary-screen must work correctly whether the app is in the foreground, background, or terminated state. Handling all three app launch states on both iOS and Android is a common source of edge-case bugs, particularly when authentication state must be restored before the deep link can be resolved.

Mitigation & Contingency

Mitigation: Implement deep-link handling through the existing notification-deep-link-handler component which already manages app-state-aware routing. Define the wrapped-summary route in the navigation config early in the epic so the router is ready before notification dispatch is wired. Test all three app states (foreground, background, terminated) explicitly in the QA checklist.

Contingency: If terminated-state deep-linking fails on specific platforms, fall back to launching the app to the home screen with an in-app notification banner prompting the user to open their summary, rather than direct deep-link navigation.

high impact low prob technical

The wrapped-summary-screen manages a large number of AnimationController instances (one or more per slide) via the wrapped-animation-controller. If disposal is not triggered correctly when the user exits mid-flow (e.g., via system back gesture or deep-link away), memory leaks will accumulate across session navigation.

Mitigation & Contingency

Mitigation: Implement screen disposal via Flutter's dispose() lifecycle method calling a single wrapped-animation-controller.disposeAll() method that iterates the named controller registry. Write a test that navigates to the screen, starts animations, then navigates away and verifies no active AnimationController listeners remain using Flutter's test binding.

Contingency: If disposal bugs are detected in production via memory profiling, patch by converting all AnimationControllers to use AutomaticKeepAliveClientMixin false and wrap each slide in a widget that disposes its own controller when removed from the widget tree.