Add regional task templates to onboarding with multiple bug fixes

- Fetch regional templates by ZIP during onboarding and display categorized
  task suggestions (iOS + KMM shared layer)
- Fix multi-expand for task categories (toggle independently, not exclusive)
- Fix ScrollViewReader auto-scroll to expanded category sections
- Fix UUID stability bug: cache task categories to prevent ID regeneration
  that caused silent task creation failures
- Fix stale data after onboarding: force refresh residences and tasks in
  RootView onComplete callback
- Fix address formatting: show just ZIP code when city/state are empty
  instead of showing ", 75028" with leading comma

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-05 15:15:47 -06:00
parent 98dbacdea0
commit 48081c0cc8
15 changed files with 706 additions and 61 deletions

View File

@@ -52,7 +52,7 @@ struct OnboardingCoordinator: View {
}
}
/// Creates a residence with the pending name from onboarding, then calls completion
/// Creates a residence with the pending name and postal code from onboarding, then calls completion
private func createResidenceIfNeeded(thenNavigateTo step: OnboardingStep) {
print("🏠 ONBOARDING: createResidenceIfNeeded called")
print("🏠 ONBOARDING: userIntent = \(onboardingState.userIntent)")
@@ -66,7 +66,8 @@ struct OnboardingCoordinator: View {
return
}
print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName)")
let postalCode = onboardingState.pendingPostalCode.isEmpty ? nil : onboardingState.pendingPostalCode
print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName), zip: \(postalCode ?? "none")")
isCreatingResidence = true
@@ -77,7 +78,7 @@ struct OnboardingCoordinator: View {
apartmentUnit: nil,
city: nil,
stateProvince: nil,
postalCode: nil,
postalCode: postalCode,
country: nil,
bedrooms: nil,
bathrooms: nil,
@@ -104,7 +105,7 @@ struct OnboardingCoordinator: View {
}
/// Current step index for progress indicator (0-based)
/// Flow: Welcome Features Name Residence Create Account Verify Tasks Upsell
/// Flow: Welcome Features Name Account Verify Location Tasks Upsell
private var currentProgressStep: Int {
switch onboardingState.currentStep {
case .welcome: return 0
@@ -113,6 +114,7 @@ struct OnboardingCoordinator: View {
case .createAccount: return 3
case .verifyEmail: return 4
case .joinResidence: return 4
case .residenceLocation: return 4
case .firstTask: return 4
case .subscriptionUpsell: return 4
}
@@ -121,7 +123,7 @@ struct OnboardingCoordinator: View {
/// Whether to show the back button
private var showBackButton: Bool {
switch onboardingState.currentStep {
case .welcome, .joinResidence, .firstTask, .subscriptionUpsell:
case .welcome, .joinResidence, .residenceLocation, .firstTask, .subscriptionUpsell:
return false
default:
return true
@@ -131,7 +133,7 @@ struct OnboardingCoordinator: View {
/// Whether to show the skip button
private var showSkipButton: Bool {
switch onboardingState.currentStep {
case .valueProps, .joinResidence, .firstTask, .subscriptionUpsell:
case .valueProps, .joinResidence, .residenceLocation, .firstTask, .subscriptionUpsell:
return true
default:
return false
@@ -141,7 +143,7 @@ struct OnboardingCoordinator: View {
/// Whether to show the progress indicator
private var showProgressIndicator: Bool {
switch onboardingState.currentStep {
case .welcome, .joinResidence, .firstTask, .subscriptionUpsell:
case .welcome, .joinResidence, .residenceLocation, .firstTask, .subscriptionUpsell:
return false
default:
return true
@@ -173,6 +175,9 @@ struct OnboardingCoordinator: View {
switch onboardingState.currentStep {
case .valueProps:
goForward()
case .residenceLocation:
// Skipping location still need to create residence (without postal code)
createResidenceIfNeeded(thenNavigateTo: .firstTask)
case .joinResidence, .firstTask:
goForward()
case .subscriptionUpsell:
@@ -187,13 +192,14 @@ struct OnboardingCoordinator: View {
VStack(spacing: 0) {
// Shared navigation bar - stays static
HStack {
// Back button
// Back button fixed width so progress dots stay centered
Button(action: handleBack) {
Image(systemName: "chevron.left")
.font(.title2)
.foregroundColor(Color.appPrimary)
}
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.backButton)
.frame(width: 44, alignment: .leading)
.opacity(showBackButton ? 1 : 0)
.disabled(!showBackButton)
@@ -207,7 +213,7 @@ struct OnboardingCoordinator: View {
Spacer()
// Skip button
// Skip button fixed width to match back button
Button(action: handleSkip) {
Text("Skip")
.font(.subheadline)
@@ -215,6 +221,7 @@ struct OnboardingCoordinator: View {
.foregroundColor(Color.appTextSecondary)
}
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.skipButton)
.frame(width: 44, alignment: .trailing)
.opacity(showSkipButton ? 1 : 0)
.disabled(!showSkipButton)
}
@@ -268,7 +275,7 @@ struct OnboardingCoordinator: View {
if onboardingState.userIntent == .joinExisting {
goForward(to: .joinResidence)
} else {
createResidenceIfNeeded(thenNavigateTo: .firstTask)
goForward(to: .residenceLocation)
}
} else {
goForward()
@@ -281,13 +288,10 @@ struct OnboardingCoordinator: View {
OnboardingVerifyEmailContent(
onVerified: {
print("🏠 ONBOARDING: onVerified callback triggered in coordinator")
// NOTE: Do NOT call markVerified() here - it would cause RootView
// to switch to MainTabView before onboarding completes.
// markVerified() is called at the end of onboarding in onComplete.
if onboardingState.userIntent == .joinExisting {
goForward(to: .joinResidence)
} else {
createResidenceIfNeeded(thenNavigateTo: .firstTask)
goForward(to: .residenceLocation)
}
}
)
@@ -301,6 +305,20 @@ struct OnboardingCoordinator: View {
)
.transition(navigationTransition)
case .residenceLocation:
OnboardingLocationContent(
onLocationDetected: { zip in
// Load regional templates in background while creating residence
onboardingState.loadRegionalTemplates(zip: zip)
// Create residence with postal code, then go to first task
createResidenceIfNeeded(thenNavigateTo: .firstTask)
},
onSkip: {
// Handled by handleSkip() above
}
)
.transition(navigationTransition)
case .firstTask:
OnboardingFirstTaskContent(
residenceName: onboardingState.pendingResidenceName,