critical priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

A search bar is rendered above the contact list; typing filters the list to rows whose name contains the search string (case-insensitive)
Search filtering is debounced by 300 ms so the list does not re-filter on every keystroke
Clearing the search bar restores the full contact list immediately
Selecting a row highlights it visually and deselects any previously selected row (radio group behaviour — only one selection at a time)
The 'Continue' button is rendered in a disabled/muted state when no mentor is selected
The 'Continue' button becomes enabled immediately when a mentor is selected
If the user taps 'Continue' while no mentor is selected (e.g., via accessibility or programmatic trigger), an inline validation error message appears near the button
The inline error message disappears as soon as a mentor is selected
The selected mentor's ID is available to pass downstream to the next screen when 'Continue' is tapped
Search state and selection state are preserved if the user scrolls the list or the keyboard is dismissed

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
data models
PeerMentor (id, fullName)
SingleSelectorState (searchQuery, selectedMentorId, hasAttemptedContinue)
performance requirements
Debounce delay of 300 ms on search input to avoid unnecessary list rebuilds
Filtering must be O(n) local operation — no Supabase call on search
List must not flicker or lose scroll position when selection state changes
security requirements
Search query is never sent to a server endpoint — purely client-side filtering of cached data
ui components
TextField with InputDecoration for search bar (reuse AppTextField if available in project)
Debounce logic via Timer.periodic cancellation or a Riverpod StateNotifier debounce utility
Radio-style ListTile or custom row with selected highlight using design token colours
AppButton (disabled variant) for the 'Continue' CTA
Inline error Text widget with error colour from design tokens
Riverpod StateNotifier or StateProvider for SingleSelectorState

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Manage search and selection state in a single Riverpod StateNotifier (SingleSelectorNotifier) so both pieces of state are co-located and can be tested in isolation. Debounce by holding a Timer in the notifier and cancelling it on each new search input event — avoid using StreamTransformer unless it is already established in the project. The filter logic should derive the visible list via a Provider.select() or a derived AutoDisposeProvider watching both the full list and the search query; this avoids re-rendering the full list widget when only the query changes. For the Continue button, pass an onPressed: null value when no mentor is selected — Flutter's ElevatedButton/TextButton automatically renders in a disabled style and is correctly announced as disabled by screen readers.

Store hasAttemptedContinue as a boolean flag in the notifier to control inline error visibility, flipping it true on a Continue tap and false when a valid selection exists.

Testing Requirements

Write widget tests for: (1) search filtering — type 'anna', assert only rows containing 'anna' (case-insensitive) are visible; (2) debounce — use fake async to verify list does not re-render before 300 ms elapses; (3) selection — tap a row, assert it is visually selected and others are not; (4) single-select enforcement — tap row A, then row B, assert row A is deselected and row B is selected; (5) Continue disabled — assert button is disabled with no selection; (6) Continue enabled — select a mentor, assert button is enabled; (7) validation error — call continue handler with no selection, assert error message is visible; (8) error clears — assert error disappears after selection. Write unit tests for the debounce and filter logic in isolation.

Component
Peer Mentor Single Selector
ui low
Epic Risks (2)
medium impact medium prob technical

NHF coordinators may manage dozens of peer mentors across multiple chapters. If the multi-select list renders all contacts in a single unsorted ListView, performance degrades with 50+ items, and coordinators cannot efficiently locate a specific mentor, increasing the probability of selection errors and wrong-person proxy registrations.

Mitigation & Contingency

Mitigation: Use a SliverList with itemExtent for fixed-height rows to enable O(1) scroll position calculation. Implement the search filter using a debounce utility operating on an in-memory list (no extra API calls). Sort the contact list alphabetically by default. Add chapter-filter chips above the list for NHF's multi-chapter coordinators.

Contingency: If performance issues arise in testing with real data sets, introduce pagination with a 'load more' trigger at the bottom of the list and cache rendered rows using Flutter's AutomaticKeepAliveClientMixin.

high impact medium prob security

NHF has a complex 12-national-association / 9-region / 1,400-chapter hierarchy. It is ambiguous whether a coordinator can proxy-register for peer mentors outside their immediately assigned chapter. If the contact list is not correctly scoped by RLS, coordinators might see — and register on behalf of — peer mentors they do not manage, creating fraudulent activity records that skew Bufdir statistics.

Mitigation & Contingency

Mitigation: The Proxy Contact List Provider must query only peer mentors linked to the coordinator's own chapter scope via RLS. Add an explicit Supabase query test asserting that a coordinator from chapter A cannot retrieve peer mentors from chapter B. Display each mentor's chapter affiliation in the list row so coordinators can visually verify scope.

Contingency: If RLS scope is found to be too permissive in testing, apply a server-side coordinator_id filter as a secondary guard on the query. Block the feature release until the scope test passes consistently.