critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A sealed class (Dart 3+) named LocalAuthFailure is defined with variants: notAvailable, notEnrolled, lockedOut, permanentlyLockedOut, cancelled, passcodeNotSet, unknown
Each variant is a separate final class extending LocalAuthFailure, not a simple enum, allowing future addition of fields per variant
A top-level or static mapper function mapLocalAuthException(Object exception) -> LocalAuthFailure is implemented and handles all known local_auth exception subtypes
The mapper handles PlatformException with error codes from local_auth ('NotAvailable', 'NotEnrolled', 'LockedOut', 'PermanentlyLockedOut', 'passcodeNotSet') and maps each to the correct domain code
User cancellation (error code 'locked_out' or 'notAvailable' with specific messages) maps to LocalAuthFailure.cancelled
Any unmapped exception or code maps to LocalAuthFailure.unknown, never throws
No direct import of local_auth types outside of the mapper function file
The domain layer (BiometricAuthService, etc.) imports only LocalAuthFailure, never local_auth package types

Technical Requirements

frameworks
Flutter
Dart 3+ sealed classes
apis
local_auth PlatformException error codes
data models
LocalAuthFailure (sealed class hierarchy)
BiometricType (domain enum)
performance requirements
Mapper function executes synchronously with negligible overhead
No heap allocation beyond the single returned failure object
security requirements
Domain error codes must not leak platform-specific strings that could expose device security state to UI layer
unknown variant must not re-throw or surface raw exception messages to callers

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Dart 3 sealed classes rather than enums so individual variants can carry typed payloads later (e.g., lockedOut could carry a Duration until unlock). Place the sealed class and the mapper in the same file: lib/features/auth/domain/failures/local_auth_failure.dart. The mapper should accept Object (not a typed exception) and use a series of if/else-if checks on runtime type and error code string, with a final else returning unknown. Avoid using package:local_auth types in the return type or parameter type of the mapper — only use them inside the function body.

This keeps the domain layer free of plugin dependencies. Cross-reference the local_auth package source for the full list of PlatformException error codes since the package documentation is incomplete.

Testing Requirements

Unit tests using flutter_test covering: (1) each known PlatformException error code maps to the exact expected LocalAuthFailure variant, (2) an unrecognised error code maps to LocalAuthFailure.unknown, (3) a null or non-PlatformException input maps to LocalAuthFailure.unknown without throwing, (4) exhaustive pattern matching on LocalAuthFailure compiles without warnings (validates sealed class completeness). No mocks required — mapper is a pure function. Minimum 10 test cases, one per mapped code plus edge cases.

Component
Local Auth Integration
infrastructure low
Epic Risks (3)
high impact medium prob technical

iOS Keychain access requires correct entitlement configuration and provisioning profile setup. Misconfigured entitlements cause silent failures in CI/CD and on physical devices, where the plugin appears to work in the simulator but fails at runtime. This can delay foundation delivery and block all downstream epics.

Mitigation & Contingency

Mitigation: Add a dedicated integration test running on a physical iOS device early in the epic. Document required entitlements and provisioning steps in a developer runbook. Validate Keychain access in the CI pipeline using an iOS simulator with correct entitlements enabled.

Contingency: If Keychain entitlements cannot be resolved quickly, temporarily use in-memory storage behind the SecureSessionStorage interface to unblock downstream epics, then resolve the Keychain issue in a hotfix before release.

medium impact medium prob dependency

The Flutter local_auth plugin has a history of breaking API changes between major versions, and its Android implementation depends on BiometricPrompt which behaves differently across Android API levels (23-34). An incompatible plugin version or unexpected Android API behaviour can cause authentication failures on a significant portion of the target device fleet.

Mitigation & Contingency

Mitigation: Pin local_auth to a specific stable version in pubspec.yaml. Test against Android API levels 23, 28, and 33 in the CI matrix. Review the plugin changelog and migration guide before adopting any version bump.

Contingency: If the pinned version proves incompatible with target devices, evaluate flutter_local_auth_android as a replacement or fork the plugin adapter to isolate the breaking surface.

high impact low prob security

If users upgrade from a version of the app that stored session data in non-encrypted storage (SharedPreferences), a migration path is required. Failing to migrate silently leaves old tokens in plain storage, creating a security gap and potentially causing confusing authentication state on first launch of the new version.

Mitigation & Contingency

Mitigation: Audit existing storage usage across the codebase before writing SecureSessionStorage. If legacy plain storage keys exist, implement a one-time migration routine that reads from SharedPreferences, writes to Keychain/Keystore, and deletes the plain-text entry.

Contingency: If migration is discovered late, ship the migration as a mandatory patch release before the biometric feature is enabled for users, and add a startup check that blocks biometric opt-in until migration is confirmed complete.