Privacy-Period-Tracker/app/src/main/kotlin/dev/privacyllc/period/navigation/RootViewModel.kt

40 lines
1.5 KiB
Kotlin
Raw Normal View History

feat: onboarding, and the Surface that dark mode was missing Seven screens, §19 and §56 verbatim: welcome, last period, period end, previous history, the privacy promise, notification privacy, first forecast. Verified end to end on a device — the flow produces a forecast, the record persists, and a relaunch goes straight to Today. Three decisions with tests behind them: - Nothing is written until the final step. Somebody who abandons onboarding halfway has not asked this app to remember anything about them. - Notification privacy is Discreet before the user touches anything (§28), and Direct is last and never pre-selected. Checked on the device, not only in a unit test. - "Still going" and "I'm not sure" both mean no end date. §24: never invent one. The date picker refuses future dates by not offering them rather than by rejecting a tap it allowed. DARK MODE WAS BROKEN FOR ALL OF BATCH 01 PeriodTheme never wrapped its content in a Surface, so every Text without an explicit colour inherited Material's default — black — and the app background never painted. In light mode that looked correct by accident, because dark text on cream is what was wanted anyway. In dark mode the onboarding headings rendered near-black on charcoal. No test caught it and no test easily would have. It was found by opening the app on a device and looking at it. The Surface now lives in the theme, so a screen without a Scaffold cannot forget, and every illustration has a light/dark preview pair. A preview is not a test, but it is the cheapest thing that puts the failure in front of whoever is editing the screen. closes #16
2026-08-18 03:44:46 -05:00
package dev.privacyllc.period.navigation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.privacyllc.period.core.datastore.UserPreferencesRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
enum class RootState { Loading, Onboarding, Ready }
@HiltViewModel
class RootViewModel @Inject constructor(
preferences: UserPreferencesRepository,
) : ViewModel() {
/**
* Set when onboarding finishes, so the switch happens immediately rather
* than waiting for the preference write to travel back through DataStore
* which is fast, but not instant, and a visible flicker on the last tap of
* onboarding is a poor first impression.
*/
private val finishedThisSession = MutableStateFlow(false)
val state: StateFlow<RootState> =
combine(
preferences.preferences.map { it.onboardingCompleted },
finishedThisSession,
) { completed, finished ->
if (completed || finished) RootState.Ready else RootState.Onboarding
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), RootState.Loading)
fun onboardingFinished() { finishedThisSession.value = true }
}