import SwiftUI import ComposeApp /// Coordinates the onboarding flow, presenting the appropriate view based on current step struct OnboardingCoordinator: View { @ObservedObject private var onboardingState = OnboardingState.shared @StateObject private var residenceViewModel = ResidenceViewModel() @State private var isNavigatingBack = false @State private var isCreatingResidence = false var onComplete: () -> Void /// Transition that respects navigation direction private var navigationTransition: AnyTransition { if isNavigatingBack { // Going back: new view slides in from left, old view fades out in place return .asymmetric( insertion: .move(edge: .leading), removal: .opacity ) } else { // Going forward: new view slides in from right, old view fades out in place return .asymmetric( insertion: .move(edge: .trailing), removal: .opacity ) } } private func goBack(to step: OnboardingStep) { isNavigatingBack = true withAnimation(.easeInOut(duration: 0.3)) { onboardingState.currentStep = step } completion: { isNavigatingBack = false } } private func goForward() { withAnimation(.easeInOut(duration: 0.3)) { isNavigatingBack = false onboardingState.nextStep() } } private func goForward(to step: OnboardingStep) { withAnimation(.easeInOut(duration: 0.3)) { isNavigatingBack = false onboardingState.currentStep = step } } /// 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)") print("🏠 ONBOARDING: pendingResidenceName = '\(onboardingState.pendingResidenceName)'") // Only create residence if user is starting fresh (not joining existing) guard onboardingState.userIntent == .startFresh, !onboardingState.pendingResidenceName.isEmpty else { print("🏠 ONBOARDING: Skipping residence creation - conditions not met") goForward(to: step) return } let postalCode = onboardingState.pendingPostalCode.isEmpty ? nil : onboardingState.pendingPostalCode print("🏠 ONBOARDING: Creating residence with name: \(onboardingState.pendingResidenceName), zip: \(postalCode ?? "none")") isCreatingResidence = true let request = ResidenceCreateRequest( name: onboardingState.pendingResidenceName, propertyTypeId: nil, streetAddress: nil, apartmentUnit: nil, city: nil, stateProvince: nil, postalCode: postalCode, country: nil, bedrooms: nil, bathrooms: nil, squareFootage: nil, lotSize: nil, yearBuilt: nil, description: nil, purchaseDate: nil, purchasePrice: nil, isPrimary: KotlinBoolean(bool: true) ) residenceViewModel.createResidence(request: request) { (residence: ResidenceResponse?) in self.isCreatingResidence = false if let residence = residence { print("🏠 ONBOARDING: Residence created successfully with ID: \(residence.id)") self.onboardingState.createdResidenceId = residence.id } else { print("🏠 ONBOARDING: Residence creation FAILED") } // Navigate regardless of success - user can create residence later if needed self.goForward(to: step) } } /// Current step index for progress indicator (0-based) /// Flow: Welcome → Features → Name → Account → Verify → Location → Tasks → Upsell private var currentProgressStep: Int { switch onboardingState.currentStep { case .welcome: return 0 case .valueProps: return 1 case .nameResidence: return 2 case .createAccount: return 3 case .verifyEmail: return 4 case .joinResidence: return 4 case .residenceLocation: return 4 case .firstTask: return 4 case .subscriptionUpsell: return 4 } } /// Whether to show the back button private var showBackButton: Bool { switch onboardingState.currentStep { case .welcome, .joinResidence, .residenceLocation, .firstTask, .subscriptionUpsell: return false default: return true } } /// Whether to show the skip button private var showSkipButton: Bool { switch onboardingState.currentStep { case .valueProps, .joinResidence, .residenceLocation, .firstTask, .subscriptionUpsell: return true default: return false } } /// Whether to show the progress indicator private var showProgressIndicator: Bool { switch onboardingState.currentStep { case .welcome, .joinResidence, .residenceLocation, .firstTask, .subscriptionUpsell: return false default: return true } } private func handleBack() { // Flow: Welcome → Features → Name Residence → Create Account → Verify → Tasks → Upsell switch onboardingState.currentStep { case .valueProps: goBack(to: .welcome) case .nameResidence: goBack(to: .valueProps) case .createAccount: if onboardingState.userIntent == .joinExisting { goBack(to: .welcome) } else { goBack(to: .nameResidence) } case .verifyEmail: AuthenticationManager.shared.logout() goBack(to: .createAccount) default: break } } private func handleSkip() { 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: onboardingState.completeOnboarding() onComplete() default: break } } var body: some View { VStack(spacing: 0) { // Shared navigation bar - stays static HStack { // 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) .accessibilityLabel("Go back") .frame(width: 44, alignment: .leading) .opacity(showBackButton ? 1 : 0) .disabled(!showBackButton) Spacer() // Progress indicator if showProgressIndicator { OnboardingProgressIndicator(currentStep: currentProgressStep, totalSteps: 5) .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.progressIndicator) .accessibilityLabel("Step \(currentProgressStep + 1) of 5") } Spacer() // Skip button — fixed width to match back button Button(action: handleSkip) { Text("Skip") .font(.subheadline) .fontWeight(.medium) .foregroundColor(Color.appTextSecondary) } .accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.skipButton) .accessibilityLabel("Skip") .frame(width: 44, alignment: .trailing) .opacity(showSkipButton ? 1 : 0) .disabled(!showSkipButton) } .padding(.horizontal, AppSpacing.lg) .padding(.vertical, AppSpacing.md) // Content area - this is what transitions ZStack { switch onboardingState.currentStep { case .welcome: OnboardingWelcomeContent( onStartFresh: { onboardingState.userIntent = .startFresh goForward() }, onJoinExisting: { onboardingState.userIntent = .joinExisting goForward() }, onLogin: { // Returning user logged in - mark onboarding complete and go to main view onboardingState.completeOnboarding() AuthenticationManager.shared.markVerified() onComplete() } ) .transition(navigationTransition) case .nameResidence: OnboardingNameResidenceContent( residenceName: $onboardingState.pendingResidenceName, onContinue: { goForward() } ) .transition(navigationTransition) case .valueProps: OnboardingValuePropsContent( onContinue: { goForward() } ) .transition(navigationTransition) case .createAccount: OnboardingCreateAccountContent( onAccountCreated: { isVerified in if isVerified { // Skip email verification if onboardingState.userIntent == .joinExisting { goForward(to: .joinResidence) } else { goForward(to: .residenceLocation) } } else { goForward() } } ) .transition(navigationTransition) case .verifyEmail: OnboardingVerifyEmailContent( onVerified: { print("🏠 ONBOARDING: onVerified callback triggered in coordinator") if onboardingState.userIntent == .joinExisting { goForward(to: .joinResidence) } else { goForward(to: .residenceLocation) } } ) .transition(navigationTransition) case .joinResidence: OnboardingJoinResidenceContent( onJoined: { goForward() } ) .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, onTaskAdded: { goForward() } ) .transition(navigationTransition) case .subscriptionUpsell: OnboardingSubscriptionContent( onSubscribe: { // Handle subscription flow onboardingState.completeOnboarding() onComplete() } ) .transition(navigationTransition) } } .animation(.easeInOut(duration: 0.3), value: onboardingState.currentStep) } .background(Color.appBackgroundPrimary) } } // MARK: - Progress Indicator struct OnboardingProgressIndicator: View { let currentStep: Int let totalSteps: Int var body: some View { HStack(spacing: AppSpacing.xs) { ForEach(0..