• Provides onboarding context to child components, managing step progression and completion state. Persists onboarding state to storage and validates step data on initialization.

    Features:

    • Persistent step completion tracking via storage
    • Automatic validation and filtering of stored steps
    • Graceful handling of storage migrations and data corruption
    • Async storage I/O with error handling
    • Strict boolean parsing for onboarding completion flag

    Parameters

    • children: Readonly<{ children: ReactNode }>

      React children to wrap with onboarding context.

    Returns Element

    Provider component with onboarding context value.

    export function OnboardingProvider({
    children,
    }: Readonly<{
    children: React.ReactNode;
    }>) {
    const [isActive, setIsActive] = useState(false);
    const [currentStep, setCurrentStep] = useState<OnboardingStep>("welcome");
    const [completedSteps, setCompletedSteps] = useState<OnboardingStep[]>([]);
    const initializationRef = useRef(false);

    /**
    * Parses a storage value to a strict boolean.
    * Only the exact string "true" is considered true; all other values are false.
    * @param value - The storage value to parse
    * @returns A strict boolean
    */
    const parseStrictBoolean = (value: string | null): boolean => {
    return value === "true";
    };

    /**
    * Filters completed steps to only include valid steps from STEP_ORDER.
    * Prevents migration issues from storage key changes.
    * @param steps - The steps to filter
    * @returns Filtered steps that exist in STEP_ORDER
    */
    const filterValidSteps = (steps: OnboardingStep[]): OnboardingStep[] => {
    return steps.filter((step) => STEP_ORDER.includes(step));
    };

    /**
    * Processes loaded steps and sets up initial state.
    * Extracts complexity from initialization effect.
    * @param stepsStr - The stringified completed steps
    */
    const processLoadedSteps = (stepsStr: string | null): void => {
    if (!stepsStr) {
    // Default to empty array if key is missing
    setCompletedSteps([]);
    setCurrentStep("welcome");
    return;
    }

    try {
    const parsed = JSON.parse(stepsStr);
    // Validate that parsed value is an array before use
    if (Array.isArray(parsed)) {
    const validSteps = filterValidSteps(parsed);
    setCompletedSteps(validSteps);

    // Find next incomplete step
    const nextStep = STEP_ORDER.find((step) => !validSteps.includes(step));
    if (nextStep) {
    setCurrentStep(nextStep);
    }
    } else {
    console.debug(
    "[OnboardingContext] Parsed steps is not an array, resetting",
    );
    setCompletedSteps([]);
    setCurrentStep("welcome");
    }
    } catch {
    // Reset if parsing fails
    console.debug(
    "[OnboardingContext] Failed to parse completed steps, resetting",
    );
    setCompletedSteps([]);
    setCurrentStep("welcome");
    }
    };

    // Initialize onboarding state from storage (runs once)
    useEffect(() => {
    if (initializationRef.current) return; // Skip if already initialized
    initializationRef.current = true;

    const initializeOnboarding = async () => {
    try {
    const completed = await storage.getItemAsync(
    STORAGE_KEYS.ONBOARDING_COMPLETED,
    );
    const completedStepsStr = await storage.getItemAsync(
    STORAGE_KEYS.ONBOARDING_STEPS_COMPLETED,
    );

    // Use strict boolean parsing
    if (!parseStrictBoolean(completed)) {
    setIsActive(true);
    processLoadedSteps(completedStepsStr);
    }
    } catch (error) {
    console.error("[OnboardingContext] Error initializing:", error);
    }
    };

    void initializeOnboarding();
    }, []);

    const completeStep = useCallback((step: OnboardingStep) => {
    setCompletedSteps((prev) => {
    const updated = Array.from(new Set([...prev, step]));
    // Persist to storage asynchronously
    storage
    .setItemAsync(
    STORAGE_KEYS.ONBOARDING_STEPS_COMPLETED,
    JSON.stringify(updated),
    )
    .catch((error) => {
    console.error(
    "[OnboardingContext] Failed to persist completed steps:",
    error,
    );
    });
    return updated;
    });
    }, []);

    const skipStep = useCallback(
    (step: OnboardingStep) => {
    // Skipping a step also marks it as completed so we can move forward
    completeStep(step);
    },
    [completeStep],
    );

    const goToStep = useCallback((step: OnboardingStep) => {
    setCurrentStep(step);
    }, []);

    const nextStep = useCallback(() => {
    const currentIndex = STEP_ORDER.indexOf(currentStep);
    if (currentIndex < STEP_ORDER.length - 1) {
    setCurrentStep(STEP_ORDER[currentIndex + 1]);
    }
    }, [currentStep]);

    const previousStep = useCallback(() => {
    const currentIndex = STEP_ORDER.indexOf(currentStep);
    if (currentIndex > 0) {
    setCurrentStep(STEP_ORDER[currentIndex - 1]);
    }
    }, [currentStep]);

    const startOnboarding = useCallback(() => {
    setIsActive(true);
    setCurrentStep("welcome");
    }, []);

    const finishOnboarding = useCallback(() => {
    // Mark all steps as completed
    storage
    .setItemAsync(STORAGE_KEYS.ONBOARDING_COMPLETED, "true")
    .catch((error) => {
    console.error(
    "[OnboardingContext] Failed to persist onboarding completion:",
    error,
    );
    });
    storage
    .setItemAsync(
    STORAGE_KEYS.ONBOARDING_STEPS_COMPLETED,
    JSON.stringify(STEP_ORDER),
    )
    .catch((error) => {
    console.error(
    "[OnboardingContext] Failed to persist completed steps:",
    error,
    );
    });
    setIsActive(false);
    }, []);

    const resetOnboarding = useCallback(() => {
    storage
    .setItemAsync(STORAGE_KEYS.ONBOARDING_COMPLETED, "false")
    .catch((error) => {
    console.error("[OnboardingContext] Failed to reset onboarding:", error);
    });
    storage
    .setItemAsync(STORAGE_KEYS.ONBOARDING_STEPS_COMPLETED, "[]")
    .catch((error) => {
    console.error(
    "[OnboardingContext] Failed to reset completed steps:",
    error,
    );
    });
    setCompletedSteps([]);
    setCurrentStep("welcome");
    setIsActive(true);
    }, []);

    const dismissOnboarding = useCallback(() => {
    setIsActive(false);
    // Still mark as completed so it doesn't show again unless explicitly reset
    storage
    .setItemAsync(STORAGE_KEYS.ONBOARDING_COMPLETED, "true")
    .catch((error) => {
    console.error(
    "[OnboardingContext] Failed to dismiss onboarding:",
    error,
    );
    });
    }, []);

    const isStepCompleted = useCallback(
    (step: OnboardingStep) => completedSteps.includes(step),
    [completedSteps],
    );

    const isStepActive = useCallback(
    (step: OnboardingStep) => currentStep === step,
    [currentStep],
    );

    const getStepProgress = useCallback(() => {
    return Math.round((completedSteps.length / STEP_ORDER.length) * 100);
    }, [completedSteps]);

    const stepProgress = STEP_ORDER.reduce(
    (acc, step) => {
    acc[step] = completedSteps.includes(step);
    return acc;
    },
    {} as Record<OnboardingStep, boolean>,
    );

    const value: OnboardingContextType = useMemo(
    () => ({
    isActive,
    currentStep,
    completedSteps,
    stepProgress,
    startOnboarding,
    completeStep,
    skipStep,
    goToStep,
    nextStep,
    previousStep,
    finishOnboarding,
    resetOnboarding,
    dismissOnboarding,
    isStepCompleted,
    isStepActive,
    getStepProgress,
    }),
    [
    isActive,
    currentStep,
    completedSteps,
    stepProgress,
    startOnboarding,
    completeStep,
    skipStep,
    goToStep,
    nextStep,
    previousStep,
    finishOnboarding,
    resetOnboarding,
    dismissOnboarding,
    isStepCompleted,
    isStepActive,
    getStepProgress,
    ],
    );

    return (
    <OnboardingContext.Provider value={value}>
    {children}
    </OnboardingContext.Provider>
    );
    }