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>
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user