high priority medium complexity infrastructure pending fullstack developer Tier 2

Acceptance Criteria

SummaryShareService.captureWidgetAsImage(GlobalKey repaintBoundaryKey) returns Future<Uint8List> of a valid PNG byte array
Capture is only triggered after receiving an animation-complete signal (AnimationController.status == AnimationStatus.completed); calling capture before completion throws a SummaryCaptureNotReadyException
Captured PNG has pixel dimensions equal to logical size * device pixel ratio (correct high-DPI resolution)
Capture succeeds when the widget is partially off-screen (e.g., inside a scrollable) by using RenderRepaintBoundary.toImage with correct pixelRatio
If the RenderObject is not yet laid out (RenderRepaintBoundary.debugNeedsPaint == true), the method awaits one post-frame callback before retrying, with a maximum of 3 retries before throwing SummaryCaptureException
Returned Uint8List is confirmed to be a valid PNG by checking the PNG magic bytes (0x89 0x50 0x4E 0x47) before returning
Method does not leak memory — image object is disposed after encoding to bytes
Capture does not cause visible UI jank (executes entirely in the raster thread pipeline; no main-thread blocking operations)

Technical Requirements

frameworks
Flutter
apis
RenderRepaintBoundary (dart:ui)
ImageByteFormat.png (dart:ui)
performance requirements
Capture and PNG encoding must complete within 500ms on a mid-range device at 3x pixel ratio
PNG output size must not exceed 5MB for a standard full-screen Wrapped card
security requirements
Captured image may contain aggregate personal stats — treat as sensitive in subsequent share/save operations
Image byte array must not be written to a world-readable file path on Android
ui components
RepaintBoundary (wrapping the shareable Wrapped summary widget)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use repaintBoundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary, then call .toImage(pixelRatio: window.devicePixelRatio). After toImage(), call image.toByteData(format: ImageByteFormat.png) and then image.dispose() immediately. Wrap the entire flow in a try/catch/finally to ensure dispose is always called. For the post-frame retry, use WidgetsBinding.instance.addPostFrameCallback().

Store the AnimationController status externally (e.g., a Completer that the animation layer resolves on completion) and have captureWidgetAsImage await that completer before proceeding. Do not use any third-party screenshot packages — Flutter's built-in RenderRepaintBoundary is sufficient and avoids Play Store policy issues.

Testing Requirements

Widget tests using flutter_test: (1) pump a RepaintBoundary-wrapped widget, trigger capture, and assert returned Uint8List starts with PNG magic bytes; (2) call capture before animation complete and assert SummaryCaptureNotReadyException; (3) simulate debugNeedsPaint=true and assert retry logic fires up to 3 times then throws on continued failure; (4) assert pixel dimensions match logical size * MediaQuery.devicePixelRatio. Manual QA on a 3x iPhone and a 2x Android device to verify sharpness of captured image.

Component
Summary Share Service
service medium
Epic Risks (3)
high impact medium prob integration

Activity records may contain duplicate entries (as evidenced by the duplicate-detection feature dependency) or proxy-registered activities that should be attributed differently. Including duplicates or mis-attributed records would produce inflated stats, undermining trust in the summary.

Mitigation & Contingency

Mitigation: Implement the aggregation query to join against the deduplication-reviewed-flag on activity records and filter out unresolved duplicates. Coordinate with the duplicate-detection feature team to confirm the authoritative flag field before implementing the RPC. Include a data-quality warning in the summary when unresolved duplicates are detected.

Contingency: If deduplication state is unreliable at release time, add a prominent disclaimer in the summary UI noting that figures reflect all registered activities and may include duplicates pending review. Track a follow-up task to re-aggregate after deduplication runs.

medium impact high prob scope

Each organisation wants to define their own milestone thresholds (e.g., NHF's counting model differs from HLF's certification model). Implementing configurable thresholds may expand scope significantly if the configuration UI is expected in this epic.

Mitigation & Contingency

Mitigation: Scope this epic strictly to the evaluation engine and a hardcoded default threshold set. Define the MilestoneDefinition interface with an organisation_id discriminator so per-org configs can be loaded from the database in a later sprint. Build the admin configuration UI as a separate follow-on task outside this epic.

Contingency: If stakeholders require per-org milestone configuration before launch, deliver a JSON-based configuration file per org as an interim solution, loaded from Supabase storage, until a full admin UI is built.

medium impact medium prob technical

Android 13+ restricts access to media collections and requires READ_MEDIA_IMAGES permission for gallery saves, while older Android versions use WRITE_EXTERNAL_STORAGE. Handling both permission models correctly across the device matrix is error-prone.

Mitigation & Contingency

Mitigation: Use the permission_handler Flutter package with version-aware permission requests abstracted behind the summary-share-service interface. Write platform-specific unit tests for both Android API levels in the test harness. Test on a minimum of three Android versions (API 29, 32, 34) in CI.

Contingency: If gallery save is broken on specific Android versions at launch, disable the 'Save to gallery' option on affected API levels and surface only clipboard and system share sheet, which require no media permissions.