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:
@@ -103,8 +103,7 @@ fun OnboardingScreen(
|
|||||||
if (userIntent == OnboardingIntent.JOIN_EXISTING) {
|
if (userIntent == OnboardingIntent.JOIN_EXISTING) {
|
||||||
viewModel.goToStep(OnboardingStep.JOIN_RESIDENCE)
|
viewModel.goToStep(OnboardingStep.JOIN_RESIDENCE)
|
||||||
} else {
|
} else {
|
||||||
viewModel.createResidence()
|
viewModel.goToStep(OnboardingStep.HOME_PROFILE)
|
||||||
viewModel.goToStep(OnboardingStep.RESIDENCE_LOCATION)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModel.nextStep()
|
viewModel.nextStep()
|
||||||
@@ -118,8 +117,7 @@ fun OnboardingScreen(
|
|||||||
if (userIntent == OnboardingIntent.JOIN_EXISTING) {
|
if (userIntent == OnboardingIntent.JOIN_EXISTING) {
|
||||||
viewModel.goToStep(OnboardingStep.JOIN_RESIDENCE)
|
viewModel.goToStep(OnboardingStep.JOIN_RESIDENCE)
|
||||||
} else {
|
} else {
|
||||||
viewModel.createResidence()
|
viewModel.goToStep(OnboardingStep.HOME_PROFILE)
|
||||||
viewModel.goToStep(OnboardingStep.RESIDENCE_LOCATION)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -129,19 +127,21 @@ fun OnboardingScreen(
|
|||||||
onJoined = { viewModel.completeOnboarding() }
|
onJoined = { viewModel.completeOnboarding() }
|
||||||
)
|
)
|
||||||
|
|
||||||
OnboardingStep.RESIDENCE_LOCATION -> OnboardingLocationContent(
|
OnboardingStep.RESIDENCE_LOCATION -> {
|
||||||
viewModel = viewModel,
|
// Location step removed — skip to home profile if we land here
|
||||||
onLocationDetected = { zip ->
|
LaunchedEffect(Unit) { viewModel.goToStep(OnboardingStep.HOME_PROFILE) }
|
||||||
viewModel.loadRegionalTemplates(zip)
|
}
|
||||||
viewModel.nextStep()
|
|
||||||
},
|
|
||||||
onSkip = { viewModel.nextStep() }
|
|
||||||
)
|
|
||||||
|
|
||||||
OnboardingStep.HOME_PROFILE -> OnboardingHomeProfileContent(
|
OnboardingStep.HOME_PROFILE -> OnboardingHomeProfileContent(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onContinue = { viewModel.nextStep() },
|
onContinue = {
|
||||||
onSkip = { viewModel.skipStep() }
|
viewModel.createResidence()
|
||||||
|
viewModel.nextStep()
|
||||||
|
},
|
||||||
|
onSkip = {
|
||||||
|
viewModel.createResidence()
|
||||||
|
viewModel.skipStep()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
OnboardingStep.FIRST_TASK -> OnboardingFirstTaskContent(
|
OnboardingStep.FIRST_TASK -> OnboardingFirstTaskContent(
|
||||||
@@ -179,7 +179,6 @@ private fun OnboardingNavigationBar(
|
|||||||
val showSkipButton = when (currentStep) {
|
val showSkipButton = when (currentStep) {
|
||||||
OnboardingStep.VALUE_PROPS,
|
OnboardingStep.VALUE_PROPS,
|
||||||
OnboardingStep.JOIN_RESIDENCE,
|
OnboardingStep.JOIN_RESIDENCE,
|
||||||
OnboardingStep.RESIDENCE_LOCATION,
|
|
||||||
OnboardingStep.HOME_PROFILE,
|
OnboardingStep.HOME_PROFILE,
|
||||||
OnboardingStep.FIRST_TASK,
|
OnboardingStep.FIRST_TASK,
|
||||||
OnboardingStep.SUBSCRIPTION_UPSELL -> true
|
OnboardingStep.SUBSCRIPTION_UPSELL -> true
|
||||||
|
|||||||
@@ -201,14 +201,14 @@ class OnboardingViewModel : ViewModel() {
|
|||||||
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
|
if (_userIntent.value == OnboardingIntent.JOIN_EXISTING) {
|
||||||
OnboardingStep.JOIN_RESIDENCE
|
OnboardingStep.JOIN_RESIDENCE
|
||||||
} else {
|
} else {
|
||||||
OnboardingStep.RESIDENCE_LOCATION
|
OnboardingStep.HOME_PROFILE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OnboardingStep.JOIN_RESIDENCE -> {
|
OnboardingStep.JOIN_RESIDENCE -> {
|
||||||
completeOnboarding()
|
completeOnboarding()
|
||||||
OnboardingStep.JOIN_RESIDENCE
|
OnboardingStep.JOIN_RESIDENCE
|
||||||
}
|
}
|
||||||
OnboardingStep.RESIDENCE_LOCATION -> OnboardingStep.HOME_PROFILE
|
OnboardingStep.RESIDENCE_LOCATION -> OnboardingStep.HOME_PROFILE // Skip past if somehow reached
|
||||||
OnboardingStep.HOME_PROFILE -> OnboardingStep.FIRST_TASK
|
OnboardingStep.HOME_PROFILE -> OnboardingStep.FIRST_TASK
|
||||||
OnboardingStep.FIRST_TASK -> {
|
OnboardingStep.FIRST_TASK -> {
|
||||||
completeOnboarding()
|
completeOnboarding()
|
||||||
@@ -253,7 +253,6 @@ class OnboardingViewModel : ViewModel() {
|
|||||||
fun skipStep() {
|
fun skipStep() {
|
||||||
when (_currentStep.value) {
|
when (_currentStep.value) {
|
||||||
OnboardingStep.VALUE_PROPS,
|
OnboardingStep.VALUE_PROPS,
|
||||||
OnboardingStep.RESIDENCE_LOCATION,
|
|
||||||
OnboardingStep.HOME_PROFILE -> nextStep()
|
OnboardingStep.HOME_PROFILE -> nextStep()
|
||||||
OnboardingStep.JOIN_RESIDENCE,
|
OnboardingStep.JOIN_RESIDENCE,
|
||||||
OnboardingStep.FIRST_TASK,
|
OnboardingStep.FIRST_TASK,
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ struct OnboardingCoordinator: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let postalCode = onboardingState.pendingPostalCode.isEmpty ? nil : onboardingState.pendingPostalCode
|
let postalCode: String? = nil
|
||||||
print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName), zip: \(postalCode ?? "none")")
|
print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName)")
|
||||||
|
|
||||||
isCreatingResidence = true
|
isCreatingResidence = true
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ struct OnboardingCoordinator: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Current step index for progress indicator (0-based)
|
/// 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 {
|
private var currentProgressStep: Int {
|
||||||
switch onboardingState.currentStep {
|
switch onboardingState.currentStep {
|
||||||
case .welcome: return 0
|
case .welcome: return 0
|
||||||
@@ -155,7 +155,7 @@ struct OnboardingCoordinator: View {
|
|||||||
/// Whether to show the skip button
|
/// Whether to show the skip button
|
||||||
private var showSkipButton: Bool {
|
private var showSkipButton: Bool {
|
||||||
switch onboardingState.currentStep {
|
switch onboardingState.currentStep {
|
||||||
case .valueProps, .joinResidence, .residenceLocation, .homeProfile, .firstTask, .subscriptionUpsell:
|
case .valueProps, .joinResidence, .homeProfile, .firstTask, .subscriptionUpsell:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -197,9 +197,6 @@ struct OnboardingCoordinator: View {
|
|||||||
switch onboardingState.currentStep {
|
switch onboardingState.currentStep {
|
||||||
case .valueProps:
|
case .valueProps:
|
||||||
goForward()
|
goForward()
|
||||||
case .residenceLocation:
|
|
||||||
// Skipping location — go to home profile
|
|
||||||
goForward(to: .homeProfile)
|
|
||||||
case .homeProfile:
|
case .homeProfile:
|
||||||
// Skipping home profile — create residence without profile data, go to tasks
|
// Skipping home profile — create residence without profile data, go to tasks
|
||||||
createResidenceIfNeeded(thenNavigateTo: .firstTask)
|
createResidenceIfNeeded(thenNavigateTo: .firstTask)
|
||||||
@@ -304,7 +301,7 @@ struct OnboardingCoordinator: View {
|
|||||||
if onboardingState.userIntent == .joinExisting {
|
if onboardingState.userIntent == .joinExisting {
|
||||||
goForward(to: .joinResidence)
|
goForward(to: .joinResidence)
|
||||||
} else {
|
} else {
|
||||||
goForward(to: .residenceLocation)
|
goForward(to: .homeProfile)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
goForward()
|
goForward()
|
||||||
@@ -320,7 +317,7 @@ struct OnboardingCoordinator: View {
|
|||||||
if onboardingState.userIntent == .joinExisting {
|
if onboardingState.userIntent == .joinExisting {
|
||||||
goForward(to: .joinResidence)
|
goForward(to: .joinResidence)
|
||||||
} else {
|
} else {
|
||||||
goForward(to: .residenceLocation)
|
goForward(to: .homeProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -336,18 +333,10 @@ struct OnboardingCoordinator: View {
|
|||||||
.transition(navigationTransition)
|
.transition(navigationTransition)
|
||||||
|
|
||||||
case .residenceLocation:
|
case .residenceLocation:
|
||||||
OnboardingLocationContent(
|
// Location step removed — skip to home profile if we land here
|
||||||
onLocationDetected: { zip in
|
EmptyView()
|
||||||
// Load regional templates in background
|
.onAppear { goForward(to: .homeProfile) }
|
||||||
onboardingState.loadRegionalTemplates(zip: zip)
|
.transition(navigationTransition)
|
||||||
// Go to home profile step (residence created after profile)
|
|
||||||
goForward()
|
|
||||||
},
|
|
||||||
onSkip: {
|
|
||||||
// Handled by handleSkip() above
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.transition(navigationTransition)
|
|
||||||
|
|
||||||
case .homeProfile:
|
case .homeProfile:
|
||||||
OnboardingHomeProfileContent(
|
OnboardingHomeProfileContent(
|
||||||
|
|||||||
@@ -53,12 +53,9 @@ struct OnboardingFirstTaskContent: View {
|
|||||||
/// Cached categories — computed once and stored to preserve stable UUIDs
|
/// Cached categories — computed once and stored to preserve stable UUIDs
|
||||||
@State private var taskCategoriesCache: [OnboardingTaskCategory]? = nil
|
@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] {
|
private var taskCategories: [OnboardingTaskCategory] {
|
||||||
if let cached = taskCategoriesCache { return cached }
|
if let cached = taskCategoriesCache { return cached }
|
||||||
if !onboardingState.regionalTemplates.isEmpty {
|
|
||||||
return categoriesFromAPI(onboardingState.regionalTemplates)
|
|
||||||
}
|
|
||||||
return fallbackCategories
|
return fallbackCategories
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,9 +294,7 @@ struct OnboardingFirstTaskContent: View {
|
|||||||
.foregroundColor(Color.appTextPrimary)
|
.foregroundColor(Color.appTextPrimary)
|
||||||
.a11yHeader()
|
.a11yHeader()
|
||||||
|
|
||||||
Text(onboardingState.regionalTemplates.isEmpty
|
Text("Let's get you started with some tasks.\nThe more you pick, the more we'll help you remember!")
|
||||||
? "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!")
|
|
||||||
.font(.system(size: 15, weight: .medium))
|
.font(.system(size: 15, weight: .medium))
|
||||||
.foregroundColor(Color.appTextSecondary)
|
.foregroundColor(Color.appTextSecondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@@ -465,11 +460,7 @@ struct OnboardingFirstTaskContent: View {
|
|||||||
isAnimating = true
|
isAnimating = true
|
||||||
// Build and cache categories once to preserve stable UUIDs
|
// Build and cache categories once to preserve stable UUIDs
|
||||||
if taskCategoriesCache == nil {
|
if taskCategoriesCache == nil {
|
||||||
if !onboardingState.regionalTemplates.isEmpty {
|
taskCategoriesCache = fallbackCategories
|
||||||
taskCategoriesCache = categoriesFromAPI(onboardingState.regionalTemplates)
|
|
||||||
} else {
|
|
||||||
taskCategoriesCache = fallbackCategories
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Expand first category by default
|
// Expand first category by default
|
||||||
if let first = taskCategories.first?.name {
|
if let first = taskCategories.first?.name {
|
||||||
@@ -485,23 +476,15 @@ struct OnboardingFirstTaskContent: View {
|
|||||||
|
|
||||||
private func selectPopularTasks() {
|
private func selectPopularTasks() {
|
||||||
withAnimation(.spring(response: 0.3)) {
|
withAnimation(.spring(response: 0.3)) {
|
||||||
if !onboardingState.regionalTemplates.isEmpty {
|
let popularTaskTitles = [
|
||||||
// API templates: select the first tasks (they're ordered by display_order)
|
"Change HVAC Filter",
|
||||||
for task in allTasks {
|
"Test Smoke Detectors",
|
||||||
selectedTasks.insert(task.id)
|
"Check for Leaks",
|
||||||
}
|
"Clean Gutters",
|
||||||
} else {
|
"Clean Refrigerator Coils"
|
||||||
// Fallback: select hardcoded popular tasks
|
]
|
||||||
let popularTaskTitles = [
|
for task in allTasks where popularTaskTitles.contains(task.title) {
|
||||||
"Change HVAC Filter",
|
selectedTasks.insert(task.id)
|
||||||
"Test Smoke Detectors",
|
|
||||||
"Check for Leaks",
|
|
||||||
"Clean Gutters",
|
|
||||||
"Clean Refrigerator Coils"
|
|
||||||
]
|
|
||||||
for task in allTasks where popularTaskTitles.contains(task.title) {
|
|
||||||
selectedTasks.insert(task.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,15 +36,6 @@ class OnboardingState: ObservableObject {
|
|||||||
/// The ID of the residence created during onboarding (used for task creation)
|
/// The ID of the residence created during onboarding (used for task creation)
|
||||||
@Published var createdResidenceId: Int32? = nil
|
@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)
|
// MARK: - Home Profile State (collected during onboarding)
|
||||||
|
|
||||||
@Published var pendingHeatingType: String? = nil
|
@Published var pendingHeatingType: String? = nil
|
||||||
@@ -80,20 +71,6 @@ class OnboardingState: ObservableObject {
|
|||||||
|
|
||||||
private init() {}
|
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
|
/// Start the onboarding flow
|
||||||
func startOnboarding() {
|
func startOnboarding() {
|
||||||
isOnboardingActive = true
|
isOnboardingActive = true
|
||||||
@@ -102,7 +79,7 @@ class OnboardingState: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Move to the next step in the flow
|
/// 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() {
|
func nextStep() {
|
||||||
switch currentStep {
|
switch currentStep {
|
||||||
case .welcome:
|
case .welcome:
|
||||||
@@ -121,11 +98,12 @@ class OnboardingState: ObservableObject {
|
|||||||
if userIntent == .joinExisting {
|
if userIntent == .joinExisting {
|
||||||
currentStep = .joinResidence
|
currentStep = .joinResidence
|
||||||
} else {
|
} else {
|
||||||
currentStep = .residenceLocation
|
currentStep = .homeProfile
|
||||||
}
|
}
|
||||||
case .joinResidence:
|
case .joinResidence:
|
||||||
completeOnboarding()
|
completeOnboarding()
|
||||||
case .residenceLocation:
|
case .residenceLocation:
|
||||||
|
// Skip past this step if we somehow land here
|
||||||
currentStep = .homeProfile
|
currentStep = .homeProfile
|
||||||
case .homeProfile:
|
case .homeProfile:
|
||||||
currentStep = .firstTask
|
currentStep = .firstTask
|
||||||
@@ -152,8 +130,6 @@ class OnboardingState: ObservableObject {
|
|||||||
hasCompletedOnboarding = true
|
hasCompletedOnboarding = true
|
||||||
isOnboardingActive = false
|
isOnboardingActive = false
|
||||||
pendingResidenceName = ""
|
pendingResidenceName = ""
|
||||||
pendingPostalCode = ""
|
|
||||||
regionalTemplates = []
|
|
||||||
createdResidenceId = nil
|
createdResidenceId = nil
|
||||||
userIntent = .unknown
|
userIntent = .unknown
|
||||||
resetHomeProfile()
|
resetHomeProfile()
|
||||||
@@ -165,8 +141,6 @@ class OnboardingState: ObservableObject {
|
|||||||
hasCompletedOnboarding = false
|
hasCompletedOnboarding = false
|
||||||
isOnboardingActive = false
|
isOnboardingActive = false
|
||||||
pendingResidenceName = ""
|
pendingResidenceName = ""
|
||||||
pendingPostalCode = ""
|
|
||||||
regionalTemplates = []
|
|
||||||
createdResidenceId = nil
|
createdResidenceId = nil
|
||||||
userIntent = .unknown
|
userIntent = .unknown
|
||||||
currentStep = .welcome
|
currentStep = .welcome
|
||||||
|
|||||||
Reference in New Issue
Block a user