Files
honeyDueKMP/composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/OnboardingViewModel.kt
T
Trey t 1e2adf7660 Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project:
- Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations)
- Gradle: rootProject.name, namespace, applicationId
- Android: manifest, strings.xml (all languages), widget resources
- iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig
- iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc.
- Swift source: all class/struct/enum renames
- Deep links: casera:// -> honeydue://, .casera -> .honeydue
- App icons replaced with honeyDue honeycomb icon
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- Database table names preserved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:57 -06:00

380 lines
12 KiB
Kotlin

package com.tt.honeyDue.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.models.AuthResponse
import com.tt.honeyDue.models.LoginRequest
import com.tt.honeyDue.models.RegisterRequest
import com.tt.honeyDue.models.ResidenceCreateRequest
import com.tt.honeyDue.models.TaskCreateRequest
import com.tt.honeyDue.models.TaskTemplate
import com.tt.honeyDue.models.VerifyEmailRequest
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.repository.LookupsRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
* User's intent during onboarding
*/
enum class OnboardingIntent {
UNKNOWN,
START_FRESH, // Creating a new residence
JOIN_EXISTING // Joining with a share code
}
/**
* Steps in the onboarding flow
*/
enum class OnboardingStep {
WELCOME,
VALUE_PROPS,
NAME_RESIDENCE,
CREATE_ACCOUNT,
VERIFY_EMAIL,
JOIN_RESIDENCE,
RESIDENCE_LOCATION,
FIRST_TASK,
SUBSCRIPTION_UPSELL
}
/**
* ViewModel for managing the onboarding flow state
*/
class OnboardingViewModel : ViewModel() {
private val _currentStep = MutableStateFlow(OnboardingStep.WELCOME)
val currentStep: StateFlow<OnboardingStep> = _currentStep
private val _userIntent = MutableStateFlow(OnboardingIntent.UNKNOWN)
val userIntent: StateFlow<OnboardingIntent> = _userIntent
private val _residenceName = MutableStateFlow("")
val residenceName: StateFlow<String> = _residenceName
private val _shareCode = MutableStateFlow("")
val shareCode: StateFlow<String> = _shareCode
// Registration state
private val _registerState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
val registerState: StateFlow<ApiResult<AuthResponse>> = _registerState
// Login state (for returning users)
private val _loginState = MutableStateFlow<ApiResult<AuthResponse>>(ApiResult.Idle)
val loginState: StateFlow<ApiResult<AuthResponse>> = _loginState
// Email verification state
private val _verifyEmailState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
val verifyEmailState: StateFlow<ApiResult<Unit>> = _verifyEmailState
// Residence creation state
private val _createResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
val createResidenceState: StateFlow<ApiResult<Unit>> = _createResidenceState
// Join residence state
private val _joinResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
val joinResidenceState: StateFlow<ApiResult<Unit>> = _joinResidenceState
// Task creation state
private val _createTasksState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
val createTasksState: StateFlow<ApiResult<Unit>> = _createTasksState
// Regional templates state
private val _regionalTemplates = MutableStateFlow<ApiResult<List<TaskTemplate>>>(ApiResult.Idle)
val regionalTemplates: StateFlow<ApiResult<List<TaskTemplate>>> = _regionalTemplates
// ZIP code entered during location step (persisted on residence)
private val _postalCode = MutableStateFlow("")
val postalCode: StateFlow<String> = _postalCode
// Whether onboarding is complete
private val _isComplete = MutableStateFlow(false)
val isComplete: StateFlow<Boolean> = _isComplete
fun setUserIntent(intent: OnboardingIntent) {
_userIntent.value = intent
}
fun setResidenceName(name: String) {
_residenceName.value = name
}
fun setShareCode(code: String) {
_shareCode.value = code
}
/**
* Move to the next step in the flow
* Flow: Welcome → Features → Name Residence → Create Account → Verify → Tasks → Upsell
*/
fun nextStep() {
_currentStep.value = when (_currentStep.value) {
OnboardingStep.WELCOME -> {
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
OnboardingStep.CREATE_ACCOUNT
} else {
OnboardingStep.VALUE_PROPS
}
}
OnboardingStep.VALUE_PROPS -> OnboardingStep.NAME_RESIDENCE
OnboardingStep.NAME_RESIDENCE -> OnboardingStep.CREATE_ACCOUNT
OnboardingStep.CREATE_ACCOUNT -> OnboardingStep.VERIFY_EMAIL
OnboardingStep.VERIFY_EMAIL -> {
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
OnboardingStep.JOIN_RESIDENCE
} else {
OnboardingStep.RESIDENCE_LOCATION
}
}
OnboardingStep.JOIN_RESIDENCE -> OnboardingStep.SUBSCRIPTION_UPSELL
OnboardingStep.RESIDENCE_LOCATION -> OnboardingStep.FIRST_TASK
OnboardingStep.FIRST_TASK -> OnboardingStep.SUBSCRIPTION_UPSELL
OnboardingStep.SUBSCRIPTION_UPSELL -> {
completeOnboarding()
OnboardingStep.SUBSCRIPTION_UPSELL
}
}
}
/**
* Go to a specific step
*/
fun goToStep(step: OnboardingStep) {
_currentStep.value = step
}
/**
* Go back to the previous step
*/
fun previousStep() {
_currentStep.value = when (_currentStep.value) {
OnboardingStep.VALUE_PROPS -> OnboardingStep.WELCOME
OnboardingStep.NAME_RESIDENCE -> OnboardingStep.VALUE_PROPS
OnboardingStep.CREATE_ACCOUNT -> {
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
OnboardingStep.WELCOME
} else {
OnboardingStep.NAME_RESIDENCE
}
}
OnboardingStep.VERIFY_EMAIL -> OnboardingStep.CREATE_ACCOUNT
else -> _currentStep.value
}
}
/**
* Skip the current step (for skippable screens)
*/
fun skipStep() {
when (_currentStep.value) {
OnboardingStep.VALUE_PROPS,
OnboardingStep.JOIN_RESIDENCE,
OnboardingStep.RESIDENCE_LOCATION,
OnboardingStep.FIRST_TASK -> nextStep()
OnboardingStep.SUBSCRIPTION_UPSELL -> completeOnboarding()
else -> {}
}
}
/**
* Register a new user
*/
fun register(username: String, email: String, password: String) {
viewModelScope.launch {
_registerState.value = ApiResult.Loading
val result = APILayer.register(
RegisterRequest(
username = username,
email = email,
password = password
)
)
_registerState.value = when (result) {
is ApiResult.Success -> {
DataManager.setAuthToken(result.data.token)
DataManager.setCurrentUser(result.data.user)
LookupsRepository.initialize()
ApiResult.Success(result.data)
}
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
/**
* Login an existing user
*/
fun login(username: String, password: String) {
viewModelScope.launch {
_loginState.value = ApiResult.Loading
val result = APILayer.login(LoginRequest(username, password))
_loginState.value = when (result) {
is ApiResult.Success -> {
LookupsRepository.initialize()
ApiResult.Success(result.data)
}
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
/**
* Verify email with 6-digit code
*/
fun verifyEmail(code: String) {
viewModelScope.launch {
_verifyEmailState.value = ApiResult.Loading
val token = DataManager.authToken.value ?: run {
_verifyEmailState.value = ApiResult.Error("Not authenticated")
return@launch
}
val result = APILayer.verifyEmail(token, VerifyEmailRequest(code = code))
_verifyEmailState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(Unit)
is ApiResult.Error -> result
else -> ApiResult.Error("Unknown error")
}
}
}
/**
* Create the residence with the name from onboarding
*/
fun createResidence() {
viewModelScope.launch {
if (_residenceName.value.isBlank()) {
_createResidenceState.value = ApiResult.Success(Unit)
return@launch
}
_createResidenceState.value = ApiResult.Loading
val result = APILayer.createResidence(
ResidenceCreateRequest(
name = _residenceName.value,
propertyTypeId = null,
streetAddress = null,
apartmentUnit = null,
city = null,
stateProvince = null,
postalCode = _postalCode.value.takeIf { it.isNotBlank() },
country = null,
bedrooms = null,
bathrooms = null,
squareFootage = null,
lotSize = null,
yearBuilt = null,
description = null,
purchaseDate = null,
purchasePrice = null,
isPrimary = true
)
)
_createResidenceState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(Unit)
is ApiResult.Error -> result
else -> ApiResult.Error("Failed to create residence")
}
}
}
/**
* Join an existing residence with a share code
*/
fun joinResidence(code: String) {
viewModelScope.launch {
_joinResidenceState.value = ApiResult.Loading
val result = APILayer.joinWithCode(code)
_joinResidenceState.value = when (result) {
is ApiResult.Success -> ApiResult.Success(Unit)
is ApiResult.Error -> result
else -> ApiResult.Error("Failed to join residence")
}
}
}
/**
* Create selected tasks during onboarding
*/
fun createTasks(taskRequests: List<TaskCreateRequest>) {
viewModelScope.launch {
if (taskRequests.isEmpty()) {
_createTasksState.value = ApiResult.Success(Unit)
return@launch
}
_createTasksState.value = ApiResult.Loading
var successCount = 0
for (request in taskRequests) {
val result = APILayer.createTask(request)
if (result is ApiResult.Success) {
successCount++
}
}
_createTasksState.value = if (successCount > 0) {
ApiResult.Success(Unit)
} else {
ApiResult.Error("Failed to create tasks")
}
}
}
/**
* Load regional templates by ZIP code (backend resolves ZIP → state → climate zone).
* Also stores the ZIP code for later use when creating the residence.
*/
fun loadRegionalTemplates(zip: String) {
_postalCode.value = zip
viewModelScope.launch {
_regionalTemplates.value = ApiResult.Loading
_regionalTemplates.value = APILayer.getRegionalTemplates(zip = zip)
}
}
/**
* Mark onboarding as complete
*/
fun completeOnboarding() {
_isComplete.value = true
}
/**
* Reset all state (useful for testing)
*/
fun reset() {
_currentStep.value = OnboardingStep.WELCOME
_userIntent.value = OnboardingIntent.UNKNOWN
_residenceName.value = ""
_shareCode.value = ""
_registerState.value = ApiResult.Idle
_loginState.value = ApiResult.Idle
_verifyEmailState.value = ApiResult.Idle
_createResidenceState.value = ApiResult.Idle
_joinResidenceState.value = ApiResult.Idle
_createTasksState.value = ApiResult.Idle
_regionalTemplates.value = ApiResult.Idle
_postalCode.value = ""
_isComplete.value = false
}
fun resetRegisterState() {
_registerState.value = ApiResult.Idle
}
fun resetLoginState() {
_loginState.value = ApiResult.Idle
}
fun resetVerifyEmailState() {
_verifyEmailState.value = ApiResult.Idle
}
}