high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

Every successful LoadSummary or ChangePeriod response is persisted to the offline cache before the Loaded state is emitted
Cache is keyed by `userId + period` so per-user and per-period data are stored independently
On network failure, the BLoC queries the cache for the requested period and emits WrappedSummaryOffline if an entry is found
WrappedSummaryOffline state contains: stale `slides` list, `period`, `milestones`, and a `cachedAt` DateTime
If no cache entry exists for the requested period on failure, WrappedSummaryError is emitted with code `NO_DATA`
Cache entries older than 30 days are treated as expired and not served — WrappedSummaryError is emitted instead
Cache write failure (disk full, permission error) is logged but does not prevent the Loaded state from being emitted
Cache interface is injected into WrappedSummaryBloc via constructor — BLoC does not instantiate the cache directly
Calling `close()` on the BLoC does not corrupt a concurrent in-progress cache write
The offline cache stores the full slides payload including milestone annotations, not raw aggregation data

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
flutter_secure_storage or Hive (encrypted)
apis
Summary offline cache service (internal abstraction over local storage)
data models
CachedSummary
SummaryPeriod
WrappedSummarySlide
WrappedMilestone
performance requirements
Cache read on app launch must complete within 200ms to avoid a blank loading screen
Cache write must be non-blocking with respect to state emission — write and emit in parallel or write then emit without awaiting write result for UI path
security requirements
Cache storage must be encrypted at rest (flutter_secure_storage or Hive with AES encryption key stored in the secure enclave)
Cache must be cleared on user logout to prevent cross-account data leakage
Cache keys must incorporate a user-specific namespace so shared-device scenarios are safe
ui components
Offline banner / stale data indicator on WrappedSummary screen

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define a `SummaryCache` abstract class with `read(userId, period)` and `write(userId, period, CachedSummary)` methods. Implement with Hive (encrypted box) or flutter_secure_storage for JSON payloads. In the `_fetchAndEmit` helper: after a successful network response, call `unawaited(_cache.write(...))` so the write does not delay UI. On catch, call `final cached = await _cache.read(userId, period)` and branch on null vs expired.

Define `_isExpired(CachedSummary c) => DateTime.now().difference(c.cachedAt).inDays > 30`. Clear the entire user cache namespace on logout — hook into an auth state listener or expose a `clearUserCache(userId)` method. For testing, pass a `FakeSummaryCache` that records calls.

Testing Requirements

Unit tests with bloc_test and mock cache service. Test cases: (1) emits Loaded and verifies cache.write was called with correct key and payload, (2) emits Offline with correct cachedAt when network fails and cache hit, (3) emits Error(NO_DATA) when network fails and cache miss, (4) emits Error when cache entry is older than 30 days, (5) cache write failure does not change Loaded emission, (6) cache is keyed separately for different periods. Integration test: write to real Hive box, simulate offline, verify WrappedSummaryOffline contains identical slides to what was written.

Component
Wrapped Summary BLoC
service medium
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.