Implement UserIdentityRepository with Supabase CRUD and local cache
epic-bankid-vipps-login-foundation-task-005 — Build the UserIdentityRepository that reads and writes user identity data (personnummer, BankID verification status, Vipps subject identifier) to Supabase. Implement upsertIdentity, getIdentityByUserId, markBankIdVerified, and storePersonnummer methods. Cache the identity record locally using SecureStorageAdapter to avoid redundant network calls on app resume. Include GDPR-compliant purge method to wipe all local identity data on logout.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Repository constructor takes SupabaseClient and SecureStorageAdapter as dependencies for testability. Cache key format: 'identity.{userId}' using SecureStorageKey enum extension. Serialize UserIdentity to/from JSON for cache storage (jsonEncode / jsonDecode with DateTime.toIso8601String()). For markBankIdVerified: call supabase.rpc('mark_bankid_verified', params: {'target_user_id': userId}) — the PostgreSQL function should do a server-side UPDATE with SECURITY DEFINER to bypass RLS for this specific column update while the Edge Function or RPC verifies the BankID token.
Cache TTL: store a cache timestamp alongside the identity JSON in secure storage (key: 'identity.{userId}.cached_at') and compare against DateTime.now() on read. For the Riverpod provider, use Provider.autoDispose only if identity data should not be retained across navigations — prefer a long-lived Provider since identity is app-global. Implement UserIdentity.fromJson / toJson using a private method, not a generated file, to keep the dependency graph minimal.
Testing Requirements
Unit tests using flutter_test, FakeSecureStorageAdapter, and a mocked Supabase client (mocktail). Test file at test/core/auth/user_identity_repository_test.dart. Required scenarios: (1) getIdentityByUserId with empty cache calls Supabase exactly once and caches result; (2) getIdentityByUserId called again within TTL returns cached value and does NOT call Supabase; (3) upsertIdentity calls Supabase upsert and updates cache; (4) purgeLocalIdentityData after getIdentityByUserId results in next getIdentityByUserId calling Supabase again; (5) SupabaseException during getIdentityByUserId is wrapped and thrown as IdentityRepositoryException; (6) storePersonnummer writes encrypted value to both Supabase and cache. Integration test (manual): verify RLS by attempting to read another user's identity via Flutter app — confirm 0 rows returned.
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.
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.
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.