Remove ZIP code step from onboarding, use home profile instead

ZIP code was US-only and redundant now that the suggestion engine
uses home profile features (heating, pool, etc.) for personalization.

Onboarding flow: Welcome → Value Props → Name → Account → Verify →
Home Profile → Task Selection (was: ...Verify → ZIP → Home Profile...)

Removed regionalTemplates references from task selection view.
Both iOS and Compose flows updated.
This commit is contained in:
Trey T
2026-03-30 11:15:06 -05:00
parent 4609d5a953
commit 266d540d28
5 changed files with 41 additions and 97 deletions

View File

@@ -64,8 +64,8 @@ struct OnboardingCoordinator: View {
return
}
let postalCode = onboardingState.pendingPostalCode.isEmpty ? nil : onboardingState.pendingPostalCode
print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName), zip: \(postalCode ?? "none")")
let postalCode: String? = nil
print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName)")
isCreatingResidence = true
@@ -126,7 +126,7 @@ struct OnboardingCoordinator: View {
}
/// Current step index for progress indicator (0-based)
/// Flow: Welcome Features Name Account Verify Location Home Profile Tasks Upsell
/// Flow: Welcome Features Name Account Verify Home Profile Tasks Upsell
private var currentProgressStep: Int {
switch onboardingState.currentStep {
case .welcome: return 0
@@ -155,7 +155,7 @@ struct OnboardingCoordinator: View {
/// Whether to show the skip button
private var showSkipButton: Bool {
switch onboardingState.currentStep {
case .valueProps, .joinResidence, .residenceLocation, .homeProfile, .firstTask, .subscriptionUpsell:
case .valueProps, .joinResidence, .homeProfile, .firstTask, .subscriptionUpsell:
return true
default:
return false
@@ -197,9 +197,6 @@ struct OnboardingCoordinator: View {
switch onboardingState.currentStep {
case .valueProps:
goForward()
case .residenceLocation:
// Skipping location go to home profile
goForward(to: .homeProfile)
case .homeProfile:
// Skipping home profile create residence without profile data, go to tasks
createResidenceIfNeeded(thenNavigateTo: .firstTask)
@@ -304,7 +301,7 @@ struct OnboardingCoordinator: View {
if onboardingState.userIntent == .joinExisting {
goForward(to: .joinResidence)
} else {
goForward(to: .residenceLocation)
goForward(to: .homeProfile)
}
} else {
goForward()
@@ -320,7 +317,7 @@ struct OnboardingCoordinator: View {
if onboardingState.userIntent == .joinExisting {
goForward(to: .joinResidence)
} else {
goForward(to: .residenceLocation)
goForward(to: .homeProfile)
}
}
)
@@ -336,18 +333,10 @@ struct OnboardingCoordinator: View {
.transition(navigationTransition)
case .residenceLocation:
OnboardingLocationContent(
onLocationDetected: { zip in
// Load regional templates in background
onboardingState.loadRegionalTemplates(zip: zip)
// Go to home profile step (residence created after profile)
goForward()
},
onSkip: {
// Handled by handleSkip() above
}
)
.transition(navigationTransition)
// Location step removed skip to home profile if we land here
EmptyView()
.onAppear { goForward(to: .homeProfile) }
.transition(navigationTransition)
case .homeProfile:
OnboardingHomeProfileContent(

View File

@@ -53,12 +53,9 @@ struct OnboardingFirstTaskContent: View {
/// Cached categories computed once and stored to preserve stable UUIDs
@State private var taskCategoriesCache: [OnboardingTaskCategory]? = nil
/// Uses API-driven regional templates when available, falls back to hardcoded defaults
/// Task categories for the Browse tab
private var taskCategories: [OnboardingTaskCategory] {
if let cached = taskCategoriesCache { return cached }
if !onboardingState.regionalTemplates.isEmpty {
return categoriesFromAPI(onboardingState.regionalTemplates)
}
return fallbackCategories
}
@@ -297,9 +294,7 @@ struct OnboardingFirstTaskContent: View {
.foregroundColor(Color.appTextPrimary)
.a11yHeader()
Text(onboardingState.regionalTemplates.isEmpty
? "Let's get you started with some tasks.\nThe more you pick, the more we'll help you remember!"
: "Here are tasks recommended for your area.\nPick the ones you'd like to track!")
Text("Let's get you started with some tasks.\nThe more you pick, the more we'll help you remember!")
.font(.system(size: 15, weight: .medium))
.foregroundColor(Color.appTextSecondary)
.multilineTextAlignment(.center)
@@ -465,11 +460,7 @@ struct OnboardingFirstTaskContent: View {
isAnimating = true
// Build and cache categories once to preserve stable UUIDs
if taskCategoriesCache == nil {
if !onboardingState.regionalTemplates.isEmpty {
taskCategoriesCache = categoriesFromAPI(onboardingState.regionalTemplates)
} else {
taskCategoriesCache = fallbackCategories
}
taskCategoriesCache = fallbackCategories
}
// Expand first category by default
if let first = taskCategories.first?.name {
@@ -485,23 +476,15 @@ struct OnboardingFirstTaskContent: View {
private func selectPopularTasks() {
withAnimation(.spring(response: 0.3)) {
if !onboardingState.regionalTemplates.isEmpty {
// API templates: select the first tasks (they're ordered by display_order)
for task in allTasks {
selectedTasks.insert(task.id)
}
} else {
// Fallback: select hardcoded popular tasks
let popularTaskTitles = [
"Change HVAC Filter",
"Test Smoke Detectors",
"Check for Leaks",
"Clean Gutters",
"Clean Refrigerator Coils"
]
for task in allTasks where popularTaskTitles.contains(task.title) {
selectedTasks.insert(task.id)
}
let popularTaskTitles = [
"Change HVAC Filter",
"Test Smoke Detectors",
"Check for Leaks",
"Clean Gutters",
"Clean Refrigerator Coils"
]
for task in allTasks where popularTaskTitles.contains(task.title) {
selectedTasks.insert(task.id)
}
}
}

View File

@@ -36,15 +36,6 @@ class OnboardingState: ObservableObject {
/// The ID of the residence created during onboarding (used for task creation)
@Published var createdResidenceId: Int32? = nil
/// ZIP code entered during the location step (used for residence creation and regional templates)
@Published var pendingPostalCode: String = ""
/// Regional task templates loaded from API based on ZIP code
@Published var regionalTemplates: [TaskTemplate] = []
/// Whether regional templates are currently loading
@Published var isLoadingTemplates: Bool = false
// MARK: - Home Profile State (collected during onboarding)
@Published var pendingHeatingType: String? = nil
@@ -80,20 +71,6 @@ class OnboardingState: ObservableObject {
private init() {}
/// Load regional task templates from the backend for the given ZIP code
func loadRegionalTemplates(zip: String) {
pendingPostalCode = zip
isLoadingTemplates = true
Task {
defer { self.isLoadingTemplates = false }
let result = try await APILayer.shared.getRegionalTemplates(state: nil, zip: zip)
if let success = result as? ApiResultSuccess<NSArray>,
let templates = success.data as? [TaskTemplate] {
self.regionalTemplates = templates
}
}
}
/// Start the onboarding flow
func startOnboarding() {
isOnboardingActive = true
@@ -102,7 +79,7 @@ class OnboardingState: ObservableObject {
}
/// Move to the next step in the flow
/// Order: Welcome Features Name Account Verify Location Tasks Upsell
/// Order: Welcome Features Name Account Verify Home Profile Tasks Upsell
func nextStep() {
switch currentStep {
case .welcome:
@@ -121,11 +98,12 @@ class OnboardingState: ObservableObject {
if userIntent == .joinExisting {
currentStep = .joinResidence
} else {
currentStep = .residenceLocation
currentStep = .homeProfile
}
case .joinResidence:
completeOnboarding()
case .residenceLocation:
// Skip past this step if we somehow land here
currentStep = .homeProfile
case .homeProfile:
currentStep = .firstTask
@@ -152,8 +130,6 @@ class OnboardingState: ObservableObject {
hasCompletedOnboarding = true
isOnboardingActive = false
pendingResidenceName = ""
pendingPostalCode = ""
regionalTemplates = []
createdResidenceId = nil
userIntent = .unknown
resetHomeProfile()
@@ -165,8 +141,6 @@ class OnboardingState: ObservableObject {
hasCompletedOnboarding = false
isOnboardingActive = false
pendingResidenceName = ""
pendingPostalCode = ""
regionalTemplates = []
createdResidenceId = nil
userIntent = .unknown
currentStep = .welcome