'use client' import React, { useRef, useCallback } from 'react' import { Button } from '../../ui/button' import { OnboardingStepCard } from './onboarding-step-card' import { useOnboardingState, type OnboardingStepConfig } from '../../../hooks/ui/use-onboarding-state' import { cn } from '../../../utils/cn' import { SECTION_HEADING_CLASS } from '../../layout/page-heading' export interface OnboardingWalkthroughProps { steps: OnboardingStepConfig[] onDismiss?: () => void storageKey?: string className?: string spacing?: string /** * Completion status from hook-based checks (e.g., useOnboardingCompletion) * Object mapping step IDs to boolean completion status */ completionStatus?: Record /** * Whether completion checks are still loading */ isLoadingCompletion?: boolean } export function OnboardingWalkthrough({ steps, onDismiss, storageKey = 'openframe-onboarding-state', className, spacing = 'space-y-4', completionStatus, isLoadingCompletion = false }: OnboardingWalkthroughProps) { const { state, markComplete, markSkipped, dismissOnboarding, isStepComplete, isStepSkipped, allStepsComplete, markMultipleComplete } = useOnboardingState(storageKey) // Refs to prevent race conditions - ALL refs must be declared before any conditional returns const hasAutoMarkedRef = useRef(false) const autoMarkingInProgressRef = useRef(false) const lastCompletionStatusRef = useRef(null) // Ref to track in-flight action handlers - moved here to be before conditional return const actionInProgressRef = useRef>(new Set()) // Sync completion status from hooks to localStorage // Uses refs to prevent duplicate calls and race conditions React.useEffect(() => { // Skip if still loading if (isLoadingCompletion) { hasAutoMarkedRef.current = false // Reset when loading starts return } // Skip if no completion status if (!completionStatus) return // Create a stable key from completion status to detect actual changes const statusKey = JSON.stringify(completionStatus) // Skip if we've already processed this exact status if (lastCompletionStatusRef.current === statusKey) return // Skip if auto-marking is currently in progress if (autoMarkingInProgressRef.current) return // Find steps that need to be marked complete // Read directly from state to avoid stale closure issues const completedStepIds = Object.entries(completionStatus) .filter(([stepId, isComplete]) => { if (!isComplete) return false // Check against current state, not the callback return !state.completedSteps.includes(stepId) }) .map(([stepId]) => stepId) if (completedStepIds.length > 0) { // Mark as in progress to prevent concurrent calls autoMarkingInProgressRef.current = true console.log('📊 Auto-marking steps as complete from hook data:', completedStepIds) markMultipleComplete(completedStepIds) // Update the last processed status key lastCompletionStatusRef.current = statusKey // Reset the in-progress flag after a short delay // This prevents rapid successive calls while allowing future updates setTimeout(() => { autoMarkingInProgressRef.current = false }, 100) } else { // Even if no steps to mark, update the status key to prevent re-processing lastCompletionStatusRef.current = statusKey } }, [completionStatus, isLoadingCompletion, state.completedSteps, markMultipleComplete]) // useCallback must be called unconditionally (before any early returns) const handleStepAction = useCallback(async (step: OnboardingStepConfig) => { // Prevent duplicate action handling for the same step if (actionInProgressRef.current.has(step.id)) { console.log(`⏳ Action already in progress for "${step.id}", skipping`) return } // Mark as in progress actionInProgressRef.current.add(step.id) try { await step.onAction() // Skip if already completed (could have been auto-marked while action was running) if (state.completedSteps.includes(step.id)) { console.log(`✓ Step "${step.id}" already completed, skipping mark`) return } // Auto-mark Knowledge Base as complete (no verification needed) if (step.id === 'knowledge-base') { console.log(`🎯 Auto-completing Knowledge Base`) markComplete(step.id) return } // For other steps, re-check completion after action if (step.checkComplete) { try { const isComplete = await step.checkComplete() console.log(`🔍 Post-action check for "${step.id}": isComplete=${isComplete}`) // Double-check it wasn't completed while we were checking if (isComplete && !state.completedSteps.includes(step.id)) { markComplete(step.id) } } catch (error) { console.error(`Error checking completion for "${step.id}":`, error) } } } finally { // Remove from in-progress set actionInProgressRef.current.delete(step.id) } }, [state.completedSteps, markComplete]) const handleStepSkip = (step: OnboardingStepConfig) => { step.onSkip?.() markSkipped(step.id) } const handleDismiss = () => { dismissOnboarding() onDismiss?.() } // ALL hooks have been called above - now safe to do conditional returns // Don't render if dismissed if (state.dismissed) { return null } const isAllComplete = allStepsComplete(steps) return (
{/* Header - responsive: stacks on mobile */}

Get Started

{isLoadingCompletion ? (
) : isAllComplete ? ( ) : ( )}
{/* Step cards - no wrapper, direct rendering */}
{steps.map((step) => ( handleStepAction(step)} onSkip={() => handleStepSkip(step)} /> ))}
) }