medium priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

Banner is visible only when the BLoC state is WrappedSummaryOffline — it must not appear in any other state
Banner displays the lastUpdated timestamp from the WrappedSummaryOffline state payload in a human-readable relative format (e.g. 'Last updated 2 hours ago')
Banner has a dismiss (close) action that hides it for the current session without dispatching any BLoC event
Banner slides in from the top with an AnimatedContainer or SlideTransition and does not cause layout shift that moves the PageView or progress indicator
Banner uses the design token warning color (e.g. AppColors.warningBackground, AppColors.warningText) — no hardcoded hex values
After dismissal, the rest of the screen continues to show stale data from the offline cache without visual glitches
Banner is accessible: it has a semantic role of 'alert' and screen readers announce its content when it appears
Banner text is fully localizable — no hardcoded English strings; uses the app's l10n system

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
data models
WrappedSummaryOffline
WrappedSummaryState
performance requirements
Banner animation must complete in under 300ms to avoid distracting from slide content
security requirements
lastUpdated timestamp must not expose internal server infrastructure details in its formatted display string
ui components
AnimatedContainer or AnimatedSlide
Semantics widget with liveRegion: true
design token color references
dismiss IconButton

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Manage the dismissed flag in local widget state (bool _bannerDismissed) — this is intentionally ephemeral and must not pollute the BLoC. Use BlocBuilder scoped only to the banner widget to avoid rebuilding the full screen on state changes. Place the banner inside a Stack above the PageView using Positioned(top: 0) so it overlays without shifting layout. Use AnimatedSwitcher or AnimatedSlide with a vertical offset to animate in/out.

For the timestamp format, use the timeago package if already present in the project, otherwise implement a simple Duration-based relative formatter. Ensure the design token reference exists in the token system before using it — if AppColors.warningBackground is not defined, add it to the design tokens file as part of this task.

Testing Requirements

Widget tests: (1) emit WrappedSummaryOffline state and verify the banner widget is present in the tree, (2) emit WrappedSummaryLoaded and verify the banner is not present, (3) find dismiss button and tap it, then verify banner is no longer visible without a new BLoC emission, (4) verify the lastUpdated text contains a formatted timestamp derived from the state payload. Accessibility test: verify Semantics node with liveRegion exists when banner is shown.

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.