Coverage Summary for Class: SessionPointersImpl (com.georgeci.moneysurfer.data.preferences)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SessionPointersImpl |
100%
(9/9)
|
85%
(17/20)
|
100%
(20/20)
|
96.1%
(148/154)
|
| SessionPointersImpl$Companion |
|
| SessionPointersImpl$special$$inlined$map$1 |
0%
(0/2)
|
|
| SessionPointersImpl$special$$inlined$map$1$1 |
|
| SessionPointersImpl$special$$inlined$map$1$2 |
0%
(0/1)
|
|
| SessionPointersImpl$special$$inlined$map$1$2$1 |
|
| SessionPointersImpl$special$$inlined$map$2 |
0%
(0/2)
|
|
| SessionPointersImpl$special$$inlined$map$2$1 |
|
| SessionPointersImpl$special$$inlined$map$2$2 |
0%
(0/1)
|
|
| SessionPointersImpl$special$$inlined$map$2$2$1 |
|
| SessionPointersImpl$special$$inlined$map$3 |
0%
(0/2)
|
|
| SessionPointersImpl$special$$inlined$map$3$1 |
|
| SessionPointersImpl$special$$inlined$map$3$2 |
0%
(0/1)
|
|
| SessionPointersImpl$special$$inlined$map$3$2$1 |
|
| Total |
50%
(9/18)
|
85%
(17/20)
|
100%
(20/20)
|
96.1%
(148/154)
|
package com.georgeci.moneysurfer.data.preferences
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.georgeci.moneysurfer.data.datastore.core.PreferenceStore
import com.georgeci.moneysurfer.domain.auth.SessionMutator
import com.georgeci.moneysurfer.domain.auth.SessionPointers
import com.georgeci.moneysurfer.domain.primitives.UserId
import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
/**
* Session pointers on their own [PreferenceStore], no longer sharing `UiSettingsDataSource` with the
* UI preferences — those moved onto the configuration engine, and these deliberately did not (see
* ADR-004 §"Session pointers stay out": `null` means "nobody is signed in" here, which is
* incompatible with the engine reading `null` from a layer as "absent").
*
* Key names and stored types are unchanged, so an existing install keeps its session.
*
* The empty string is the on-disk stand-in for "cleared" — DataStore preferences are non-null, and
* removing the entry instead would mean re-deriving "cleared" from a missing key at every read.
*/
@Single(binds = [SessionPointers::class, SessionMutator::class])
class SessionPointersImpl(dataStore: DataStore<Preferences>) : SessionPointers, SessionMutator {
private val prefs = PreferenceStore(dataStore)
private val userId = prefs.custom(stringPreferencesKey("session.current_user_id"), CLEARED)
private val workspaceId = prefs.custom(stringPreferencesKey("session.current_workspace_id"), CLEARED)
/**
* Firebase auth uid. Set when a Firebase-backed flow (login/signup/anon) succeeds, cleared on
* logout. Stays empty for local-only sessions (demo), which lets callers detect "is there a
* Firebase session right now?" without round-tripping through the auth SDK.
*/
private val firebaseUid = prefs.custom(stringPreferencesKey("session.firebase_uid"), CLEARED)
/**
* Sticky flag set by `DemoLoginUseCase` and cleared by `WipeDemoDataUseCase`. Device-scoped: it
* survives [clearSession] on purpose, because the auth flow consults it on the next real
* login/signup to purge orphan demo data before bootstrapping. Demo data must never reach
* Firestore — see sync.md §2.11.
*/
private val hasUsedDemoPref = prefs.custom(booleanPreferencesKey("session.has_used_demo"), false)
override val currentUserId: Flow<UserId?> = userId.flow.map { stored -> stored.orNull()?.let(::UserId) }
override val currentWorkspaceId: Flow<WorkspaceId?> =
workspaceId.flow.map { stored -> stored.orNull()?.let(::WorkspaceId) }
override val currentFirebaseUid: Flow<String?> = firebaseUid.flow.map { stored -> stored.orNull() }
override val hasUsedDemo: Flow<Boolean> = hasUsedDemoPref.flow
override suspend fun setCurrentUser(id: UserId?) = userId.set(id?.value.orEmpty())
override suspend fun setCurrentWorkspace(id: WorkspaceId?) = workspaceId.set(id?.value.orEmpty())
override suspend fun setFirebaseUid(uid: String?) = firebaseUid.set(uid.orEmpty())
override suspend fun setHasUsedDemo(value: Boolean) = hasUsedDemoPref.set(value)
override suspend fun clearSession() {
prefs.edit { preferences ->
preferences[userId.key] = CLEARED
preferences[workspaceId.key] = CLEARED
preferences[firebaseUid.key] = CLEARED
}
}
private fun String.orNull(): String? = ifEmpty { null }
private companion object {
const val CLEARED = ""
}
}