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

View File

@@ -0,0 +1,59 @@
package com.tt.honeyDue.models
/**
* Static option lists for home profile pickers.
* Each entry is a (apiValue, displayLabel) pair.
*/
object HomeProfileOptions {
val heatingTypes = listOf(
"gas_furnace" to "Gas Furnace",
"electric" to "Electric",
"heat_pump" to "Heat Pump",
"boiler" to "Boiler",
"radiant" to "Radiant",
"wood_stove" to "Wood Stove",
"none" to "None"
)
val coolingTypes = listOf(
"central_ac" to "Central AC",
"window_unit" to "Window Unit",
"mini_split" to "Mini Split",
"evaporative" to "Evaporative",
"none" to "None"
)
val waterHeaterTypes = listOf(
"tank_gas" to "Tank (Gas)",
"tank_electric" to "Tank (Electric)",
"tankless" to "Tankless",
"solar" to "Solar",
"heat_pump_wh" to "Heat Pump"
)
val roofTypes = listOf(
"asphalt_shingle" to "Asphalt Shingle",
"metal" to "Metal",
"tile" to "Tile",
"flat_tpo" to "Flat/TPO",
"slate" to "Slate",
"wood_shake" to "Wood Shake"
)
val exteriorTypes = listOf(
"vinyl_siding" to "Vinyl Siding",
"brick" to "Brick",
"stucco" to "Stucco",
"wood" to "Wood",
"stone" to "Stone",
"fiber_cement" to "Fiber Cement"
)
val flooringTypes = listOf(
"hardwood" to "Hardwood",
"carpet" to "Carpet",
"tile" to "Tile",
"laminate" to "Laminate",
"vinyl" to "Vinyl"
)
val landscapingTypes = listOf(
"lawn" to "Lawn",
"xeriscaping" to "Xeriscaping",
"none" to "None"
)
}

View File

