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
@@ -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"
)
}
@@ -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
)
/**
@@ -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
)