high priority medium complexity integration pending integration specialist Tier 5

Acceptance Criteria

Tapping a 'summary-ready' push notification while the app is fully closed (cold-start) launches the app and navigates directly to WrappedSummaryScreen with the correct year parameter extracted from the notification payload
Tapping a 'summary-ready' push notification while the app is backgrounded (warm-start) brings the app to the foreground and navigates to WrappedSummaryScreen without triggering a full app restart
WrappedSummaryScreen dispatches LoadSummary(year: <year_from_payload>) on initState immediately after the widget is mounted, regardless of entry path (deep-link or manual navigation)
The year parameter is parsed from the FCM notification data payload field (e.g. data.year) and validated as a 4-digit integer before dispatching LoadSummary; invalid or missing year falls back to the current calendar year with a logged warning
If the feature flag for the Wrapped feature is disabled for the current organisation, the deep-link handler redirects to the home screen instead of WrappedSummaryScreen and logs the blocked navigation attempt
The existing notification deep-link handler infrastructure is extended (not duplicated) — a new route entry for the wrapped summary deep-link path is registered in the central route map
No duplicate navigation stack entries are created when the deep-link fires; pressing back from WrappedSummaryScreen returns to the home screen regardless of launch scenario
Integration with FCM v1 API is server-side only; the mobile client reads the notification payload via the existing push handler and does not call FCM APIs directly

Technical Requirements

frameworks
Flutter (go_router or existing routing infrastructure for deep-link registration)
BLoC (WrappedSummaryBloc with LoadSummary event)
Riverpod (feature-flag-provider for Wrapped gate check)
firebase_messaging Flutter package (FCM payload parsing)
apis
Firebase Cloud Messaging (FCM) API v1 — notification payload contains data.year and data.type fields
Supabase Edge Functions — push dispatch is server-side; client only receives the payload
data models
annual_summary (peer_mentor_id, year, period_type — used to resolve the correct summary on screen init)
performance requirements
Cold-start deep-link navigation must complete and dispatch LoadSummary within 2 seconds of app process launch on a mid-range device
Warm-start navigation must be imperceptible to the user — under 300 ms from notification tap to WrappedSummaryScreen appearing
security requirements
FCM server key and service account are never bundled in the mobile app binary — all push dispatch happens via server-side Edge Functions only
Notification payloads contain only non-sensitive metadata (year, type) — full summary content is fetched from Supabase after navigation
Deep-link route must validate that the currently authenticated user is the intended recipient before loading summary data; mismatched user sessions must redirect to login
ui components
WrappedSummaryScreen (existing, must accept year parameter via constructor or route argument)
Loading skeleton shown immediately while LoadSummary is in-flight after deep-link entry

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Register a new named route (e.g. /wrapped/:year) in the central go_router configuration. In the notification handler (likely a top-level onMessageOpenedApp callback and getInitialMessage for cold-start), inspect data.type == 'wrapped_summary_ready' and extract data.year before pushing the route. Use go_router's extra or pathParameters to pass the year — avoid storing it in a global singleton to prevent stale-state bugs.

For cold-start, getInitialMessage must be awaited in main() or the root widget's initState before any navigation occurs. Ensure the route guard checks the Wrapped feature flag via the feature-flag-provider Riverpod provider; if disabled, use go_router's redirect to push /home instead. Use the Navigator 2.0 / go_router initialLocation to set WrappedSummaryScreen as the current route without adding home to the back stack beneath it — use go_router's replace semantics (context.go) rather than push semantics (context.push) so that back navigation lands on home.

Testing Requirements

Write widget tests mocking the WrappedSummaryBloc to assert that LoadSummary is dispatched with the correct year when the screen is constructed via deep-link route arguments. Write integration tests (integration_test package) covering cold-start deep-link navigation using a seeded FCM payload — assert correct screen is reached and correct year is loaded. Write unit tests for the deep-link payload parser covering: valid year, missing year (fallback), non-integer year (fallback), feature-flag-disabled scenario (redirect). Achieve 90% branch coverage on the deep-link handler extension.

Verify warm-start scenario manually via TestFlight on physical iOS and Android devices.

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.