high priority medium complexity infrastructure pending backend specialist Tier 5

Acceptance Criteria

A feature flag key named wrapped_summary_enabled (or equivalent per existing naming convention) is defined and retrievable via the feature-flag-provider Riverpod provider
The flag is scoped per organisation_id — enabling it for NHF does not affect HLF or other organisations, and the provider resolves the correct value based on the currently authenticated user's organisation
The home screen Wrapped card entry point is completely hidden (not merely greyed out) when the flag is false for the current organisation
Push notification deep-link routing for wrapped summary is blocked and redirected to home screen when the flag is false
All code paths that render WrappedSummaryScreen are wrapped in a feature-gate-widget or programmatic flag check — no direct navigation to the screen is possible when the flag is off
Toggling the flag in the backend (Supabase) causes the entry points to appear or disappear on next app launch or session refresh without requiring an app update
Flag resolution is synchronous from cache on subsequent app launches — no UI flash of the card before the flag resolves
The implementation uses the existing feature-gate-widget and feature-flag-provider patterns; no new flag infrastructure is introduced

Technical Requirements

frameworks
Riverpod (feature-flag-provider — existing provider, extend with wrapped_summary_enabled key)
Flutter (feature-gate-widget — existing widget for conditional rendering)
BLoC (no changes required; gate is at routing and UI layer)
apis
Supabase PostgreSQL — feature flags stored in a feature_flags or organisation_settings table; query scoped by organisation_id
Supabase Auth — organisation_id extracted from authenticated user's JWT claims for flag scoping
data models
annual_summary (indirectly — gated feature reads this entity)
assignment (organisation_id used for flag scoping context)
performance requirements
Feature flag value must be available from local cache within 50 ms on app launch to prevent UI flicker
Remote flag refresh must not block the UI thread — use async loading with a cached fallback (default: false/disabled)
security requirements
Feature flag values are fetched using the authenticated user's JWT — no organisation can query another organisation's flag values due to Supabase RLS
Default value when flag is absent or fetch fails must be false (feature disabled) — fail-closed behaviour
Flag state must not be persisted in a way that allows a user to manually override it (e.g. not in plain SharedPreferences)
ui components
feature-gate-widget (existing) — wraps home screen Wrapped card
Home screen Wrapped card (hidden when flag is false, shown when true)

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Extend the existing feature-flag-provider with a new enum or string constant for the wrapped_summary_enabled key. The provider should read from a local cache (Hive or flutter_secure_storage if sensitive, or in-memory Riverpod state) populated on login and refreshed on session resume. For the home screen card, wrap it with the existing FeatureGateWidget passing the wrapped_summary_enabled flag key — this avoids ad-hoc if-checks scattered across the widget tree. For the deep-link handler, call ref.read(featureFlagProvider).isEnabled(FeatureFlag.wrappedSummary) synchronously from the cached value; since deep-links can fire before async refreshes complete, the cached value from the last session is acceptable with a conservative default of false.

Document the flag key name and expected Supabase schema (organisation_id, flag_key, is_enabled) in the feature-flag-provider's existing documentation or README so backend engineers can configure per-org rollout correctly.

Testing Requirements

Write unit tests for the feature-flag-provider covering: flag true for org A returns true, flag false for org A returns false, flag true for org A does not affect org B (isolation), missing flag key returns false (default). Write widget tests for the home screen asserting that the Wrapped card widget is absent from the tree when the flag provider returns false, and present when it returns true — use Riverpod's ProviderScope overrides to inject mock flag states. Write widget tests for the deep-link handler asserting that navigation to WrappedSummaryScreen is blocked when the flag is false. All tests must run without network access using mocked Supabase responses.

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.