medium priority low complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

SummaryPeriodSelector is not rendered when WrappedSummaryLoaded.availablePeriods has fewer than 2 items
SummaryPeriodSelector is rendered and visible when availablePeriods contains 2 or more items
Tapping a period chip/button dispatches ChangePeriod(period: selectedPeriod) to the BLoC
The currently active period is visually highlighted using the design token active/selected color
While the BLoC is in WrappedSummaryLoading state (triggered by ChangePeriod), the selector shows an inline loading indicator (e.g. a spinner replacing the selected chip's label) and is non-interactive
After the BLoC returns WrappedSummaryLoaded with the new period, the selector returns to interactive state with the new active period highlighted
Period labels display the year (e.g. '2024', '2023') derived from the SummaryPeriod.label field — not raw IDs
Selector is keyboard and screen-reader accessible: each period option has a semantic label including whether it is selected
Widget does not maintain its own selected period state — the active period is always derived from WrappedSummaryLoaded.currentPeriod

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
data models
SummaryPeriod
WrappedSummaryLoaded
ChangePeriod
performance requirements
Selector widget must be wrapped in BlocBuilder scoped to availablePeriods and currentPeriod only to minimise unnecessary rebuilds
ui components
SummaryPeriodSelector
Wrap or Row of chip widgets
Semantics widgets
inline CircularProgressIndicator

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

The SummaryPeriodSelector widget should be a stateless widget that receives (List periods, SummaryPeriod currentPeriod, bool isLoading, ValueChanged onPeriodChanged) as constructor parameters. This makes it trivially testable in isolation. The parent BlocBuilder in WrappedSummaryScreen reads the BLoC state and passes the appropriate props. Use AbsorbPointer(absorbing: isLoading) to disable interaction rather than conditionally rebuilding the entire chip list.

Position the selector below the progress indicator and above the slide PageView — use a Column with mainAxisSize: MainAxisSize.min. For the chip UI, prefer a horizontally scrollable Row with FilterChip or custom chip widgets styled with design tokens over a DropdownButton, as the visual affordance better suits a full-screen immersive flow.

Testing Requirements

Widget tests: (1) emit WrappedSummaryLoaded with 1 period and verify SummaryPeriodSelector is absent from the tree, (2) emit WrappedSummaryLoaded with 3 periods and verify 3 period chips are present, (3) tap a non-active period chip and verify ChangePeriod was dispatched with the correct period, (4) emit WrappedSummaryLoading after ChangePeriod and verify chips are non-interactive, (5) verify currentPeriod chip has a selected semantic state. All tests use a mocked BLoC stream — no real Supabase calls.

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.