critical priority low complexity infrastructure pending backend specialist Tier 0

Acceptance Criteria

An abstract class or Dart interface named SecureStorageAdapter is defined in lib/core/storage/secure_storage_adapter.dart
The interface declares read(SecureStorageKey key) → Future<String?> with explicit nullable return for missing keys
The interface declares write(SecureStorageKey key, String value) → Future<void>
The interface declares delete(SecureStorageKey key) → Future<void>
The interface declares clear() → Future<void> to wipe all secured entries
A SecureStorageKey enum or sealed class is defined listing all valid storage keys (access_token, refresh_token, token_expiry, user_identity, personnummer)
The interface is registered as a Riverpod provider (Provider<SecureStorageAdapter>) with the concrete implementation as the default
flutter_secure_storage is added to pubspec.yaml with a pinned compatible version
The abstraction does not leak flutter_secure_storage types into the public interface — consumers depend only on SecureStorageAdapter
File compiles with zero warnings under dart analyze --fatal-infos
npm run build equivalent (flutter pub get && dart analyze) passes in CI

Technical Requirements

frameworks
Flutter
Riverpod
flutter_secure_storage
apis
Android Keystore API (via flutter_secure_storage AndroidOptions)
iOS Secure Enclave / Keychain (via flutter_secure_storage IOSOptions)
data models
SecureStorageKey (enum/sealed class)
SecureStorageAdapter (abstract interface)
performance requirements
Interface methods must be asynchronous (Future-based) to avoid blocking the UI thread
Key lookup must be O(1) via enum/sealed class — no string key construction at call sites
security requirements
All keys passed to flutter_secure_storage must be derived from the SecureStorageKey enum — no raw string keys allowed
AES-256 encryption must be configured via AndroidOptions(encryptedSharedPreferences: true)
iOS accessibility must be set to first_unlock_this_device and synchronizable: false to prevent iCloud sync of sensitive tokens
The interface must not expose any method that returns raw platform storage handles or file paths

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Place all files under lib/core/storage/. Define SecureStorageKey as an enum with a toKey() method that returns the string key used by flutter_secure_storage (e.g., 'auth.access_token') — this prevents typos and makes refactoring safe. Register the Riverpod provider in a dedicated providers file (lib/core/storage/storage_providers.dart) to avoid circular imports. The concrete FlutterSecureStorageAdapter class should be defined in the same file as the provider registration.

Do not expose IOSOptions or AndroidOptions in the interface — configure them only inside the concrete class. Follow the Repository pattern: the adapter is infrastructure, not domain. Consider defining a StorageException typed error for read/write failures so callers can catch it specifically instead of platform exceptions.

Testing Requirements

Unit tests are not required for this task (interface-only). Integration test: verify that a FakeSecureStorageAdapter (in-memory Map implementation) satisfies the SecureStorageAdapter contract — write, read back, delete, then confirm read returns null, then clear and confirm all keys return null. This fake implementation should be committed alongside the interface for use in all downstream tests.

Component
Secure Storage Adapter
infrastructure low
Epic Risks (3)
high impact medium prob technical

Flutter Secure Storage behavior differs between iOS Keychain and Android Keystore — key accessibility attributes (kSecAttrAccessibleWhenUnlocked vs. WhenUnlockedThisDeviceOnly) may cause tokens to become inaccessible after device restart or OS upgrade, breaking session restoration for returning users.

Mitigation & Contingency

Mitigation: Define explicit Keychain accessibility attributes during implementation and write integration tests on both platforms. Follow flutter_secure_storage documentation for cross-platform accessibility configuration.

Contingency: Implement a recovery flow that detects secure storage read failures and falls back to full re-authentication rather than crashing. Add a migration utility to re-write tokens with corrected attributes if a misconfiguration is discovered post-release.

high impact medium prob security

Personnummer is a legally sensitive national identifier under Norwegian GDPR implementation. If encryption-at-rest or data minimization requirements are not met before launch, the feature could be blocked by legal/compliance review from any of the four partner organizations.

Mitigation & Contingency

Mitigation: Ensure personnummer is only persisted after explicit user consent via the personnummer confirmation widget. Use Supabase column-level encryption for the personnummer field. Document the data processing basis and retention policy before the first TestFlight release.

Contingency: If legal review blocks the personnummer write-back, implement the feature as opt-in only with a deferred sync model, allowing BankID/Vipps login to proceed without storing the personnummer until compliance is confirmed.

medium impact high prob dependency

If the VippsOrgCostConfig data is not populated in Supabase for all four partner organizations before the feature ships, users from unconfigured organizations will see no Vipps login option and may report it as broken, creating confusion and support load.

Mitigation & Contingency

Mitigation: Create a seed migration script for Vipps org configuration and include it in the deployment checklist. Implement a clear admin UI warning when an organization is missing Vipps configuration.

Contingency: Add a feature flag in VippsOrgCostConfig so individual organizations can be enabled/disabled without a code deploy, allowing rapid remediation.