Smart onboarding: home profile, tabbed tasks, free app

New onboarding step: "Tell us about your home" with chip-based pickers
for systems (heating/cooling/water heater), features (pool, fireplace,
garage, etc.), exterior (roof, siding), interior (flooring, landscaping).
All optional, skippable.

Tabbed task selection: "For You" tab shows personalized suggestions
based on home profile, "Browse All" has existing category browser.
Removed 5-task limit — users can add unlimited tasks.

Removed subscription upsell from onboarding flow — app is free.
Fixed picker capsule squishing bug with .fixedSize() modifier.

Both iOS and Compose implementations updated.
This commit is contained in:
Trey T
2026-03-30 09:02:27 -05:00
parent 8f86fa2cd0
commit 4609d5a953
18 changed files with 2293 additions and 266 deletions
@@ -8,6 +8,7 @@ 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.TaskSuggestionsResponse
import com.tt.honeyDue.models.TaskTemplate
import com.tt.honeyDue.models.VerifyEmailRequest
import com.tt.honeyDue.network.ApiResult
@@ -37,6 +38,7 @@ enum class OnboardingStep {
VERIFY_EMAIL,
JOIN_RESIDENCE,
RESIDENCE_LOCATION,
HOME_PROFILE,
FIRST_TASK,
SUBSCRIPTION_UPSELL
}
@@ -90,6 +92,53 @@ class OnboardingViewModel : ViewModel() {
private val _postalCode = MutableStateFlow("")
val postalCode: StateFlow<String> = _postalCode
// Home profile fields
private val _heatingType = MutableStateFlow<String?>(null)
val heatingType: StateFlow<String?> = _heatingType
private val _coolingType = MutableStateFlow<String?>(null)
val coolingType: StateFlow<String?> = _coolingType
private val _waterHeaterType = MutableStateFlow<String?>(null)
val waterHeaterType: StateFlow<String?> = _waterHeaterType
private val _roofType = MutableStateFlow<String?>(null)
val roofType: StateFlow<String?> = _roofType
private val _hasPool = MutableStateFlow(false)
val hasPool: StateFlow<Boolean> = _hasPool
private val _hasSprinklerSystem = MutableStateFlow(false)
val hasSprinklerSystem: StateFlow<Boolean> = _hasSprinklerSystem
private val _hasSeptic = MutableStateFlow(false)
val hasSeptic: StateFlow<Boolean> = _hasSeptic
private val _hasFireplace = MutableStateFlow(false)
val hasFireplace: StateFlow<Boolean> = _hasFireplace
private val _hasGarage = MutableStateFlow(false)
val hasGarage: StateFlow<Boolean> = _hasGarage
private val _hasBasement = MutableStateFlow(false)
val hasBasement: StateFlow<Boolean> = _hasBasement
private val _hasAttic = MutableStateFlow(false)
val hasAttic: StateFlow<Boolean> = _hasAttic
private val _exteriorType = MutableStateFlow<String?>(null)
val exteriorType: StateFlow<String?> = _exteriorType
private val _flooringPrimary = MutableStateFlow<String?>(null)
val flooringPrimary: StateFlow<String?> = _flooringPrimary
private val _landscapingType = MutableStateFlow<String?>(null)
val landscapingType: StateFlow<String?> = _landscapingType
// Task suggestions state
private val _suggestionsState = MutableStateFlow<ApiResult<TaskSuggestionsResponse>>(ApiResult.Idle)
val suggestionsState: StateFlow<ApiResult<TaskSuggestionsResponse>> = _suggestionsState
// Whether onboarding is complete
private val _isComplete = MutableStateFlow(false)
val isComplete: StateFlow<Boolean> = _isComplete
@@ -106,6 +155,32 @@ class OnboardingViewModel : ViewModel() {
_shareCode.value = code
}
// Home profile setters
fun setHeatingType(value: String?) { _heatingType.value = value }
fun setCoolingType(value: String?) { _coolingType.value = value }
fun setWaterHeaterType(value: String?) { _waterHeaterType.value = value }
fun setRoofType(value: String?) { _roofType.value = value }
fun setHasPool(value: Boolean) { _hasPool.value = value }
fun setHasSprinklerSystem(value: Boolean) { _hasSprinklerSystem.value = value }
fun setHasSeptic(value: Boolean) { _hasSeptic.value = value }
fun setHasFireplace(value: Boolean) { _hasFireplace.value = value }
fun setHasGarage(value: Boolean) { _hasGarage.value = value }
fun setHasBasement(value: Boolean) { _hasBasement.value = value }
fun setHasAttic(value: Boolean) { _hasAttic.value = value }
fun setExteriorType(value: String?) { _exteriorType.value = value }
fun setFlooringPrimary(value: String?) { _flooringPrimary.value = value }
fun setLandscapingType(value: String?) { _landscapingType.value = value }
/**
* Load personalized task suggestions for the given residence.
*/
fun loadSuggestions(residenceId: Int) {
viewModelScope.launch {
_suggestionsState.value = ApiResult.Loading
_suggestionsState.value = APILayer.getTaskSuggestions(residenceId)
}
}
/**
* Move to the next step in the flow
* Flow: Welcome → Features → Name Residence → Create Account → Verify → Tasks → Upsell
@@ -129,9 +204,16 @@ class OnboardingViewModel : ViewModel() {
OnboardingStep.RESIDENCE_LOCATION
}
}
OnboardingStep.JOIN_RESIDENCE -> OnboardingStep.SUBSCRIPTION_UPSELL
OnboardingStep.RESIDENCE_LOCATION -> OnboardingStep.FIRST_TASK
OnboardingStep.FIRST_TASK -> OnboardingStep.SUBSCRIPTION_UPSELL
OnboardingStep.JOIN_RESIDENCE -> {
completeOnboarding()
OnboardingStep.JOIN_RESIDENCE
}
OnboardingStep.RESIDENCE_LOCATION -> OnboardingStep.HOME_PROFILE
OnboardingStep.HOME_PROFILE -> OnboardingStep.FIRST_TASK
OnboardingStep.FIRST_TASK -> {
completeOnboarding()
OnboardingStep.FIRST_TASK
}
OnboardingStep.SUBSCRIPTION_UPSELL -> {
completeOnboarding()
OnboardingStep.SUBSCRIPTION_UPSELL
@@ -171,9 +253,10 @@ class OnboardingViewModel : ViewModel() {
fun skipStep() {
when (_currentStep.value) {
OnboardingStep.VALUE_PROPS,
OnboardingStep.JOIN_RESIDENCE,
OnboardingStep.RESIDENCE_LOCATION,
OnboardingStep.FIRST_TASK -> nextStep()
OnboardingStep.HOME_PROFILE -> nextStep()
OnboardingStep.JOIN_RESIDENCE,
OnboardingStep.FIRST_TASK,
OnboardingStep.SUBSCRIPTION_UPSELL -> completeOnboarding()
else -> {}
}
@@ -272,7 +355,21 @@ class OnboardingViewModel : ViewModel() {
description = null,
purchaseDate = null,
purchasePrice = null,
isPrimary = true
isPrimary = true,
heatingType = _heatingType.value,
coolingType = _coolingType.value,
waterHeaterType = _waterHeaterType.value,
roofType = _roofType.value,
hasPool = _hasPool.value.takeIf { it },
hasSprinklerSystem = _hasSprinklerSystem.value.takeIf { it },
hasSeptic = _hasSeptic.value.takeIf { it },
hasFireplace = _hasFireplace.value.takeIf { it },
hasGarage = _hasGarage.value.takeIf { it },
hasBasement = _hasBasement.value.takeIf { it },
hasAttic = _hasAttic.value.takeIf { it },
exteriorType = _exteriorType.value,
flooringPrimary = _flooringPrimary.value,
landscapingType = _landscapingType.value
)
)
@@ -362,6 +459,21 @@ class OnboardingViewModel : ViewModel() {
_createTasksState.value = ApiResult.Idle
_regionalTemplates.value = ApiResult.Idle
_postalCode.value = ""
_heatingType.value = null
_coolingType.value = null
_waterHeaterType.value = null
_roofType.value = null
_hasPool.value = false
_hasSprinklerSystem.value = false
_hasSeptic.value = false
_hasFireplace.value = false
_hasGarage.value = false
_hasBasement.value = false
_hasAttic.value = false
_exteriorType.value = null
_flooringPrimary.value = null
_landscapingType.value = null
_suggestionsState.value = ApiResult.Idle
_isComplete.value = false
}