Implement SecureStorage write and read path in TenantSessionStore
epic-organization-selection-and-onboarding-data-layer-task-006 — Implement the SecureStorageAdapter integration within TenantSessionStore for offline-capable session persistence. On persistSelection(), serialize TenantSessionData to JSON and write to SecureStorageAdapter under a fixed key scoped to the authenticated user ID. On restoreSelection(), read and deserialize the stored JSON, returning null if absent or invalid. On clearSelection(), delete the secure storage entry. Ensure all operations are atomic and handle serialization errors gracefully.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Inject SecureStorageAdapter and a SupabaseAuthClient as constructor dependencies for testability — do not instantiate FlutterSecureStorage directly inside the class. Use a private helper `String _storageKey(String userId) => 'tenant_session_$userId'` for key generation. Wrap the FlutterSecureStorage write in a try/catch on PlatformException, rethrowing as a typed SecureStoragePersistenceException with a descriptive message. For the read path, use `jsonDecode` inside a try/catch to guard against corrupt data.
The `persistSelection` implementation for this task covers only the SecureStorage path; the Supabase claim path is added in task-007 as a second phase of the same method.
Testing Requirements
Write unit tests in flutter_test using a MockSecureStorageAdapter (Mockito or manual fake): (1) persistSelection writes correct key and JSON-serialized value, (2) restoreSelection reads correct key and returns deserialized TenantSessionData, (3) restoreSelection returns null when key not found, (4) restoreSelection returns null when stored JSON is corrupt, (5) clearSelection calls delete on the correct key, (6) verify key includes userId suffix by injecting a mock auth provider returning a known UID. No device-level integration tests required at this layer.
TenantSessionStore must write to both SecureStorageAdapter and Supabase session synchronously. If the Supabase write succeeds but the local secure storage write fails (or vice versa), the system ends up in an inconsistent state: local app thinks org A is selected but Supabase queries are scoped to org B (or unscoped), causing silent data leakage or empty result sets.
Mitigation & Contingency
Mitigation: Implement the dual-write with compensating rollback: if the second write fails, undo the first write and surface a typed DualWriteFailureError to callers. Add a startup integrity check in restoreSession() that validates local storage and Supabase session agree on the current org_id.
Contingency: If integrity check fails on startup, clear both stores and redirect the user to the org selection screen rather than proceeding with potentially mismatched state.
An organization could be deactivated in Supabase between the time the org list is cached and the time the user taps to select it. If the repository serves stale cached data the org-selection-service will attempt to seed a context for an inactive org, potentially causing RLS scope issues or confusing error states.
Mitigation & Contingency
Mitigation: The OrganizationRepository.getOrganizationById() path used during selection validation always performs a fresh network fetch (bypassing cache) to confirm the org is still active before the TenantSessionStore writes anything.
Contingency: If a freshness check finds the org is inactive, surface a localized error message on the selection screen ('This organization is no longer available') and refresh the org list to show only currently active partners.