critical priority high complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

Widget renders a masked placeholder (e.g., '••••••••') by default; the encrypted value is never held in widget state until an explicit reveal action
Tapping the widget (or activating via keyboard/screen reader) triggers a warning dialog: 'You are about to view sensitive personal data. This action will be recorded.' with Cancel and Continue buttons
On dialog confirmation, `field-encryption-utils` decrypts the value and the widget transitions to showing the decrypted text with a lock-open icon
On first reveal only, `read-receipt-service.recordReceipt(contactId, fieldIdentifier)` is called; subsequent reveals of the same field in the same session do not re-record
If decryption fails (key unavailable, network error), the widget shows a non-PII error message ('Unable to decrypt field') and does not reveal partial data
The widget streams `read-receipt-service.watchReceiptStatus()` and displays a 'Previously viewed' badge when `ReceiptStatus.read` is returned, even in masked state
Warning dialog is announced to screen readers (VoiceOver/TalkBack) before any sensitive content is spoken; focus is set to the dialog's first element on open
In masked state, the `Semantics` label is: '[Field label], sensitive field, tap to reveal'; in revealed state: '[Field label], revealed, [value]'
Widget meets WCAG 2.2 AA contrast ratio (4.5:1) for both masked placeholder text and revealed text against background
Widget never logs the decrypted value to console, analytics, or crash reporting tools
Widget re-masks automatically if the app is backgrounded (AppLifecycleState.paused) with a revealed field

Technical Requirements

frameworks
Flutter
Riverpod
apis
field-encryption-utils (decrypt)
read-receipt-service (recordReceipt, watchReceiptStatus)
data models
EncryptedFieldDisplayConfig (fieldIdentifier, contactId, encryptedValue, label)
ReceiptStatus
RevealState (enum: masked / revealing / revealed / error)
performance requirements
Decryption must complete within 500ms of dialog confirmation; a loading indicator must be shown during decryption
Widget must not cause frame drops during the masked-to-revealed transition
Re-masking on app background must occur within one frame of `AppLifecycleState.paused`
security requirements
Decrypted value must be stored only in local widget state (`_decryptedValue`) and cleared on re-mask or widget disposal
Widget must never pass the decrypted value to analytics, logging, or error reporting services
Warning dialog must always be shown before reveal — it must not be bypassable programmatically or via testing flags in production builds
Re-masking on background is mandatory to prevent shoulder-surfing when the device is handed over
Screen reader announcements must not read the decrypted value unless the user has explicitly confirmed the warning dialog
ui components
EncryptedFieldDisplayWidget (stateful, self-contained)
SensitiveDataWarningDialog (reusable modal)
MaskedTextDisplay
RevealedTextDisplay
PreviouslyViewedBadge
DecryptionLoadingIndicator

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use a `StatefulWidget` with `WidgetsBindingObserver` to listen for `AppLifecycleState.paused` and call `setState(() => _revealState = RevealState.masked)`. Store `_decryptedValue` as a nullable `String?` and set it to `null` on re-mask. Use a `Semantics` widget with `onTap` override to ensure VoiceOver/TalkBack exposes the tap affordance. The warning dialog must use `showDialog()` with `barrierDismissible: false` so the user must make an explicit choice.

Guard the `recordReceipt` call with a session-level flag (stored in the provider, not the widget) so it fires only once per app session per field identifier. Use `AnimatedSwitcher` for the masked-to-revealed transition to avoid layout jank. The `PreviouslyViewedBadge` should be a small, accessible chip that does not interfere with the tap target of the main widget.

Testing Requirements

Widget tests (flutter_test): (1) verify masked placeholder is shown on initial render; (2) tap widget — verify warning dialog appears before any decryption; (3) cancel dialog — verify field remains masked and no decrypt/receipt call made; (4) confirm dialog — mock decryption success and verify revealed text is shown; (5) mock decryption failure — verify error message shown, no partial data; (6) verify `recordReceipt` called exactly once on first reveal, not on second; (7) verify `watchReceiptStatus` returning `read` shows 'Previously viewed' badge in masked state; (8) simulate `AppLifecycleState.paused` — verify field re-masks. Accessibility: verify Semantics labels in both masked and revealed states. Security: verify decrypted value is not present in widget state after re-mask. Target ≥90% line coverage.

Component
Contact Detail Screen
ui medium
Epic Risks (2)
low impact medium prob dependency

The Peer Mentor Profile tab on the contact detail screen depends on the peer-mentor-detail-screen-widget being delivered by the separate Peer Mentor Detail feature. If that feature is delayed, the navigation affordance will be present but lead to a stub screen, which may confuse coordinators in the TestFlight pilot.

Mitigation & Contingency

Mitigation: Implement the peer mentor tab with a feature flag guard. When the Peer Mentor Detail feature is incomplete, the flag disables the tab. Coordinate delivery timelines with the team responsible for Peer Mentor Detail to align TestFlight releases.

Contingency: If the Peer Mentor Detail feature is significantly delayed, ship the contact detail screen without the peer mentor tab in the first TestFlight build and add it as an incremental update once the dependent screen is ready.

medium impact medium prob technical

The contact detail screen must adapt its layout significantly based on organisation context: NHF shows affiliation chips, Blindeforbundet shows encrypted fields and assignment status, standard contacts show neither. Managing this conditional rendering without introducing bugs in each variant is complex and increases the risk of organisation-specific regressions.

Mitigation & Contingency

Mitigation: Define a ContactDetailViewModel that resolves all org-specific flags (showEncryptedFields, showAssignmentStatus, showMultiChapterChips) from the organisation config before the widget tree renders. Widget tests must cover all three organisation variants as separate test cases to catch regressions.

Contingency: If conditional rendering logic grows unwieldy, refactor into separate composable section widgets (ProfileHeaderSection, AffiliationSection, EncryptedFieldsSection) that are conditionally included by the parent screen, isolating org-specific logic to individual components.