critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

Immediately after successful decryption, SemanticsService.announce('Field [label] is now visible', TextDirection.ltr) is called where [label] is the widget's label prop
ReadReceiptService.recordReadReceipt is called with the correct contactId and fieldName after decryption succeeds
The read-receipt call is fire-and-forget: it is not awaited and does not delay the UI transition to revealed state
ReadReceiptFailure from the service is silently ignored (logged to a debug channel only) and does not affect the user-visible state
When the field is re-masked (by unlock icon tap or timeout), SemanticsService.announce('Field [label] is now hidden', TextDirection.ltr) is called
Neither the SemanticsService call nor the ReadReceiptService call blocks the frame in which the revealed state is first rendered
Widget tests verify that SemanticsService.announce is called with the correct string on reveal
The contactId required by the read-receipt service is passed as a required parameter from the parent screen, not derived inside the widget

Technical Requirements

frameworks
Flutter
Dart
apis
SemanticsService.announce (dart:ui / flutter/semantics)
ReadReceiptService.recordReadReceipt
data models
ReadReceiptResult
performance requirements
SemanticsService.announce must be called within the same frame as the setState that reveals the field
ReadReceiptService.recordReadReceipt must be invoked via unawaited() to guarantee zero frame-blocking
security requirements
The announced text must NEVER include the plaintext value — only the field label name

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Add contactId (String) as a required constructor parameter to EncryptedFieldDisplay — the parent screen (ContactDetailScreen) has the contactId in scope and must pass it down. In the _onDecryptionSuccess() private method (called after compute() resolves): (1) call setState to set _isRevealed = true and _plaintextValue; (2) call SemanticsService.announce immediately after setState; (3) call unawaited(widget.readReceiptService.recordReadReceipt(contactId: widget.contactId, fieldName: widget.label)). The unawaited() call from package:meta or dart:async ensures the linter does not warn about the discarded Future. For the re-mask announcement, create a _onRemask() method called by both the unlock icon tap and the timeout logic (task-010): it sets _isRevealed = false, clears _plaintextValue, and calls SemanticsService.announce('Field ${widget.label} is now hidden', TextDirection.ltr).

Inject ReadReceiptService via the constructor so tests can provide a mock.

Testing Requirements

Widget tests (flutter_test): (1) mock ReadReceiptService and verify recordReadReceipt is called exactly once with correct contactId and fieldName after confirm+decrypt flow; (2) verify ReadReceiptService is NOT called if the user dismisses the dialog; (3) use SemanticsController or override SemanticsService to capture announced strings, then assert the reveal announcement text equals 'Field [label] is now visible'; (4) assert the re-mask announcement text equals 'Field [label] is now hidden' when unlock icon is tapped; (5) mock ReadReceiptService to return ReadReceiptFailure and assert the widget remains in revealed state (failure is silent). Use fake_async to verify the receipt call is non-blocking.

Component
Encrypted Field Display Widget
ui high
Epic Risks (3)
medium impact medium prob scope

The encrypted-field-display confirmation dialog adds interaction steps that may frustrate coordinators who access sensitive fields frequently, leading to requests to bypass the flow or skip read-receipt logging, which would violate Blindeforbundet's compliance requirements.

Mitigation & Contingency

Mitigation: Design the confirmation dialog to be as minimal as possible (one clear sentence, single confirm action) and ensure it does not reappear for the same field within a single screen session. Validate the UX with Blindeforbundet coordinators during the TestFlight pilot before finalising.

Contingency: If coordinators raise strong objections, escalate to Blindeforbundet's data protection officer to determine whether a lighter confirmation pattern (e.g., biometric confirmation instead of dialog) satisfies their compliance obligation.

medium impact medium prob technical

The activity-history-list infinite scroll requires paginated Supabase queries. Contacts with hundreds of activities (e.g., an HLF peer mentor with 380 annual registrations) could cause slow page loads or memory pressure on older devices if pagination boundaries are set too large.

Mitigation & Contingency

Mitigation: Use a page size of 20 records with cursor-based pagination. Implement list item recycling using Flutter's ListView.builder. Benchmark memory usage with 400+ item simulation on a low-end test device before TestFlight release.

Contingency: If performance degrades on older devices, reduce page size to 10 and add a time-window filter (last 30 days, last 6 months, all) so the default view loads a manageable record count for most coordinators.

low impact high prob scope

The cognitive load rule engine (from the Cognitive Accessibility feature) mandates no more than 7 fields per screen section. If a contact model has more than 7 editable fields, the edit-contact-screen layout must be split into sections, adding complexity not accounted for in the initial scope.

Mitigation & Contingency

Mitigation: Audit the full contact field list from all four organisations before implementation. Group fields into logical sections (personal info, contact details, affiliation) so no single section exceeds 7 fields. Use the cognitive-load-rule-engine component if it is already delivered by the Cognitive Accessibility feature.

Contingency: If the rule-engine component is not yet available, implement a simple manual section layout with accordion-style expansion for less-frequently edited fields to stay within the 7-field guideline without blocking delivery.