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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user