@@ -53,6 +53,20 @@ data class ResidenceResponse(
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("overdue_count") val overdueCount: Int = 0,
@SerialName("completion_summary") val completionSummary: CompletionSummary? = null,
@SerialName("heating_type") val heatingType: String? = null,
@SerialName("cooling_type") val coolingType: String? = null,
@SerialName("water_heater_type") val waterHeaterType: String? = null,
@SerialName("roof_type") val roofType: String? = null,
@SerialName("has_pool") val hasPool: Boolean = false,
@SerialName("has_sprinkler_system") val hasSprinklerSystem: Boolean = false,
@SerialName("has_septic") val hasSeptic: Boolean = false,
@SerialName("has_fireplace") val hasFireplace: Boolean = false,
@SerialName("has_garage") val hasGarage: Boolean = false,
@SerialName("has_basement") val hasBasement: Boolean = false,
@SerialName("has_attic") val hasAttic: Boolean = false,
@SerialName("exterior_type") val exteriorType: String? = null,
@SerialName("flooring_primary") val flooringPrimary: String? = null,
@SerialName("landscaping_type") val landscapingType: String? = null,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
) {
@@ -94,7 +108,21 @@ data class ResidenceCreateRequest(
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean? = null
@SerialName("is_primary") val isPrimary: Boolean? = null,
@SerialName("heating_type") val heatingType: String? = null,
@SerialName("cooling_type") val coolingType: String? = null,
@SerialName("water_heater_type") val waterHeaterType: String? = null,
@SerialName("roof_type") val roofType: String? = null,
@SerialName("has_pool") val hasPool: Boolean? = null,
@SerialName("has_sprinkler_system") val hasSprinklerSystem: Boolean? = null,
@SerialName("has_septic") val hasSeptic: Boolean? = null,
@SerialName("has_fireplace") val hasFireplace: Boolean? = null,
@SerialName("has_garage") val hasGarage: Boolean? = null,
@SerialName("has_basement") val hasBasement: Boolean? = null,
@SerialName("has_attic") val hasAttic: Boolean? = null,
@SerialName("exterior_type") val exteriorType: String? = null,
@SerialName("flooring_primary") val flooringPrimary: String? = null,
@SerialName("landscaping_type") val landscapingType: String? = null
)
/**
@@ -118,7 +146,21 @@ data class ResidenceUpdateRequest(
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean? = null
@SerialName("is_primary") val isPrimary: Boolean? = null,
@SerialName("heating_type") val heatingType: String? = null,
@SerialName("cooling_type") val coolingType: String? = null,
@SerialName("water_heater_type") val waterHeaterType: String? = null,
@SerialName("roof_type") val roofType: String? = null,
@SerialName("has_pool") val hasPool: Boolean? = null,
@SerialName("has_sprinkler_system") val hasSprinklerSystem: Boolean? = null,
@SerialName("has_septic") val hasSeptic: Boolean? = null,
@SerialName("has_fireplace") val hasFireplace: Boolean? = null,
@SerialName("has_garage") val hasGarage: Boolean? = null,
@SerialName("has_basement") val hasBasement: Boolean? = null,
@SerialName("has_attic") val hasAttic: Boolean? = null,
@SerialName("exterior_type") val exteriorType: String? = null,
@SerialName("flooring_primary") val flooringPrimary: String? = null,
@SerialName("landscaping_type") val landscapingType: String? = null
)
/**

View File

@@ -0,0 +1,24 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* A single task suggestion with relevance scoring from the backend.
*/
@Serializable
data class TaskSuggestionResponse(
val template: TaskTemplate,
@SerialName("relevance_score") val relevanceScore: Double,
@SerialName("match_reasons") val matchReasons: List<String>
)
/**
* Response wrapper for task suggestions endpoint.
*/
@Serializable
data class TaskSuggestionsResponse(
val suggestions: List<TaskSuggestionResponse>,
@SerialName("total_count") val totalCount: Int,
@SerialName("profile_completeness") val profileCompleteness: Double
)

View File

@@ -1209,6 +1209,14 @@ object APILayer {
return taskTemplateApi.getTemplatesByRegion(state = state, zip = zip)
}
/**
* Get personalized task suggestions for a residence based on its home profile.
*/
suspend fun getTaskSuggestions(residenceId: Int): ApiResult<TaskSuggestionsResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
return taskTemplateApi.getTaskSuggestions(token, residenceId)
}
// ==================== Auth Operations ====================
suspend fun login(request: LoginRequest): ApiResult<AuthResponse> {

View File

@@ -1,6 +1,7 @@
package com.tt.honeyDue.network
import com.tt.honeyDue.models.TaskTemplate
import com.tt.honeyDue.models.TaskSuggestionsResponse
import com.tt.honeyDue.models.TaskTemplatesGroupedResponse
import io.ktor.client.*
import io.ktor.client.call.*
@@ -105,6 +106,27 @@ class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
/**
* Get personalized task suggestions for a residence based on its home profile.
* Requires authentication.
*/
suspend fun getTaskSuggestions(token: String, residenceId: Int): ApiResult<TaskSuggestionsResponse> {
return try {
val response = client.get("$baseUrl/tasks/suggestions/") {
header("Authorization", "Token $token")
parameter("residence_id", residenceId)
}
if (response.status.isSuccess()) {
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to fetch task suggestions", response.status.value)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Unknown error occurred")
}
}
/**
* Get a single template by ID
*/

View File

@@ -23,6 +23,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.models.TaskCreateRequest
import com.tt.honeyDue.models.TaskSuggestionResponse
import com.tt.honeyDue.models.TaskSuggestionsResponse
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.ui.theme.*
import com.tt.honeyDue.viewmodel.OnboardingViewModel
@@ -54,12 +56,22 @@ fun OnboardingFirstTaskContent(
viewModel: OnboardingViewModel,
onTasksAdded: () -> Unit
) {
val maxTasksAllowed = 5
var selectedTaskIds by remember { mutableStateOf(setOf<String>()) }
var selectedBrowseIds by remember { mutableStateOf(setOf<String>()) }
var selectedSuggestionIds by remember { mutableStateOf(setOf<Int>()) }
var expandedCategoryId by remember { mutableStateOf<String?>(null) }
var isCreatingTasks by remember { mutableStateOf(false) }
var selectedTabIndex by remember { mutableStateOf(0) }
val createTasksState by viewModel.createTasksState.collectAsState()
val suggestionsState by viewModel.suggestionsState.collectAsState()
// Load suggestions on mount if a residence exists
LaunchedEffect(Unit) {
val residence = DataManager.residences.value.firstOrNull()
if (residence != null) {
viewModel.loadSuggestions(residence.id)
}
}
LaunchedEffect(createTasksState) {
when (createTasksState) {
@@ -69,7 +81,6 @@ fun OnboardingFirstTaskContent(
}
is ApiResult.Error -> {
isCreatingTasks = false
// Still proceed even if task creation fails
onTasksAdded()
}
is ApiResult.Loading -> {
@@ -148,159 +159,178 @@ fun OnboardingFirstTaskContent(
)
)
val allTasks = taskCategories.flatMap { it.tasks }
val selectedCount = selectedTaskIds.size
val isAtMaxSelection = selectedCount >= maxTasksAllowed
val allBrowseTasks = taskCategories.flatMap { it.tasks }
val totalSelectedCount = selectedBrowseIds.size + selectedSuggestionIds.size
val isAtMaxSelection = false // No task selection limit
// Set first category expanded by default
LaunchedEffect(Unit) {
expandedCategoryId = taskCategories.firstOrNull()?.id
}
// Determine if suggestions are available
val hasSuggestions = suggestionsState is ApiResult.Success &&
(suggestionsState as? ApiResult.Success<TaskSuggestionsResponse>)?.data?.suggestions?.isNotEmpty() == true
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(
// Header (shared across tabs)
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Header
item {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
OrganicIconContainer(
icon = Icons.Default.Celebration,
size = 80.dp,
iconSize = 40.dp,
gradientColors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary
),
contentDescription = null
)
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Text(
text = stringResource(Res.string.onboarding_tasks_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_tasks_subtitle),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Selection counter
Surface(
shape = RoundedCornerShape(OrganicRadius.xl),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
) {
Row(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
// Celebration icon using OrganicIconContainer
OrganicIconContainer(
icon = Icons.Default.Celebration,
size = 80.dp,
iconSize = 40.dp,
gradientColors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary
),
contentDescription = null
Icon(
imageVector = Icons.Default.CheckCircleOutline,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Text(
text = stringResource(Res.string.onboarding_tasks_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_tasks_subtitle),
text = "$totalSelectedCount task${if (totalSelectedCount == 1) "" else "s"} selected",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Selection counter
Surface(
shape = RoundedCornerShape(OrganicRadius.xl),
color = if (isAtMaxSelection) {
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f)
} else {
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
}
) {
Row(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isAtMaxSelection) Icons.Default.CheckCircle else Icons.Default.CheckCircleOutline,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = if (isAtMaxSelection) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary
)
Text(
text = "$selectedCount/$maxTasksAllowed tasks selected",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = if (isAtMaxSelection) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary
)
}
}
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
}
// Task categories
items(taskCategories) { category ->
TaskCategorySection(
category = category,
selectedTaskIds = selectedTaskIds,
isExpanded = expandedCategoryId == category.id,
isAtMaxSelection = isAtMaxSelection,
onToggleExpand = {
expandedCategoryId = if (expandedCategoryId == category.id) null else category.id
},
onToggleTask = { taskId ->
selectedTaskIds = if (taskId in selectedTaskIds) {
selectedTaskIds - taskId
} else if (!isAtMaxSelection) {
selectedTaskIds + taskId
} else {
selectedTaskIds
}
}
)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
// Add popular tasks button
item {
OutlinedButton(
onClick = {
val popularTitles = listOf(
"Change HVAC Filter",
"Test Smoke Detectors",
"Check for Leaks",
"Clean Gutters",
"Clean Refrigerator Coils"
)
val popularIds = allTasks
.filter { it.title in popularTitles }
.take(maxTasksAllowed)
.map { it.id }
.toSet()
selectedTaskIds = popularIds
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(OrganicRadius.lg),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.tertiary
)
)
)
) {
Icon(Icons.Default.AutoAwesome, contentDescription = null)
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_tasks_add_popular),
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(100.dp)) // Space for bottom button
}
}
// Bottom action area
// Tab row (only show if we have suggestions)
if (hasSuggestions || suggestionsState is ApiResult.Loading) {
TabRow(
selectedTabIndex = selectedTabIndex,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary,
modifier = Modifier.fillMaxWidth()
) {
Tab(
selected = selectedTabIndex == 0,
onClick = { selectedTabIndex = 0 },
text = {
Text(
text = stringResource(Res.string.for_you_tab),
fontWeight = if (selectedTabIndex == 0) FontWeight.SemiBold else FontWeight.Normal
)
},
icon = {
Icon(
Icons.Default.AutoAwesome,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
)
Tab(
selected = selectedTabIndex == 1,
onClick = { selectedTabIndex = 1 },
text = {
Text(
text = stringResource(Res.string.browse_tab),
fontWeight = if (selectedTabIndex == 1) FontWeight.SemiBold else FontWeight.Normal
)
},
icon = {
Icon(
Icons.Default.ViewList,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
)
}
}
// Tab content
when {
(hasSuggestions || suggestionsState is ApiResult.Loading) && selectedTabIndex == 0 -> {
// For You tab
ForYouTabContent(
suggestionsState = suggestionsState,
selectedSuggestionIds = selectedSuggestionIds,
isAtMaxSelection = isAtMaxSelection,
onToggleSuggestion = { templateId ->
selectedSuggestionIds = if (templateId in selectedSuggestionIds) {
selectedSuggestionIds - templateId
} else if (!isAtMaxSelection) {
selectedSuggestionIds + templateId
} else {
selectedSuggestionIds
}
},
modifier = Modifier.weight(1f)
)
}
else -> {
// Browse tab (or default when no suggestions)
BrowseTabContent(
taskCategories = taskCategories,
allTasks = allBrowseTasks,
selectedTaskIds = selectedBrowseIds,
expandedCategoryId = expandedCategoryId,
isAtMaxSelection = isAtMaxSelection,
onToggleExpand = { catId ->
expandedCategoryId = if (expandedCategoryId == catId) null else catId
},
onToggleTask = { taskId ->
selectedBrowseIds = if (taskId in selectedBrowseIds) {
selectedBrowseIds - taskId
} else if (!isAtMaxSelection) {
selectedBrowseIds + taskId
} else {
selectedBrowseIds
}
},
onAddPopular = { popularIds ->
selectedBrowseIds = popularIds
},
modifier = Modifier.weight(1f)
)
}
}
// Bottom action area (shared)
Surface(
modifier = Modifier.fillMaxWidth(),
shadowElevation = 8.dp
@@ -309,30 +339,30 @@ fun OnboardingFirstTaskContent(
modifier = Modifier.padding(OrganicSpacing.lg)
) {
OrganicPrimaryButton(
text = if (selectedCount > 0) {
"Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue"
text = if (totalSelectedCount > 0) {
"Add $totalSelectedCount Task${if (totalSelectedCount == 1) "" else "s"} & Continue"
} else {
stringResource(Res.string.onboarding_tasks_skip)
},
onClick = {
if (selectedTaskIds.isEmpty()) {
if (selectedBrowseIds.isEmpty() && selectedSuggestionIds.isEmpty()) {
onTasksAdded()
} else {
val residences = DataManager.residences.value
val residence = residences.firstOrNull()
if (residence != null) {
val today = DateUtils.getTodayString()
val taskRequests = mutableListOf<TaskCreateRequest>()
val selectedTemplates = allTasks.filter { it.id in selectedTaskIds }
val taskRequests = selectedTemplates.map { template ->
// Browse tab selections
val selectedBrowseTemplates = allBrowseTasks.filter { it.id in selectedBrowseIds }
taskRequests.addAll(selectedBrowseTemplates.map { template ->
val categoryId = DataManager.taskCategories.value
.find { cat -> cat.name.lowercase() == template.category.lowercase() }
?.id
val frequencyId = DataManager.taskFrequencies.value
.find { freq -> freq.name.lowercase() == template.frequency.lowercase() }
?.id
TaskCreateRequest(
residenceId = residence.id,
title = template.title,
@@ -346,7 +376,29 @@ fun OnboardingFirstTaskContent(
estimatedCost = null,
contractorId = null
)
})
// For You tab selections
val suggestions = (suggestionsState as? ApiResult.Success<TaskSuggestionsResponse>)?.data?.suggestions
suggestions?.filter { it.template.id in selectedSuggestionIds }?.forEach { suggestion ->
val tmpl = suggestion.template
taskRequests.add(
TaskCreateRequest(
residenceId = residence.id,
title = tmpl.title,
description = tmpl.description.takeIf { it.isNotBlank() },
categoryId = tmpl.categoryId,
priorityId = null,
inProgress = false,
frequencyId = tmpl.frequencyId,
assignedToId = null,
dueDate = today,
estimatedCost = null,
contractorId = null
)
)
}
viewModel.createTasks(taskRequests)
} else {
onTasksAdded()
@@ -363,6 +415,237 @@ fun OnboardingFirstTaskContent(
}
}
// ==================== For You Tab ====================
@Composable
private fun ForYouTabContent(
suggestionsState: ApiResult<TaskSuggestionsResponse>,
selectedSuggestionIds: Set<Int>,
isAtMaxSelection: Boolean,
onToggleSuggestion: (Int) -> Unit,
modifier: Modifier = Modifier
) {
when (suggestionsState) {
is ApiResult.Loading -> {
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Text(
text = "Finding tasks for your home...",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
is ApiResult.Success -> {
val suggestions = suggestionsState.data.suggestions
LazyColumn(
modifier = modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
) {
items(suggestions) { suggestion ->
SuggestionRow(
suggestion = suggestion,
isSelected = suggestion.template.id in selectedSuggestionIds,
isDisabled = isAtMaxSelection && suggestion.template.id !in selectedSuggestionIds,
onToggle = { onToggleSuggestion(suggestion.template.id) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
}
item {
Spacer(modifier = Modifier.height(24.dp))
}
}
}
is ApiResult.Error -> {
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = "Could not load suggestions. Try the Browse tab.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
}
else -> {}
}
}
@Composable
private fun SuggestionRow(
suggestion: TaskSuggestionResponse,
isSelected: Boolean,
isDisabled: Boolean,
onToggle: () -> Unit
) {
val template = suggestion.template
val relevancePercent = (suggestion.relevanceScore * 100).toInt()
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = !isDisabled) { onToggle() },
accentColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
showBlob = false
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically
) {
// Checkbox
Box(
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
.background(
if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outline.copy(alpha = if (isDisabled) 0.15f else 0.3f)
),
contentAlignment = Alignment.Center
) {
if (isSelected) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
Spacer(modifier = Modifier.width(OrganicSpacing.md))
Column(modifier = Modifier.weight(1f)) {
Text(
text = template.title,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = if (isDisabled) {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
} else {
MaterialTheme.colorScheme.onSurface
}
)
Text(
text = template.frequencyDisplay,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = if (isDisabled) 0.5f else 1f
)
)
if (suggestion.matchReasons.isNotEmpty()) {
Text(
text = suggestion.matchReasons.first(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
)
}
}
// Relevance indicator
Surface(
shape = RoundedCornerShape(OrganicRadius.lg),
color = MaterialTheme.colorScheme.primary.copy(
alpha = (suggestion.relevanceScore * 0.2f).toFloat().coerceIn(0.05f, 0.2f)
)
) {
Text(
text = "$relevancePercent%",
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = OrganicSpacing.xs),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
// ==================== Browse Tab ====================
@Composable
private fun BrowseTabContent(
taskCategories: List<OnboardingTaskCategory>,
allTasks: List<OnboardingTaskTemplate>,
selectedTaskIds: Set<String>,
expandedCategoryId: String?,
isAtMaxSelection: Boolean,
onToggleExpand: (String) -> Unit,
onToggleTask: (String) -> Unit,
onAddPopular: (Set<String>) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
) {
// Task categories
items(taskCategories) { category ->
TaskCategorySection(
category = category,
selectedTaskIds = selectedTaskIds,
isExpanded = expandedCategoryId == category.id,
isAtMaxSelection = isAtMaxSelection,
onToggleExpand = { onToggleExpand(category.id) },
onToggleTask = onToggleTask
)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
// Add popular tasks button
item {
OutlinedButton(
onClick = {
val popularTitles = listOf(
"Change HVAC Filter",
"Test Smoke Detectors",
"Check for Leaks",
"Clean Gutters",
"Clean Refrigerator Coils"
)
val popularIds = allTasks
.filter { it.title in popularTitles }
.map { it.id }
.toSet()
onAddPopular(popularIds)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(OrganicRadius.lg),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.tertiary
)
)
)
) {
Icon(Icons.Default.AutoAwesome, contentDescription = null)
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_tasks_add_popular),
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
// ==================== Category / Row Components ====================
@Composable
private fun TaskCategorySection(
category: OnboardingTaskCategory,

View File

@@ -0,0 +1,378 @@
package com.tt.honeyDue.ui.screens.onboarding
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tt.honeyDue.models.HomeProfileOptions
import com.tt.honeyDue.ui.theme.*
import com.tt.honeyDue.viewmodel.OnboardingViewModel
import honeydue.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@Composable
fun OnboardingHomeProfileContent(
viewModel: OnboardingViewModel,
onContinue: () -> Unit,
onSkip: () -> Unit
) {
val heatingType by viewModel.heatingType.collectAsState()
val coolingType by viewModel.coolingType.collectAsState()
val waterHeaterType by viewModel.waterHeaterType.collectAsState()
val roofType by viewModel.roofType.collectAsState()
val hasPool by viewModel.hasPool.collectAsState()
val hasSprinklerSystem by viewModel.hasSprinklerSystem.collectAsState()
val hasSeptic by viewModel.hasSeptic.collectAsState()
val hasFireplace by viewModel.hasFireplace.collectAsState()
val hasGarage by viewModel.hasGarage.collectAsState()
val hasBasement by viewModel.hasBasement.collectAsState()
val hasAttic by viewModel.hasAttic.collectAsState()
val exteriorType by viewModel.exteriorType.collectAsState()
val flooringPrimary by viewModel.flooringPrimary.collectAsState()
val landscapingType by viewModel.landscapingType.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
) {
// Header
item {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
OrganicIconContainer(
icon = Icons.Default.Tune,
size = 80.dp,
iconSize = 40.dp,
gradientColors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.tertiary
),
contentDescription = null
)
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Text(
text = stringResource(Res.string.onboarding_home_profile_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_home_profile_subtitle),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
}
// Systems section
item {
ProfileSectionHeader(
icon = Icons.Default.Settings,
title = stringResource(Res.string.onboarding_home_profile_systems)
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
}
item {
OptionDropdownChips(
label = "Heating",
options = HomeProfileOptions.heatingTypes,
selectedValue = heatingType,
onSelect = { viewModel.setHeatingType(it) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
item {
OptionDropdownChips(
label = "Cooling",
options = HomeProfileOptions.coolingTypes,
selectedValue = coolingType,
onSelect = { viewModel.setCoolingType(it) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
item {
OptionDropdownChips(
label = "Water Heater",
options = HomeProfileOptions.waterHeaterTypes,
selectedValue = waterHeaterType,
onSelect = { viewModel.setWaterHeaterType(it) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
// Features section
item {
ProfileSectionHeader(
icon = Icons.Default.Star,
title = stringResource(Res.string.onboarding_home_profile_features)
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
}
item {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
ToggleChip(label = "Pool", selected = hasPool, onToggle = { viewModel.setHasPool(!hasPool) })
ToggleChip(label = "Sprinkler System", selected = hasSprinklerSystem, onToggle = { viewModel.setHasSprinklerSystem(!hasSprinklerSystem) })
ToggleChip(label = "Fireplace", selected = hasFireplace, onToggle = { viewModel.setHasFireplace(!hasFireplace) })
ToggleChip(label = "Garage", selected = hasGarage, onToggle = { viewModel.setHasGarage(!hasGarage) })
ToggleChip(label = "Basement", selected = hasBasement, onToggle = { viewModel.setHasBasement(!hasBasement) })
ToggleChip(label = "Attic", selected = hasAttic, onToggle = { viewModel.setHasAttic(!hasAttic) })
ToggleChip(label = "Septic", selected = hasSeptic, onToggle = { viewModel.setHasSeptic(!hasSeptic) })
}
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
// Exterior section
item {
ProfileSectionHeader(
icon = Icons.Default.Roofing,
title = stringResource(Res.string.onboarding_home_profile_exterior)
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
}
item {
OptionDropdownChips(
label = "Roof Type",
options = HomeProfileOptions.roofTypes,
selectedValue = roofType,
onSelect = { viewModel.setRoofType(it) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
item {
OptionDropdownChips(
label = "Exterior",
options = HomeProfileOptions.exteriorTypes,
selectedValue = exteriorType,
onSelect = { viewModel.setExteriorType(it) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
// Interior section
item {
ProfileSectionHeader(
icon = Icons.Default.Weekend,
title = stringResource(Res.string.onboarding_home_profile_interior)
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
}
item {
OptionDropdownChips(
label = "Flooring",
options = HomeProfileOptions.flooringTypes,
selectedValue = flooringPrimary,
onSelect = { viewModel.setFlooringPrimary(it) }
)
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
item {
OptionDropdownChips(
label = "Landscaping",
options = HomeProfileOptions.landscapingTypes,
selectedValue = landscapingType,
onSelect = { viewModel.setLandscapingType(it) }
)
Spacer(modifier = Modifier.height(100.dp)) // Space for bottom button
}
}
// Bottom action area
Surface(
modifier = Modifier.fillMaxWidth(),
shadowElevation = 8.dp
) {
Column(
modifier = Modifier.padding(OrganicSpacing.lg)
) {
OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.ArrowForward
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
TextButton(
onClick = onSkip,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(Res.string.onboarding_skip),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@Composable
private fun ProfileSectionHeader(
icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onBackground
)
}
}
@Composable
private fun OptionDropdownChips(
label: String,
options: List<Pair<String, String>>,
selectedValue: String?,
onSelect: (String?) -> Unit
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(OrganicSpacing.xs))
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
options.forEach { (apiValue, displayLabel) ->
val isSelected = selectedValue == apiValue
FilterChip(
selected = isSelected,
onClick = {
onSelect(if (isSelected) null else apiValue)
},
label = {
Text(
text = displayLabel,
style = MaterialTheme.typography.bodySmall
)
},
shape = RoundedCornerShape(OrganicRadius.lg),
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
selectedLabelColor = MaterialTheme.colorScheme.primary
),
border = BorderStroke(
width = 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
)
)
}
}
}
}
@Composable
private fun ToggleChip(
label: String,
selected: Boolean,
onToggle: () -> Unit
) {
val containerColor by animateColorAsState(
targetValue = if (selected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
},
label = "toggleChipColor"
)
val contentColor by animateColorAsState(
targetValue = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
label = "toggleChipContentColor"
)
FilterChip(
selected = selected,
onClick = onToggle,
label = {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal
)
},
leadingIcon = if (selected) {
{
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
} else null,
shape = RoundedCornerShape(OrganicRadius.xl),
colors = FilterChipDefaults.filterChipColors(
containerColor = containerColor,
labelColor = contentColor,
iconColor = contentColor,
selectedContainerColor = containerColor,
selectedLabelColor = contentColor,
selectedLeadingIconColor = contentColor
),
border = FilterChipDefaults.filterChipBorder(
borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
selectedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
enabled = true,
selected = selected
)
)
}

View File

@@ -126,7 +126,7 @@ fun OnboardingScreen(
OnboardingStep.JOIN_RESIDENCE -> OnboardingJoinResidenceContent(
viewModel = viewModel,
onJoined = { viewModel.nextStep() }
onJoined = { viewModel.completeOnboarding() }
)
OnboardingStep.RESIDENCE_LOCATION -> OnboardingLocationContent(
@@ -138,15 +138,21 @@ fun OnboardingScreen(
onSkip = { viewModel.nextStep() }
)
OnboardingStep.FIRST_TASK -> OnboardingFirstTaskContent(
OnboardingStep.HOME_PROFILE -> OnboardingHomeProfileContent(
viewModel = viewModel,
onTasksAdded = { viewModel.nextStep() }
onContinue = { viewModel.nextStep() },
onSkip = { viewModel.skipStep() }
)
OnboardingStep.SUBSCRIPTION_UPSELL -> OnboardingSubscriptionContent(
onSubscribe = { viewModel.completeOnboarding() },
onSkip = { viewModel.completeOnboarding() }
OnboardingStep.FIRST_TASK -> OnboardingFirstTaskContent(
viewModel = viewModel,
onTasksAdded = { viewModel.completeOnboarding() }
)
OnboardingStep.SUBSCRIPTION_UPSELL -> {
// Subscription removed from onboarding — app is free
LaunchedEffect(Unit) { viewModel.completeOnboarding() }
}
}
}
}
@@ -164,6 +170,7 @@ private fun OnboardingNavigationBar(
OnboardingStep.WELCOME,
OnboardingStep.JOIN_RESIDENCE,
OnboardingStep.RESIDENCE_LOCATION,
OnboardingStep.HOME_PROFILE,
OnboardingStep.FIRST_TASK,
OnboardingStep.SUBSCRIPTION_UPSELL -> false
else -> true
@@ -173,6 +180,7 @@ private fun OnboardingNavigationBar(
OnboardingStep.VALUE_PROPS,
OnboardingStep.JOIN_RESIDENCE,
OnboardingStep.RESIDENCE_LOCATION,
OnboardingStep.HOME_PROFILE,
OnboardingStep.FIRST_TASK,
OnboardingStep.SUBSCRIPTION_UPSELL -> true
else -> false
@@ -182,6 +190,7 @@ private fun OnboardingNavigationBar(
OnboardingStep.WELCOME,
OnboardingStep.JOIN_RESIDENCE,
OnboardingStep.RESIDENCE_LOCATION,
OnboardingStep.HOME_PROFILE,
OnboardingStep.FIRST_TASK,
OnboardingStep.SUBSCRIPTION_UPSELL -> false
else -> true
@@ -195,6 +204,7 @@ private fun OnboardingNavigationBar(
OnboardingStep.VERIFY_EMAIL -> 4
OnboardingStep.JOIN_RESIDENCE -> 4
OnboardingStep.RESIDENCE_LOCATION -> 4
OnboardingStep.HOME_PROFILE -> 4
OnboardingStep.FIRST_TASK -> 4
OnboardingStep.SUBSCRIPTION_UPSELL -> 4
}

View File

@@ -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
}