high priority medium complexity integration pending frontend specialist Tier 1

Acceptance Criteria

A Riverpod StreamProvider (e.g., receiptThresholdStateProvider) exposes a ReceiptThresholdState enum (optional | required) derived from the expense amount stream
The attachment indicator widget subscribes via ref.watch(receiptThresholdStateProvider) and rebuilds only the indicator subtree, not the parent form widget
When the expense amount field value crosses the org-configured threshold (upward), the indicator transitions to 'required' within one frame after the stream emits
When the expense amount is reduced below the threshold, the indicator transitions back to 'optional' within one frame
The threshold value is sourced exclusively from the org config repository — no hardcoded numeric value exists anywhere in the UI layer or indicator widget
Two organizations with different thresholds (e.g., 100 NOK vs 500 NOK) each trigger 'required' at their own configured amount with no code change
If the org config stream is unavailable (loading), the indicator shows a neutral/indeterminate state rather than defaulting to either required or optional
No unnecessary widget rebuilds occur outside the indicator subtree when amount changes (verified via Flutter DevTools rebuild counter)
Unit tests cover: threshold not yet crossed → optional, exactly at threshold → required, above threshold → required, below threshold after being above → optional, null/loading config → neutral state

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase PostgreSQL 15 (org config read)
data models
activity
activity_type
performance requirements
Indicator state update must complete within a single frame (~16ms) after stream emission
No full-page widget rebuild triggered by threshold state changes — use granular ref.watch scoping
StreamProvider must debounce rapid amount-field keystrokes by 300ms to avoid excessive emissions
security requirements
Threshold value read from Supabase with RLS enforced — org cannot read another org's config
No threshold logic executed client-side with hardcoded values that could be bypassed
ui components
ReceiptAttachmentIndicator widget
StreamProvider subscription consumer
Org config repository interface

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define a ReceiptThresholdState sealed class (optional | required | loading) rather than a raw bool to handle the loading/unavailable config state gracefully. The StreamProvider should combine the expense amount stream with the org config stream using Riverpod's combineLatest equivalent (StreamProvider.family or a custom provider). Avoid placing threshold logic in the widget build method — keep it in a dedicated ThresholdEvaluator service class injected via Riverpod. Use select() on the provider where possible to further narrow rebuilds.

The org config repository must be the single source of truth; add a lint rule or comment guard in the indicator widget explicitly forbidding hardcoded numeric comparisons.

Testing Requirements

Unit tests (flutter_test): StreamProvider emits correct ReceiptThresholdState for amounts below, at, and above threshold; null-safety for loading config state. Widget tests: indicator renders 'optional' and 'required' visual states correctly; verify no parent rebuild using tester.binding.hasScheduledFrame. Integration test: provision two orgs with different thresholds via Supabase test fixtures, fill expense amount field incrementally, assert indicator state transitions at each org's correct threshold. No e2e device test required for this task — covered by task-009.

Component
Receipt Attachment Indicator
ui low
Epic Risks (2)
medium impact medium prob technical

Flutter's accessibility live region support (SemanticsProperties.liveRegion) has known inconsistencies between iOS VoiceOver and Android TalkBack, and between Flutter versions. Threshold-crossing announcements may fail to fire or double-fire, breaking the accessibility contract for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Test live region announcements on physical devices with VoiceOver and TalkBack enabled from the first iteration. Use the AccessibilityLiveRegionAnnouncer component pattern already established in the project. Verify announcement timing relative to Bloc state emissions to avoid double-fires.

Contingency: If Flutter live regions prove unreliable, implement a platform-channel fallback that calls UIAccessibility.post(notification:) on iOS and AccessibilityManager.sendAccessibilityEvent() on Android directly, bypassing Flutter's abstraction.

medium impact low prob integration

The org-configurable threshold must be available at form-render time. If the threshold configuration is fetched asynchronously and not cached, the indicator may briefly show the wrong state (e.g., 'optional' before the threshold loads), confusing users and potentially allowing invalid submissions.

Mitigation & Contingency

Mitigation: Ensure the receipt threshold validator loads and caches the org configuration at app startup or organization selection time, not lazily on form open. Use a loading state in the indicator widget rather than defaulting to 'optional' while configuration is pending.

Contingency: If startup caching is not feasible, treat an unknown threshold as 'receipt required' (fail safe) and surface a clear loading indicator until the configuration resolves, preventing invalid submissions while the config loads.