critical priority high complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

PageView renders the correct slide widget type (StatCard, MilestoneBadge, or ActivityTypeBreakdown) for each index in WrappedSummaryLoaded.slides
Swiping the PageView dispatches NavigateSlide(direction: forward/backward) to the BLoC — PageView must not manage its own independent index state
Segmented progress indicator has exactly N segments where N equals the number of slides in the loaded state
The active segment is visually distinct (filled, brighter, or wider) and updates immediately when the BLoC emits a new currentSlideIndex
WrappedAnimationController triggers the entrance animation for the incoming slide and completes before user interaction is re-enabled
All slide transitions are smooth at 60 fps on a mid-range device (no jank caused by rebuilding the entire tree)
Loading state renders a shimmer or branded spinner instead of an empty PageView
Error state renders an inline error card with a retry button that dispatches LoadSummary
Swipe gesture is disabled while the BLoC is in WrappedSummaryLoading or WrappedSummarySharing state
Progress indicator segments meet WCAG 2.2 AA contrast ratio (4.5:1) between active and inactive states

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
flutter_test
data models
WrappedSummaryState
WrappedSummaryLoaded
SlideModel
StatCardData
MilestoneBadgeData
ActivityTypeBreakdownData
performance requirements
Use PageView.builder (not PageView) to avoid building off-screen slide widgets eagerly
Animation controller must be disposed in the widget's dispose() method to prevent memory leaks
Limit BlocBuilder rebuild scope — wrap only the PageView controller sync and progress indicator, not the entire screen
ui components
PageView.builder
PageController
WrappedAnimationController
segmented progress Row
StatCard widget
MilestoneBadge widget
ActivityTypeBreakdown widget
shimmer loading placeholder

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Sync the PageController with the BLoC's currentSlideIndex using a BlocListener — do NOT use the PageView's onPageChanged as the source of truth (BLoC state is the source of truth). Pattern: BlocListener listens for index changes and calls pageController.animateToPage(). The PageView's onPageChanged callback dispatches NavigateSlide to the BLoC so the BLoC stays in sync. Use IgnorePointer wrapping the PageView when the BLoC is in a loading or sharing state to prevent mid-transition user input.

For the WrappedAnimationController, create one AnimationController per slide and run it in didUpdateWidget when the active index changes. Use a factory or map keyed by slideIndex to avoid recreating controllers unnecessarily. The segmented progress indicator is a Row of Expanded containers with AnimatedContainer for smooth width/colour transitions.

Testing Requirements

Widget tests: (1) seed BLoC with WrappedSummaryLoaded containing 3 slides and verify PageView has 3 children, (2) simulate swipe right and verify NavigateSlide(forward) was dispatched, (3) emit currentSlideIndex=1 from BLoC and verify progress segment 1 is active, (4) seed with WrappedSummaryLoading and verify shimmer is shown and PageView physics are NeverScrollableScrollPhysics. Golden tests (optional): capture the progress indicator at indices 0, 1, and 2 for a 3-slide set. Accessibility: confirm each slide widget has semantic labels readable by screen readers.

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.