diff --git a/e2e/utils/event-test-utils.ts b/e2e/utils/event-test-utils.ts index d0207de663..9966e87ec6 100644 --- a/e2e/utils/event-test-utils.ts +++ b/e2e/utils/event-test-utils.ts @@ -1,5 +1,4 @@ import { type Locator, type Page, expect } from "@playwright/test"; -import { ONBOARDING_STATE } from "./test-constants"; type SomedaySection = "week" | "month"; @@ -101,10 +100,6 @@ export const updateEventTitle = (prefix: string) => `${prefix} Updated ${Date.now()}`; export const prepareCalendarPage = async (page: Page) => { - await page.addInitScript((value) => { - localStorage.setItem("compass.onboarding", JSON.stringify(value)); - }, ONBOARDING_STATE); - await page.goto("/week", { waitUntil: "networkidle" }); // Wait for React app to mount by checking for root element with content diff --git a/e2e/utils/oauth-test-utils.ts b/e2e/utils/oauth-test-utils.ts index e6dfd09d8c..76942af1a4 100644 --- a/e2e/utils/oauth-test-utils.ts +++ b/e2e/utils/oauth-test-utils.ts @@ -1,19 +1,16 @@ import { Page, expect } from "@playwright/test"; -import { ONBOARDING_STATE } from "./test-constants"; /** * Sets up the page for OAuth overlay testing. - * - Skips onboarding * - Exposes test hooks for session state manipulation * - Mocks API endpoints */ export const prepareOAuthTestPage = async (page: Page) => { - // Enable test mode and skip onboarding before app loads - await page.addInitScript((onboardingState) => { + // Enable test mode before app loads + await page.addInitScript(() => { // Enable e2e test mode - this exposes test hooks in the app (window as any).__COMPASS_E2E_TEST__ = true; - localStorage.setItem("compass.onboarding", JSON.stringify(onboardingState)); - }, ONBOARDING_STATE); + }); // Mock API endpoints to prevent real network calls await page.route("**/api/**", (route) => { diff --git a/e2e/utils/test-constants.ts b/e2e/utils/test-constants.ts index fd5236ea38..d2f593b792 100644 --- a/e2e/utils/test-constants.ts +++ b/e2e/utils/test-constants.ts @@ -1,11 +1,3 @@ /** * Shared constants for e2e tests. */ - -export const ONBOARDING_STATE = { - completedSteps: [], - isCompleted: true, - isSignupComplete: true, - isOnboardingSkipped: true, - isAuthPromptDismissed: true, -}; diff --git a/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts b/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts index 02a5a35f8a..e97d810e32 100644 --- a/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts +++ b/packages/web/src/auth/hooks/oauth/useGoogleAuth.test.ts @@ -1,11 +1,8 @@ import { renderHook, waitFor } from "@testing-library/react"; import { useGoogleAuth } from "@web/auth/hooks/oauth/useGoogleAuth"; -import { useIsSignupComplete } from "@web/auth/hooks/onboarding/useIsSignupComplete"; -import { useSkipOnboarding } from "@web/auth/hooks/onboarding/useSkipOnboarding"; import { useSession } from "@web/auth/hooks/session/useSession"; import { authenticate, - fetchOnboardingStatus, syncLocalEvents, } from "@web/common/utils/auth/google-auth.util"; import { markUserAsAuthenticated } from "@web/common/utils/storage/auth-state.util"; @@ -15,8 +12,6 @@ import { SignInUpInput } from "@web/components/oauth/ouath.types"; // Mock dependencies jest.mock("@web/common/utils/auth/google-auth.util"); jest.mock("@web/auth/hooks/session/useSession"); -jest.mock("@web/auth/hooks/onboarding/useIsSignupComplete"); -jest.mock("@web/auth/hooks/onboarding/useSkipOnboarding"); jest.mock("@web/components/oauth/google/useGoogleLogin"); jest.mock("@web/common/utils/storage/auth-state.util"); jest.mock("@web/store/store.hooks", () => ({ @@ -36,19 +31,10 @@ jest.mock("react-toastify", () => ({ const mockAuthenticate = authenticate as jest.MockedFunction< typeof authenticate >; -const mockFetchOnboardingStatus = fetchOnboardingStatus as jest.MockedFunction< - typeof fetchOnboardingStatus ->; const mockSyncLocalEvents = syncLocalEvents as jest.MockedFunction< typeof syncLocalEvents >; const mockUseSession = useSession as jest.MockedFunction; -const mockUseIsSignupComplete = useIsSignupComplete as jest.MockedFunction< - typeof useIsSignupComplete ->; -const mockUseSkipOnboarding = useSkipOnboarding as jest.MockedFunction< - typeof useSkipOnboarding ->; const mockUseGoogleLogin = useGoogleLogin as jest.MockedFunction< typeof useGoogleLogin >; @@ -61,8 +47,6 @@ const mockMarkUserAsAuthenticated = describe("useGoogleAuth", () => { const mockSetAuthenticated = jest.fn(); - const mockMarkSignupCompleted = jest.fn(); - const mockUpdateOnboardingStatus = jest.fn(); const mockLogin = jest.fn(); const originalConsoleError = console.error; let mockDispatchFn: jest.Mock; @@ -78,16 +62,7 @@ describe("useGoogleAuth", () => { setAuthenticated: mockSetAuthenticated, authenticated: false, }); - mockUseIsSignupComplete.mockReturnValue({ - markSignupCompleted: mockMarkSignupCompleted, - isSignupComplete: false, - }); - mockUseSkipOnboarding.mockReturnValue({ - updateOnboardingStatus: mockUpdateOnboardingStatus, - skipOnboarding: false, - }); mockAuthenticate.mockResolvedValue({ success: true }); - mockFetchOnboardingStatus.mockResolvedValue({ skipOnboarding: true }); mockSyncLocalEvents.mockResolvedValue({ syncedCount: 0, success: true }); }); @@ -332,56 +307,5 @@ describe("useGoogleAuth", () => { expect(mockMarkUserAsAuthenticated).not.toHaveBeenCalled(); expect(mockSetAuthenticated).not.toHaveBeenCalled(); }); - - it("clears import flow when other operations throw errors after OAuth succeeds", async () => { - mockAuthenticate.mockResolvedValue({ success: true }); - const fetchError = new Error("Failed to fetch onboarding status"); - mockFetchOnboardingStatus.mockRejectedValue(fetchError); - - let onSuccessCallback: - | ((data: SignInUpInput) => Promise) - | undefined; - - mockUseGoogleLogin.mockImplementation(({ onSuccess }) => { - onSuccessCallback = onSuccess; - return { - login: mockLogin, - loading: false, - data: null, - }; - }); - - renderHook(() => useGoogleAuth()); - - if (onSuccessCallback) { - await onSuccessCallback({ - clientType: "web", - thirdPartyId: "google", - redirectURIInfo: { - redirectURIOnProviderDashboard: "", - redirectURIQueryParams: { - code: "test-auth-code", - scope: "email profile", - state: undefined, - }, - }, - }); - } - - await waitFor(() => { - expect(mockAuthenticate).toHaveBeenCalled(); - }); - - expect(mockDispatchFn).toHaveBeenCalledWith( - expect.objectContaining({ - type: "async/importGCal/setAwaitingImportResults", - payload: false, - }), - ); - - // Authentication succeeded, so these should be called - expect(mockMarkUserAsAuthenticated).toHaveBeenCalled(); - expect(mockSetAuthenticated).toHaveBeenCalled(); - }); }); }); diff --git a/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts b/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts index 2686ee965d..779b6c759e 100644 --- a/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts +++ b/packages/web/src/auth/hooks/oauth/useGoogleAuth.ts @@ -1,13 +1,10 @@ import { batch } from "react-redux"; import { toast } from "react-toastify"; import { useGoogleAuthWithOverlay } from "@web/auth/hooks/oauth/useGoogleAuthWithOverlay"; -import { useIsSignupComplete } from "@web/auth/hooks/onboarding/useIsSignupComplete"; -import { useSkipOnboarding } from "@web/auth/hooks/onboarding/useSkipOnboarding"; import { useSession } from "@web/auth/hooks/session/useSession"; import { toastDefaultOptions } from "@web/common/constants/toast.constants"; import { authenticate, - fetchOnboardingStatus, syncLocalEvents, } from "@web/common/utils/auth/google-auth.util"; import { markUserAsAuthenticated } from "@web/common/utils/storage/auth-state.util"; @@ -29,8 +26,6 @@ import { useAppDispatch } from "@web/store/store.hooks"; export function useGoogleAuth() { const dispatch = useAppDispatch(); const { setAuthenticated } = useSession(); - const { markSignupCompleted } = useIsSignupComplete(); - const { updateOnboardingStatus } = useSkipOnboarding(); const googleLogin = useGoogleAuthWithOverlay({ onStart: () => { @@ -67,12 +62,6 @@ export function useGoogleAuth() { dispatch(importGCalSlice.actions.setAwaitingImportResults(true)); }); - const { skipOnboarding } = await fetchOnboardingStatus(); - - updateOnboardingStatus(skipOnboarding); - - markSignupCompleted(); - const syncResult = await syncLocalEvents(); if (syncResult.success && syncResult.syncedCount > 0) { diff --git a/packages/web/src/auth/hooks/onboarding/useIsSignupComplete.test.ts b/packages/web/src/auth/hooks/onboarding/useIsSignupComplete.test.ts deleted file mode 100644 index 4ea67ac4e2..0000000000 --- a/packages/web/src/auth/hooks/onboarding/useIsSignupComplete.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { act } from "react"; -import { renderHook } from "@testing-library/react"; -import { STORAGE_KEYS } from "@web/common/constants/storage.constants"; -import { updateOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; -import { useIsSignupComplete } from "./useIsSignupComplete"; - -describe("useIsSignupComplete", () => { - beforeEach(() => { - localStorage.clear(); - }); - - it("should return false when localStorage is empty", () => { - const { result } = renderHook(() => useIsSignupComplete()); - - expect(result.current.isSignupComplete).toBe(false); - }); - - it("should return true when localStorage has completed signup flag", () => { - updateOnboardingProgress({ isSignupComplete: true }); - - const { result } = renderHook(() => useIsSignupComplete()); - - expect(result.current.isSignupComplete).toBe(true); - }); - - it("should return false when localStorage has invalid value", () => { - localStorage.setItem(STORAGE_KEYS.ONBOARDING_PROGRESS, "invalid"); - - const { result } = renderHook(() => useIsSignupComplete()); - - expect(result.current.isSignupComplete).toBe(false); - }); - - it("should update hasCompletedSignup when markSignupCompleted is called", () => { - const { result } = renderHook(() => useIsSignupComplete()); - - expect(result.current.isSignupComplete).toBe(false); - - act(() => { - result.current.markSignupCompleted(); - }); - - expect(result.current.isSignupComplete).toBe(true); - const stored = JSON.parse( - localStorage.getItem(STORAGE_KEYS.ONBOARDING_PROGRESS) ?? "{}", - ); - expect(stored.isSignupComplete).toBe(true); - }); - - it("should handle multiple calls to markSignupCompleted", () => { - const { result } = renderHook(() => useIsSignupComplete()); - - act(() => { - result.current.markSignupCompleted(); - }); - - expect(result.current.isSignupComplete).toBe(true); - - act(() => { - result.current.markSignupCompleted(); - }); - - expect(result.current.isSignupComplete).toBe(true); - const stored = JSON.parse( - localStorage.getItem(STORAGE_KEYS.ONBOARDING_PROGRESS) ?? "{}", - ); - expect(stored.isSignupComplete).toBe(true); - }); -}); diff --git a/packages/web/src/auth/hooks/onboarding/useIsSignupComplete.ts b/packages/web/src/auth/hooks/onboarding/useIsSignupComplete.ts deleted file mode 100644 index 7bfb7d175e..0000000000 --- a/packages/web/src/auth/hooks/onboarding/useIsSignupComplete.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { - getOnboardingProgress, - updateOnboardingProgress, -} from "@web/views/Onboarding/utils/onboarding.storage.util"; - -export const useIsSignupComplete = () => { - const [isSignupComplete, setIsSignupComplete] = useState( - null, - ); - - useEffect(() => { - const checkSignupStatus = () => { - const { isSignupComplete: storedValue } = getOnboardingProgress(); - setIsSignupComplete(storedValue); - }; - - checkSignupStatus(); - }, []); - - const markSignupCompleted = useCallback(() => { - updateOnboardingProgress({ isSignupComplete: true }); - setIsSignupComplete(true); - }, [setIsSignupComplete]); - - return { - isSignupComplete, - markSignupCompleted, - }; -}; diff --git a/packages/web/src/auth/hooks/onboarding/useSkipOnboarding.ts b/packages/web/src/auth/hooks/onboarding/useSkipOnboarding.ts deleted file mode 100644 index b336242c71..0000000000 --- a/packages/web/src/auth/hooks/onboarding/useSkipOnboarding.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { - getOnboardingProgress, - updateOnboardingProgress, -} from "@web/views/Onboarding/utils/onboarding.storage.util"; - -export const useSkipOnboarding = () => { - const [skipOnboarding, setSkipOnboarding] = useState(false); - - useEffect(() => { - const checkOnboardingStatus = () => { - const { isOnboardingSkipped: storedValue } = getOnboardingProgress(); - setSkipOnboarding(storedValue); - }; - - checkOnboardingStatus(); - }, []); - - const updateOnboardingStatus = useCallback( - (skip: boolean) => { - updateOnboardingProgress({ isOnboardingSkipped: skip }); - - setSkipOnboarding(skip); - }, - [setSkipOnboarding], - ); - - return { - skipOnboarding, - updateOnboardingStatus, - }; -}; diff --git a/packages/web/src/common/constants/onboarding.constants.ts b/packages/web/src/common/constants/onboarding.constants.ts deleted file mode 100644 index 4fbd0190a7..0000000000 --- a/packages/web/src/common/constants/onboarding.constants.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from "zod"; - -const CompletedStepsSchema = z.array( - z.enum([ - "navigateToDay", - "createTask", - "navigateToNow", - "editDescription", - "editReminder", - "navigateToWeek", - "connectGoogleCalendar", - ]), -); -export const OnboardingProgressSchema = z.object({ - completedSteps: CompletedStepsSchema.default([]), - isCompleted: z.boolean().default(false), - isSignupComplete: z.boolean().default(false), - isOnboardingSkipped: z.boolean().default(false), - isAuthPromptDismissed: z.boolean().default(false), -}); - -export type OnboardingProgress = z.infer; - -export const DEFAULT_ONBOARDING_PROGRESS: OnboardingProgress = { - completedSteps: [], - isCompleted: false, - isSignupComplete: false, - isOnboardingSkipped: false, - isAuthPromptDismissed: false, -}; diff --git a/packages/web/src/common/constants/storage.constants.ts b/packages/web/src/common/constants/storage.constants.ts index fde34facd6..342f98357b 100644 --- a/packages/web/src/common/constants/storage.constants.ts +++ b/packages/web/src/common/constants/storage.constants.ts @@ -1,18 +1,10 @@ import { z } from "zod"; -export const StorageKeySchema = z.enum([ - "compass.reminder", - "compass.onboarding", - "compass.auth", -]); +export const StorageKeySchema = z.enum(["compass.reminder", "compass.auth"]); export type StorageKey = z.infer; -export const STORAGE_KEYS: Record< - "REMINDER" | "ONBOARDING_PROGRESS" | "AUTH", - StorageKey -> = { +export const STORAGE_KEYS: Record<"REMINDER" | "AUTH", StorageKey> = { REMINDER: "compass.reminder", - ONBOARDING_PROGRESS: "compass.onboarding", AUTH: "compass.auth", } as const; diff --git a/packages/web/src/common/utils/auth/google-auth.util.test.ts b/packages/web/src/common/utils/auth/google-auth.util.test.ts index 1d529da351..fa0cc533ab 100644 --- a/packages/web/src/common/utils/auth/google-auth.util.test.ts +++ b/packages/web/src/common/utils/auth/google-auth.util.test.ts @@ -1,19 +1,12 @@ import { AuthApi } from "@web/common/apis/auth.api"; -import { UserApi } from "@web/common/apis/user.api"; import { syncLocalEventsToCloud } from "@web/common/utils/sync/local-event-sync.util"; import { SignInUpInput } from "@web/components/oauth/ouath.types"; -import { - authenticate, - fetchOnboardingStatus, - syncLocalEvents, -} from "./google-auth.util"; +import { authenticate, syncLocalEvents } from "./google-auth.util"; jest.mock("@web/common/apis/auth.api"); -jest.mock("@web/common/apis/user.api"); jest.mock("@web/common/utils/sync/local-event-sync.util"); const mockAuthApi = AuthApi as jest.Mocked; -const mockUserApi = UserApi as jest.Mocked; const mockSyncLocalEventsToCloud = syncLocalEventsToCloud as jest.MockedFunction; @@ -70,32 +63,6 @@ describe("google-auth.util", () => { }); }); - describe("fetchOnboardingStatus", () => { - it("returns skipOnboarding from metadata", async () => { - mockUserApi.getMetadata.mockResolvedValue({ skipOnboarding: false }); - - const result = await fetchOnboardingStatus(); - - expect(result).toEqual({ skipOnboarding: false }); - }); - - it("returns skipOnboarding: true when metadata has undefined skipOnboarding", async () => { - mockUserApi.getMetadata.mockResolvedValue({}); - - const result = await fetchOnboardingStatus(); - - expect(result).toEqual({ skipOnboarding: true }); - }); - - it("returns skipOnboarding: true when metadata request fails", async () => { - mockUserApi.getMetadata.mockRejectedValue(new Error("Network error")); - - const result = await fetchOnboardingStatus(); - - expect(result).toEqual({ skipOnboarding: true }); - }); - }); - describe("syncLocalEvents", () => { it("returns syncedCount and success when sync succeeds", async () => { mockSyncLocalEventsToCloud.mockResolvedValue(5); diff --git a/packages/web/src/common/utils/auth/google-auth.util.ts b/packages/web/src/common/utils/auth/google-auth.util.ts index 4daa2b16e7..4d72f72766 100644 --- a/packages/web/src/common/utils/auth/google-auth.util.ts +++ b/packages/web/src/common/utils/auth/google-auth.util.ts @@ -1,5 +1,4 @@ import { AuthApi } from "@web/common/apis/auth.api"; -import { UserApi } from "@web/common/apis/user.api"; import { syncLocalEventsToCloud } from "@web/common/utils/sync/local-event-sync.util"; import { SignInUpInput } from "@web/components/oauth/ouath.types"; @@ -8,10 +7,6 @@ export interface AuthenticateResult { error?: Error; } -export interface OnboardingStatusResult { - skipOnboarding: boolean; -} - export interface SyncLocalEventsResult { syncedCount: number; success: boolean; @@ -32,19 +27,6 @@ export async function authenticate( } } -/** - * Fetch onboarding status from the server. - * Returns skipOnboarding: true as default if the request fails. - */ -export async function fetchOnboardingStatus(): Promise { - try { - const metadata = await UserApi.getMetadata(); - return { skipOnboarding: metadata.skipOnboarding ?? true }; - } catch { - return { skipOnboarding: true }; - } -} - /** * Sync local events to the cloud. */ diff --git a/packages/web/src/common/utils/toast/session-expired.toast.test.tsx b/packages/web/src/common/utils/toast/session-expired.toast.test.tsx index ba659ae454..85f7fd0ab6 100644 --- a/packages/web/src/common/utils/toast/session-expired.toast.test.tsx +++ b/packages/web/src/common/utils/toast/session-expired.toast.test.tsx @@ -27,9 +27,7 @@ describe("SessionExpiredToast", () => { render(); expect( - screen.getByText( - "Session expired. Please log in again to reconnect Google Calendar.", - ), + screen.getByText("Google Calendar connection expired. Please reconnect."), ).toBeInTheDocument(); expect( screen.getByRole("button", { name: /reconnect google calendar/i }), diff --git a/packages/web/src/common/utils/toast/session-expired.toast.tsx b/packages/web/src/common/utils/toast/session-expired.toast.tsx index 92e877888f..a9e5f2c32f 100644 --- a/packages/web/src/common/utils/toast/session-expired.toast.tsx +++ b/packages/web/src/common/utils/toast/session-expired.toast.tsx @@ -16,10 +16,10 @@ export const SessionExpiredToast = ({ toastId }: SessionExpiredToastProps) => { return (

- Session expired. Please log in again to reconnect Google Calendar. + Google Calendar connection expired. Please reconnect.

- ); -}; diff --git a/packages/web/src/views/Onboarding/components/OnboardingGuide.tsx b/packages/web/src/views/Onboarding/components/OnboardingGuide.tsx deleted file mode 100644 index a11c2f6af6..0000000000 --- a/packages/web/src/views/Onboarding/components/OnboardingGuide.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { FC, useCallback, useEffect, useRef, useState } from "react"; -import { selectImportResults } from "@web/ducks/events/selectors/sync.selector"; -import { importGCalSlice } from "@web/ducks/events/slices/sync.slice"; -import { useAppDispatch, useAppSelector } from "@web/store/store.hooks"; -import { OnboardingStepName } from "../constants/onboarding.constants"; -import { useCmdPaletteGuide } from "../hooks/useCmdPaletteGuide"; -import { useGuideOverlayState } from "../hooks/useGuideOverlayState"; -import { useStepDetection } from "../hooks/useStepDetection"; -import { - GuideInstructionContent, - GuideSuccessMessage, -} from "./GuideInstructionContent"; -import { GuideProgressIndicator } from "./GuideProgressIndicator"; -import { GuideSkipButton } from "./GuideSkipButton"; - -const AUTO_DISMISS_DELAY_MS = 8000; - -export const OnboardingGuide: FC = () => { - const dispatch = useAppDispatch(); - const importResults = useAppSelector(selectImportResults); - const { currentStep, completeStep, skipGuide, isGuideActive } = - useCmdPaletteGuide(); - const [isSuccessMessageDismissed, setIsSuccessMessageDismissed] = - useState(false); - const autoDismissTimerRef = useRef | null>( - null, - ); - - const { - welcomeMessage, - showSuccessMessage, - instructionParts, - actualStep, - stepText, - isNowViewOverlay, - } = useGuideOverlayState({ - currentStep, - isSuccessMessageDismissed, - hasImportResults: !!importResults, - }); - - useEffect(() => { - if (importResults) { - setIsSuccessMessageDismissed(false); - } - }, [importResults]); - - // Auto-dismiss timer for import results - useEffect(() => { - if (importResults && showSuccessMessage) { - autoDismissTimerRef.current = setTimeout(() => { - dispatch(importGCalSlice.actions.clearImportResults(undefined)); - setIsSuccessMessageDismissed(true); - }, AUTO_DISMISS_DELAY_MS); - } - - return () => { - if (autoDismissTimerRef.current) { - clearTimeout(autoDismissTimerRef.current); - autoDismissTimerRef.current = null; - } - }; - }, [importResults, showSuccessMessage, dispatch]); - - // Stable callback to prevent effect re-runs that reset task count tracking - const handleStepComplete = useCallback( - (step: OnboardingStepName) => { - completeStep(step); - }, - [completeStep], - ); - - // Unified step detection hook - handles all detection types - useStepDetection({ - currentStep, - onStepComplete: handleStepComplete, - }); - - const handleSkip = useCallback(() => { - if (showSuccessMessage) { - setIsSuccessMessageDismissed(true); - if (importResults) { - dispatch(importGCalSlice.actions.clearImportResults(undefined)); - } - return; - } - skipGuide(); - }, [showSuccessMessage, importResults, dispatch, skipGuide]); - - if (!isGuideActive && !showSuccessMessage) { - return null; - } - - const displayTitle = showSuccessMessage - ? "Welcome to Compass" - : welcomeMessage; - - return ( -
-
-
-
-

- {displayTitle} -

-

- {showSuccessMessage ? ( - - ) : ( - - )} -

- -
- -
-
-
- ); -}; diff --git a/packages/web/src/views/Onboarding/constants/onboarding.constants.ts b/packages/web/src/views/Onboarding/constants/onboarding.constants.ts deleted file mode 100644 index 4d3f07a84c..0000000000 --- a/packages/web/src/views/Onboarding/constants/onboarding.constants.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { ROOT_ROUTES } from "@web/common/constants/routes"; -import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants"; -import { - OnboardingGuideViewConfig, - OnboardingStepConfig, -} from "../types/onboarding.guide.types"; - -export type { OnboardingStepName } from "../types/onboarding.guide.types"; - -/** - * Command palette guide step names - * Used for tracking completion of the interactive command palette guide - */ -export const ONBOARDING_STEPS = { - NAVIGATE_TO_DAY: "navigateToDay", - CREATE_TASK: "createTask", - NAVIGATE_TO_NOW: "navigateToNow", - NAVIGATE_TO_WEEK: "navigateToWeek", - CONNECT_GOOGLE_CALENDAR: "connectGoogleCalendar", -} as const; - -/** - * Ordered array of onboarding step configurations - * This is the single source of truth for step order - */ -export const ONBOARDING_STEP_CONFIGS: readonly OnboardingStepConfig[] = [ - { - id: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - order: 0, - detectionType: "route", - detectionConfig: { - route: ROOT_ROUTES.DAY, - routePrefixes: [`${ROOT_ROUTES.DAY}/`], - }, - guide: { - instructionsByView: { - day: [{ type: "text", value: "You're already on the Day view." }], - default: [ - { type: "text", value: "Type " }, - { type: "kbd", value: VIEW_SHORTCUTS.day.key }, - { type: "text", value: " to go to the Day view" }, - ], - }, - }, - }, - { - id: ONBOARDING_STEPS.CREATE_TASK, - order: 1, - detectionType: "task-count", - guide: { - instructionsByView: { - day: [ - { type: "text", value: "Type " }, - { type: "kbd", value: "c" }, - { type: "text", value: " to create a task" }, - ], - }, - }, - }, - { - id: ONBOARDING_STEPS.NAVIGATE_TO_NOW, - order: 2, - detectionType: "route", - detectionConfig: { - route: ROOT_ROUTES.NOW, - routePrefixes: [`${ROOT_ROUTES.NOW}/`], - }, - guide: { - instructionsByView: { - default: [ - { type: "text", value: "Type " }, - { type: "kbd", value: VIEW_SHORTCUTS.now.key }, - { type: "text", value: " to go to the Now view" }, - ], - }, - }, - }, - { - id: ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - order: 3, - detectionType: "route", - detectionConfig: { route: ROOT_ROUTES.WEEK }, - guide: { - instructionsByView: { - default: [ - { type: "text", value: "Type " }, - { type: "kbd", value: VIEW_SHORTCUTS.week.key }, - { type: "text", value: " to go to the week view" }, - ], - }, - }, - }, - { - id: ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR, - order: 4, - detectionType: "google-auth", - guide: { - title: "Bring your events", - instructionsByView: { - default: [ - { type: "meta-key" }, - { type: "text", value: " + " }, - { type: "kbd", value: "K" }, - { type: "text", value: ", then select " }, - { type: "kbd", value: "Connect Google Calendar" }, - ], - }, - }, - }, -] as const; - -export const ONBOARDING_GUIDE_VIEWS: readonly OnboardingGuideViewConfig[] = [ - { - id: "now", - label: "Now", - routes: [ROOT_ROUTES.NOW], - routePrefixes: [`${ROOT_ROUTES.NOW}/`], - overlayVariant: "pinned", - }, - { - id: "day", - label: "Day", - routes: [ROOT_ROUTES.DAY], - routePrefixes: [`${ROOT_ROUTES.DAY}/`], - overlayVariant: "centered", - }, - { - id: "week", - label: "Week", - routes: [ROOT_ROUTES.WEEK], - overlayVariant: "centered", - }, - { - id: "unknown", - label: "Compass", - routes: [], - overlayVariant: "centered", - }, -] as const; - -/** - * Custom event name for restarting the onboarding guide - * Dispatched when user clicks "Re-do onboarding" in command palette - */ -export const ONBOARDING_RESTART_EVENT = "compass:restart-onboarding" as const; diff --git a/packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.test.ts b/packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.test.ts deleted file mode 100644 index d47333ff7d..0000000000 --- a/packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { act } from "react"; -import { renderHook } from "@testing-library/react"; -import { - ONBOARDING_RESTART_EVENT, - ONBOARDING_STEPS, -} from "../constants/onboarding.constants"; -import { - getOnboardingProgress, - loadCompletedSteps, - resetOnboardingProgress, - updateOnboardingProgress, -} from "../utils/onboarding.storage.util"; -import { useCmdPaletteGuide } from "./useCmdPaletteGuide"; - -describe("useCmdPaletteGuide", () => { - beforeEach(() => { - localStorage.clear(); - }); - - it("should initialize guide as active for new users", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - expect(result.current.isGuideActive).toBe(true); - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - it("should not initialize guide if already completed", () => { - updateOnboardingProgress({ isCompleted: true }); - - const { result } = renderHook(() => useCmdPaletteGuide()); - - expect(result.current.isGuideActive).toBe(false); - expect(result.current.currentStep).toBe(null); - const progress = getOnboardingProgress(); - expect(progress.isCompleted).toBe(true); - }); - - it("should persist step 1 completion to localStorage", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - expect(result.current.isGuideActive).toBe(true); - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - it("should advance to step 2 when step 1 is completed", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - expect(result.current.isGuideActive).toBe(true); - }); - - it("should advance to step 3 when step 2 is completed", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CREATE_TASK); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - expect(result.current.isGuideActive).toBe(true); - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.CREATE_TASK); - }); - - it("should advance to step 4 when step 3 is completed", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CREATE_TASK); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_WEEK); - expect(result.current.isGuideActive).toBe(true); - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - }); - - it("should complete guide when step 5 is completed", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CREATE_TASK); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_WEEK); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_WEEK); - }); - - expect(result.current.currentStep).toBe( - ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR, - ); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR); - }); - - expect(result.current.currentStep).toBe(null); - expect(result.current.isGuideActive).toBe(false); - const progress = getOnboardingProgress(); - expect(progress.isCompleted).toBe(true); - expect(loadCompletedSteps()).toContain( - ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR, - ); - }); - - it("should skip guide and clear completed steps", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - expect(result.current.isGuideActive).toBe(true); - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - // Complete step 1 first - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - // Then skip - act(() => { - result.current.skipGuide(); - }); - - expect(result.current.currentStep).toBe(null); - expect(result.current.isGuideActive).toBe(false); - const progress = getOnboardingProgress(); - expect(progress.isCompleted).toBe(true); - expect(loadCompletedSteps()).toEqual([]); - }); - - it("should complete guide directly and mark all steps as completed", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - act(() => { - result.current.completeGuide(); - }); - - expect(result.current.currentStep).toBe(null); - expect(result.current.isGuideActive).toBe(false); - const progress = getOnboardingProgress(); - expect(progress.isCompleted).toBe(true); - // Steps are marked in the order defined in ONBOARDING_STEP_CONFIGS - expect(loadCompletedSteps()).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR, - ]); - }); - - it("should resume from last completed step on remount", () => { - const { result: firstRender } = renderHook(() => useCmdPaletteGuide()); - - expect(firstRender.current.currentStep).toBe( - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ); - - act(() => { - firstRender.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(firstRender.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - // Unmount and remount - const { result: secondRender } = renderHook(() => useCmdPaletteGuide()); - - // Should resume at step 2 (next incomplete step) - expect(secondRender.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - expect(secondRender.current.isGuideActive).toBe(true); - }); - - it("should handle window being undefined gracefully", () => { - // The hook checks for typeof window === "undefined" internally - // This test verifies the hook doesn't crash when window is checked - // Note: We can't actually remove window in jsdom environment, but the hook - // has guards in place to handle SSR scenarios - const { result } = renderHook(() => useCmdPaletteGuide()); - - // Should initialize guide normally in test environment - expect(result.current.isGuideActive).toBe(true); - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - it("should restart guide when restart event is dispatched", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - // Complete a few steps first - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CREATE_TASK); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - expect(loadCompletedSteps()).toContain(ONBOARDING_STEPS.CREATE_TASK); - - // Reset storage and dispatch restart event - act(() => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }); - - // Should restart from the beginning - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - expect(result.current.isGuideActive).toBe(true); - expect(loadCompletedSteps()).toEqual([]); - }); - - it("should restart guide even when guide is completed", () => { - // Complete the entire guide first - const { result } = renderHook(() => useCmdPaletteGuide()); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CREATE_TASK); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_WEEK); - }); - - act(() => { - result.current.completeStep(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR); - }); - - expect(result.current.isGuideActive).toBe(false); - expect(result.current.currentStep).toBe(null); - const progress = getOnboardingProgress(); - expect(progress.isCompleted).toBe(true); - - // Reset and restart - act(() => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }); - - // Should restart from the beginning - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - expect(result.current.isGuideActive).toBe(true); - const newProgress = getOnboardingProgress(); - expect(newProgress.isCompleted).toBe(false); - }); - - it("should handle multiple restart events", () => { - const { result } = renderHook(() => useCmdPaletteGuide()); - - // Complete step 1 - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - - // First restart - act(() => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - // Complete step 1 again - act(() => { - result.current.completeStep(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.CREATE_TASK); - - // Second restart - act(() => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }); - - expect(result.current.currentStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - expect(result.current.isGuideActive).toBe(true); - }); -}); diff --git a/packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.ts b/packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.ts deleted file mode 100644 index 83481ea702..0000000000 --- a/packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { - ONBOARDING_RESTART_EVENT, - ONBOARDING_STEP_CONFIGS, - type OnboardingStepName, -} from "../constants/onboarding.constants"; -import { - clearCompletedSteps, - getOnboardingProgress, - markStepCompleted, - updateOnboardingProgress, -} from "../utils/onboarding.storage.util"; - -export type GuideStep = OnboardingStepName | null; - -interface UseCmdPaletteGuideReturn { - currentStep: GuideStep; - isGuideActive: boolean; - completeStep: (step: OnboardingStepName) => void; - skipGuide: () => void; - completeGuide: () => void; -} - -/** - * Helper function to initialize guide state based on onboarding progress - */ -function initializeGuideState( - setCurrentStep: (step: GuideStep) => void, - setIsGuideActive: (active: boolean) => void, -): void { - if (typeof window === "undefined") return; - - const progress = getOnboardingProgress(); - - if (progress.isCompleted) { - setIsGuideActive(false); - setCurrentStep(null); - return; - } - - // Load completed steps from onboarding progress - const completedSteps = progress.completedSteps; - - // Determine current step based on completed steps - // Find the first incomplete step using ordered configuration - let nextStep: GuideStep = null; - for (const stepConfig of ONBOARDING_STEP_CONFIGS) { - if (!completedSteps.includes(stepConfig.id)) { - nextStep = stepConfig.id; - break; - } - } - - if (nextStep !== null) { - setIsGuideActive(true); - setCurrentStep(nextStep); - } else { - // All steps completed, mark guide as completed - updateOnboardingProgress({ isCompleted: true }); - setIsGuideActive(false); - setCurrentStep(null); - } -} - -export function useCmdPaletteGuide(): UseCmdPaletteGuideReturn { - const [currentStep, setCurrentStep] = useState(null); - const [isGuideActive, setIsGuideActive] = useState(false); - - // Initialize guide state on mount and listen for restart events - useEffect(() => { - if (typeof window === "undefined") return; - - const initialize = () => { - initializeGuideState(setCurrentStep, setIsGuideActive); - }; - - // Initialize on mount - initialize(); - - // Listen for restart event - window.addEventListener(ONBOARDING_RESTART_EVENT, initialize); - - return () => { - window.removeEventListener(ONBOARDING_RESTART_EVENT, initialize); - }; - }, []); - - const completeStep = useCallback((step: OnboardingStepName) => { - // Mark step as completed in onboarding progress - markStepCompleted(step); - - // Find current step index in ordered configuration - const currentStepIndex = ONBOARDING_STEP_CONFIGS.findIndex( - (config) => config.id === step, - ); - - // Check if this is the last step - const isLastStep = currentStepIndex === ONBOARDING_STEP_CONFIGS.length - 1; - - if (isLastStep) { - // All steps completed - if (typeof window !== "undefined") { - updateOnboardingProgress({ isCompleted: true }); - } - setCurrentStep(null); - setIsGuideActive(false); - } else { - // Move to next step - const nextStepConfig = ONBOARDING_STEP_CONFIGS[currentStepIndex + 1]; - if (nextStepConfig) { - setCurrentStep(nextStepConfig.id); - } - } - }, []); - - const skipGuide = useCallback(() => { - if (typeof window !== "undefined") { - updateOnboardingProgress({ isCompleted: true }); - clearCompletedSteps(); - } - setCurrentStep(null); - setIsGuideActive(false); - }, []); - - const completeGuide = useCallback(() => { - if (typeof window !== "undefined") { - updateOnboardingProgress({ isCompleted: true }); - // Mark all steps as completed using ordered configuration - ONBOARDING_STEP_CONFIGS.forEach((config) => { - markStepCompleted(config.id); - }); - } - setCurrentStep(null); - setIsGuideActive(false); - }, []); - - return { - currentStep, - isGuideActive, - completeStep, - skipGuide, - completeGuide, - }; -} diff --git a/packages/web/src/views/Onboarding/hooks/useGuideOverlayState.test.ts b/packages/web/src/views/Onboarding/hooks/useGuideOverlayState.test.ts deleted file mode 100644 index 87c03b5c9c..0000000000 --- a/packages/web/src/views/Onboarding/hooks/useGuideOverlayState.test.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { useLocation } from "react-router-dom"; -import { renderHook } from "@testing-library/react"; -import { ROOT_ROUTES } from "@web/common/constants/routes"; -import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants"; -import { ONBOARDING_STEPS } from "../constants/onboarding.constants"; -import { markStepCompleted } from "../utils/onboarding.storage.util"; -import { useGuideOverlayState } from "./useGuideOverlayState"; - -jest.mock("react-router-dom", () => ({ - useLocation: jest.fn(), -})); - -const mockUseLocation = useLocation as jest.MockedFunction; - -describe("useGuideOverlayState", () => { - beforeEach(() => { - jest.clearAllMocks(); - localStorage.clear(); - }); - - describe("currentView", () => { - it("should return 'day' for /day pathname", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.currentView).toBe("day"); - }); - - it("should return 'now' for /now pathname", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.currentView).toBe("now"); - }); - - it("should return 'week' for /week pathname", () => { - mockUseLocation.mockReturnValue({ pathname: ROOT_ROUTES.WEEK } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.currentView).toBe("week"); - }); - }); - - describe("overlayVariant", () => { - it("should return 'pinned' for Now view", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.overlayVariant).toBe("pinned"); - }); - - it("should return 'centered' for Day view", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.overlayVariant).toBe("centered"); - }); - }); - - describe("actualStep", () => { - it("should return first incomplete step when step skipped ahead", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - // Current step is 3 but step 1 is not completed - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_NOW, - isSuccessMessageDismissed: false, - }), - ); - - // Should show step 1 since it's not completed - expect(result.current.actualStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - }); - - it("should return current step when previous steps are completed", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - markStepCompleted(ONBOARDING_STEPS.CREATE_TASK); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_NOW, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.actualStep).toBe(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - }); - - it("should return null when currentStep is null", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: null, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.actualStep).toBe(null); - }); - }); - - describe("welcomeMessage", () => { - it("should return correct message for Day view", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.welcomeMessage).toBe("Welcome to the Day View"); - }); - - it("should return correct message for Now view", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.welcomeMessage).toBe("Welcome to the Now View"); - }); - }); - - describe("showSuccessMessage", () => { - it("should be true when connectGoogleCalendar is completed and not dismissed", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - markStepCompleted(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: null, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.showSuccessMessage).toBe(true); - }); - - it("should be false when success message is dismissed", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - markStepCompleted(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: null, - isSuccessMessageDismissed: true, - }), - ); - - expect(result.current.showSuccessMessage).toBe(false); - }); - - it("should be false when connectGoogleCalendar is not completed", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.showSuccessMessage).toBe(false); - }); - }); - - describe("instructionParts", () => { - it("should return view-specific instructions when available", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.CREATE_TASK, - isSuccessMessageDismissed: false, - }), - ); - - // Step 2 has day-specific instructions - expect(result.current.instructionParts).toEqual([ - { type: "text", value: "Type " }, - { type: "kbd", value: "c" }, - { type: "text", value: " to create a task" }, - ]); - }); - - it("should fall back to default instructions when view-specific not available", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - markStepCompleted(ONBOARDING_STEPS.CREATE_TASK); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - isSuccessMessageDismissed: false, - }), - ); - - // Step 4 only has default instructions - expect(result.current.instructionParts).toEqual([ - { type: "text", value: "Type " }, - { type: "kbd", value: VIEW_SHORTCUTS.week.key }, - { type: "text", value: " to go to the week view" }, - ]); - }); - }); - - describe("stepNumber and stepText", () => { - it("should return correct step number for step 1", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.stepNumber).toBe(1); - expect(result.current.stepText).toBe("Step 1 of 5"); - }); - - it("should return correct step number for step 3", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - markStepCompleted(ONBOARDING_STEPS.CREATE_TASK); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_NOW, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.stepNumber).toBe(3); - expect(result.current.stepText).toBe("Step 3 of 5"); - }); - - it("should return 'All steps completed' when showing success message", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - markStepCompleted(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: null, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.stepNumber).toBe(null); - expect(result.current.stepText).toBe("All steps completed"); - }); - - it("should omit step text when showing import results", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: null, - isSuccessMessageDismissed: false, - hasImportResults: true, - }), - ); - - expect(result.current.stepNumber).toBe(null); - expect(result.current.stepText).toBeUndefined(); - }); - }); - - describe("isNowViewOverlay", () => { - it("should be true for Now view when not showing success message", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.isNowViewOverlay).toBe(true); - }); - - it("should be false for Now view when showing success message", () => { - mockUseLocation.mockReturnValue({ pathname: "/now" } as any); - markStepCompleted(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR); - - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: null, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.isNowViewOverlay).toBe(false); - }); - - it("should be false for Day view", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.isNowViewOverlay).toBe(false); - }); - }); - - describe("totalSteps", () => { - it("should return 7 (the total number of steps)", () => { - mockUseLocation.mockReturnValue({ pathname: "/day" } as any); - const { result } = renderHook(() => - useGuideOverlayState({ - currentStep: ONBOARDING_STEPS.NAVIGATE_TO_DAY, - isSuccessMessageDismissed: false, - }), - ); - - expect(result.current.totalSteps).toBe(5); - }); - }); -}); diff --git a/packages/web/src/views/Onboarding/hooks/useGuideOverlayState.ts b/packages/web/src/views/Onboarding/hooks/useGuideOverlayState.ts deleted file mode 100644 index 3d7c9db0bd..0000000000 --- a/packages/web/src/views/Onboarding/hooks/useGuideOverlayState.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { useMemo } from "react"; -import { useLocation } from "react-router-dom"; -import { - ONBOARDING_GUIDE_VIEWS, - ONBOARDING_STEPS, - ONBOARDING_STEP_CONFIGS, - OnboardingStepName, -} from "../constants/onboarding.constants"; -import { - OnboardingGuideView, - OnboardingInstructionPart, -} from "../types/onboarding.guide.types"; -import { isStepCompleted } from "../utils/onboarding.storage.util"; -import { - getGuideViewFromPathname, - getGuideWelcomeMessage, -} from "../utils/onboarding.util"; - -export interface GuideOverlayState { - /** Current view based on pathname (day, now, week, unknown) */ - currentView: OnboardingGuideView; - /** Overlay positioning variant - pinned for Now view, centered for others */ - overlayVariant: "pinned" | "centered"; - /** The actual step to display (first incomplete step) */ - actualStep: OnboardingStepName | null; - /** Config for the actual step */ - actualStepConfig: (typeof ONBOARDING_STEP_CONFIGS)[number] | null; - /** Welcome message based on current view */ - welcomeMessage: string; - /** Whether to show the success message (all steps completed) */ - showSuccessMessage: boolean; - /** Instruction parts for the current step and view */ - instructionParts: OnboardingInstructionPart[]; - /** Current step number (1-indexed) */ - stepNumber: number | null; - /** Total number of steps */ - totalSteps: number; - /** Step progress text (e.g., "Step 1 of 6") */ - stepText?: string; - /** Whether this is the Now view pinned variant */ - isNowViewOverlay: boolean; -} - -interface UseGuideOverlayStateOptions { - currentStep: OnboardingStepName | null; - isSuccessMessageDismissed: boolean; - hasImportResults?: boolean; -} - -/** - * Hook to compute the display state for the guide overlay. - * Extracts the complex computed logic from CmdPaletteGuide. - */ -export function useGuideOverlayState({ - currentStep, - isSuccessMessageDismissed, - hasImportResults = false, -}: UseGuideOverlayStateOptions): GuideOverlayState { - const location = useLocation(); - - const currentView = useMemo( - () => getGuideViewFromPathname(location.pathname), - [location.pathname], - ); - - const viewConfig = useMemo( - () => ONBOARDING_GUIDE_VIEWS.find((config) => config.id === currentView), - [currentView], - ); - - const overlayVariant = viewConfig?.overlayVariant ?? "centered"; - - // Determine actual step based on completion status - // If on step 2 but step 1 wasn't completed, show step 1 instead - const actualStep = useMemo(() => { - if (currentStep === null) return null; - const firstIncomplete = ONBOARDING_STEP_CONFIGS.find( - (config) => !isStepCompleted(config.id), - ); - return firstIncomplete?.id ?? currentStep; - }, [currentStep]); - - const actualStepConfig = useMemo( - () => - actualStep - ? (ONBOARDING_STEP_CONFIGS.find((config) => config.id === actualStep) ?? - null) - : null, - [actualStep], - ); - - const welcomeMessage = useMemo( - () => actualStepConfig?.guide.title ?? getGuideWelcomeMessage(currentView), - [actualStepConfig, currentView], - ); - - // Check if connectGoogleCalendar step is completed OR import results exist (show success message on any view) - const showSuccessMessage = - (isStepCompleted(ONBOARDING_STEPS.CONNECT_GOOGLE_CALENDAR) || - hasImportResults) && - !isSuccessMessageDismissed; - - const instructionParts = useMemo( - () => - actualStepConfig?.guide.instructionsByView[currentView] ?? - actualStepConfig?.guide.instructionsByView.default ?? - [], - [actualStepConfig, currentView], - ); - - const stepNumber = useMemo(() => { - if (showSuccessMessage) return null; - if (!actualStep) return 0; - const config = ONBOARDING_STEP_CONFIGS.find((c) => c.id === actualStep); - return config ? config.order + 1 : 0; - }, [actualStep, showSuccessMessage]); - - const shouldShowStepText = !(showSuccessMessage && hasImportResults); - const stepText = shouldShowStepText - ? showSuccessMessage - ? "All steps completed" - : `Step ${stepNumber} of ${ONBOARDING_STEP_CONFIGS.length}` - : undefined; - - const isNowViewOverlay = overlayVariant === "pinned" && !showSuccessMessage; - - return { - currentView, - overlayVariant, - actualStep, - actualStepConfig, - welcomeMessage, - showSuccessMessage, - instructionParts, - stepNumber, - totalSteps: ONBOARDING_STEP_CONFIGS.length, - stepText, - isNowViewOverlay, - }; -} diff --git a/packages/web/src/views/Onboarding/hooks/useOnboardingProgress.ts b/packages/web/src/views/Onboarding/hooks/useOnboardingProgress.ts deleted file mode 100644 index 00cd12dcc4..0000000000 --- a/packages/web/src/views/Onboarding/hooks/useOnboardingProgress.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import dayjs from "@core/util/date/dayjs"; -import { useDateInView } from "@web/views/Day/hooks/navigation/useDateInView"; - -export function useOnboardingProgress() { - const dateInView = useDateInView(); - const [hasNavigatedDates, setHasNavigatedDates] = useState(false); - const previousDateRef = useRef(dayjs().format("YYYY-MM-DD")); - // Track date navigation - useEffect(() => { - const currentDate = dateInView.format("YYYY-MM-DD"); - if (previousDateRef.current !== currentDate) { - setHasNavigatedDates(true); - previousDateRef.current = currentDate; - } - }, [dateInView]); - - return { hasNavigatedDates }; -} diff --git a/packages/web/src/views/Onboarding/hooks/useStepDetection.ts b/packages/web/src/views/Onboarding/hooks/useStepDetection.ts deleted file mode 100644 index 4bafd77c64..0000000000 --- a/packages/web/src/views/Onboarding/hooks/useStepDetection.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; -import { useSession } from "@web/auth/hooks/session/useSession"; -import { CompassTasksSavedEvent } from "@web/common/utils/storage/storage.types"; -import { - COMPASS_TASKS_SAVED_EVENT_NAME, - getDateKey, - loadTasksFromStorage, -} from "@web/common/utils/storage/storage.util"; -import { - ONBOARDING_STEP_CONFIGS, - type OnboardingStepName, -} from "../constants/onboarding.constants"; -import { isStepCompleted } from "../utils/onboarding.storage.util"; - -interface UseStepDetectionProps { - currentStep: OnboardingStepName | null; - onStepComplete: (step: OnboardingStepName) => void; -} - -/** - * Unified hook to detect step completion for all onboarding steps - * Dynamically sets up detection logic based on step configuration - */ -export function useStepDetection({ - currentStep, - onStepComplete, -}: UseStepDetectionProps): void { - const location = useLocation(); - const { authenticated } = useSession(); - - // Refs for tracking state across detection types - const initialTaskCountRef = useRef(null); - const hasCompletedRef = useRef(false); - - useEffect(() => { - if (currentStep === null) { - // Reset all refs when no step is active - hasCompletedRef.current = false; - initialTaskCountRef.current = null; - return; - } - - // Find step configuration - const stepConfig = ONBOARDING_STEP_CONFIGS.find( - (config) => config.id === currentStep, - ); - - if (!stepConfig) { - return; - } - - // Skip detection if step is already completed - if (isStepCompleted(currentStep)) { - return; - } - - if (hasCompletedRef.current) return; - - // Handle different detection types - switch (stepConfig.detectionType) { - case "task-count": { - // Initialize task count when step becomes active - const dateKey = getDateKey(); - const initialTasks = loadTasksFromStorage(dateKey); - initialTaskCountRef.current = initialTasks.length; - - if (typeof window === "undefined") return; - - const handleTasksSaved = (event: CompassTasksSavedEvent) => { - if (hasCompletedRef.current) return; - - const dateKey = getDateKey(); - if (event.detail.dateKey !== dateKey) return; - - const currentTasks = loadTasksFromStorage(dateKey); - const currentCount = currentTasks.length; - - // Check if a new task was added - if ( - initialTaskCountRef.current !== null && - currentCount > initialTaskCountRef.current - ) { - hasCompletedRef.current = true; - onStepComplete(currentStep); - } - }; - - window.addEventListener( - COMPASS_TASKS_SAVED_EVENT_NAME, - handleTasksSaved as EventListener, - ); - - return () => { - if (typeof window !== "undefined") { - window.removeEventListener( - COMPASS_TASKS_SAVED_EVENT_NAME, - handleTasksSaved as EventListener, - ); - } - }; - } - - case "route": { - const routeConfig = stepConfig.detectionConfig as - | { route: string; routePrefixes?: string[] } - | undefined; - if (!routeConfig) return; - - // Get route paths for comparison - const targetRoute = routeConfig.route; - const currentPath = location.pathname; - const matchesPrefix = - routeConfig.routePrefixes?.some((prefix) => - currentPath.startsWith(prefix), - ) ?? false; - - // Check if we're on the target route - if ( - !hasCompletedRef.current && - (currentPath === targetRoute || matchesPrefix) - ) { - hasCompletedRef.current = true; - onStepComplete(currentStep); - } - return; - } - - case "google-auth": { - // Complete when user is authenticated with Google Calendar - if (authenticated && !hasCompletedRef.current) { - hasCompletedRef.current = true; - onStepComplete(currentStep); - } - return; - } - - default: - return; - } - }, [currentStep, location.pathname, onStepComplete, authenticated]); - - // Reset refs when step changes - useEffect(() => { - return () => { - hasCompletedRef.current = false; - initialTaskCountRef.current = null; - }; - }, [currentStep]); -} diff --git a/packages/web/src/views/Onboarding/types/onboarding.guide.types.ts b/packages/web/src/views/Onboarding/types/onboarding.guide.types.ts deleted file mode 100644 index f376e98ad2..0000000000 --- a/packages/web/src/views/Onboarding/types/onboarding.guide.types.ts +++ /dev/null @@ -1,58 +0,0 @@ -export type OnboardingGuideView = "day" | "now" | "week" | "unknown"; - -export interface OnboardingGuideViewConfig { - id: OnboardingGuideView; - label: string; - routes: string[]; - routePrefixes?: string[]; - overlayVariant: "pinned" | "centered"; -} - -interface OnboardingInstructionText { - type: "text"; - value: string; -} - -interface OnboardingInstructionKey { - type: "kbd"; - value: string; -} - -interface OnboardingInstructionMetaKey { - type: "meta-key"; -} - -export type OnboardingInstructionPart = - | OnboardingInstructionText - | OnboardingInstructionKey - | OnboardingInstructionMetaKey; - -export type OnboardingInstructionVariant = OnboardingGuideView | "default"; - -export type OnboardingStepName = - | "navigateToDay" - | "createTask" - | "navigateToNow" - | "navigateToWeek" - | "connectGoogleCalendar"; - -/** - * Detection types for onboarding steps - */ -export type StepDetectionType = "task-count" | "route" | "google-auth"; - -/** - * Step configuration with order and detection metadata - */ -export interface OnboardingStepConfig { - id: OnboardingStepName; - order: number; - detectionType: StepDetectionType; - detectionConfig?: { route: string; routePrefixes?: string[] }; - guide: { - title?: string; - instructionsByView: Partial< - Record - >; - }; -} diff --git a/packages/web/src/views/Onboarding/utils/onboarding.storage.util.test.ts b/packages/web/src/views/Onboarding/utils/onboarding.storage.util.test.ts deleted file mode 100644 index 177bba2136..0000000000 --- a/packages/web/src/views/Onboarding/utils/onboarding.storage.util.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { - DEFAULT_ONBOARDING_PROGRESS, - OnboardingProgress, -} from "@web/common/constants/onboarding.constants"; -import { STORAGE_KEYS } from "@web/common/constants/storage.constants"; -import { ONBOARDING_STEPS } from "../constants/onboarding.constants"; -import { - clearCompletedSteps, - getOnboardingProgress, - isStepCompleted, - loadCompletedSteps, - markStepCompleted, - resetOnboardingProgress, - saveCompletedSteps, - updateOnboardingProgress, -} from "./onboarding.storage.util"; - -describe("onboarding.storage.util", () => { - beforeEach(() => { - localStorage.clear(); - }); - - describe("getOnboardingProgress", () => { - it("should return default progress when no data exists", () => { - const progress = getOnboardingProgress(); - expect(progress).toEqual(DEFAULT_ONBOARDING_PROGRESS); - }); - - it("should return stored progress from consolidated key", () => { - const testProgress: OnboardingProgress = { - completedSteps: [ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ], - isCompleted: false, - isSignupComplete: true, - isOnboardingSkipped: false, - isAuthPromptDismissed: true, - }; - localStorage.setItem( - STORAGE_KEYS.ONBOARDING_PROGRESS, - JSON.stringify(testProgress), - ); - const progress = getOnboardingProgress(); - expect(progress).toEqual(testProgress); - }); - - it("should handle invalid JSON gracefully", () => { - localStorage.setItem(STORAGE_KEYS.ONBOARDING_PROGRESS, "invalid json"); - const progress = getOnboardingProgress(); - const expected: OnboardingProgress = { - completedSteps: [], - isCompleted: false, - isSignupComplete: false, - isOnboardingSkipped: false, - isAuthPromptDismissed: false, - }; - expect(progress).toEqual(expected); - }); - - it("should handle invalid array format gracefully", () => { - const invalidFormatProgress = { - completedSteps: [1, 2, 4, 5, 0, -1, "invalid"], - isCompleted: false, - hasCompletedSignup: false, - skipOnboarding: false, - authPromptDismissed: false, - }; - localStorage.setItem( - STORAGE_KEYS.ONBOARDING_PROGRESS, - JSON.stringify(invalidFormatProgress), - ); - const progress = getOnboardingProgress(); - // Invalid format should be rejected and return default empty array - expect(progress.completedSteps).toEqual([]); - }); - }); - - describe("updateOnboardingProgress", () => { - it("should update onboarding progress", () => { - updateOnboardingProgress({ isAuthPromptDismissed: true }); - }); - - it("should merge partial updates", () => { - updateOnboardingProgress({ isAuthPromptDismissed: true }); - const progress = getOnboardingProgress(); - expect(progress.isAuthPromptDismissed).toBe(true); - }); - - it("should update completed steps", () => { - updateOnboardingProgress({ - completedSteps: [ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ], - }); - const progress = getOnboardingProgress(); - expect(progress.completedSteps).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ]); - }); - }); - - describe("loadCompletedSteps", () => { - it("should return empty array when no steps are stored", () => { - expect(loadCompletedSteps()).toEqual([]); - }); - - it("should return completed steps from onboarding progress", () => { - updateOnboardingProgress({ - completedSteps: [ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ], - }); - expect(loadCompletedSteps()).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ]); - }); - - it("should return all completed steps", () => { - updateOnboardingProgress({ - completedSteps: [ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - ], - }); - expect(loadCompletedSteps()).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - ]); - }); - }); - - describe("saveCompletedSteps", () => { - it("should save completed steps to onboarding progress", () => { - saveCompletedSteps([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ]); - const progress = getOnboardingProgress(); - expect(progress.completedSteps).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ]); - }); - - it("should save all steps correctly", () => { - saveCompletedSteps([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - ]); - const progress = getOnboardingProgress(); - expect(progress.completedSteps).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - ]); - }); - }); - - describe("isStepCompleted", () => { - it("should return false when step is not completed", () => { - expect(isStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY)).toBe(false); - }); - - it("should return true when step is completed", () => { - saveCompletedSteps([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ]); - expect(isStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY)).toBe(true); - expect(isStepCompleted(ONBOARDING_STEPS.CREATE_TASK)).toBe(true); - expect(isStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_NOW)).toBe(true); - expect(isStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_WEEK)).toBe(false); - }); - }); - - describe("markStepCompleted", () => { - it("should add step to completed steps", () => { - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - expect(isStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY)).toBe(true); - }); - - it("should not duplicate steps", () => { - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_DAY); - expect(loadCompletedSteps()).toEqual([ONBOARDING_STEPS.NAVIGATE_TO_DAY]); - }); - - it("should preserve existing completed steps", () => { - saveCompletedSteps([ONBOARDING_STEPS.NAVIGATE_TO_DAY]); - markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_NOW); - expect(loadCompletedSteps()).toEqual([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ]); - }); - }); - - describe("clearCompletedSteps", () => { - it("should remove completed steps from localStorage", () => { - saveCompletedSteps([ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ONBOARDING_STEPS.NAVIGATE_TO_WEEK, - ]); - clearCompletedSteps(); - expect(loadCompletedSteps()).toEqual([]); - }); - }); - - describe("resetOnboardingProgress", () => { - it("should remove the onboarding progress key from localStorage", () => { - // Set up some progress - updateOnboardingProgress({ - completedSteps: [ - ONBOARDING_STEPS.NAVIGATE_TO_DAY, - ONBOARDING_STEPS.CREATE_TASK, - ONBOARDING_STEPS.NAVIGATE_TO_NOW, - ], - isCompleted: true, - isSignupComplete: true, - isOnboardingSkipped: true, - isAuthPromptDismissed: true, - }); - - // Verify it exists - expect( - localStorage.getItem(STORAGE_KEYS.ONBOARDING_PROGRESS), - ).toBeTruthy(); - const progressBefore = getOnboardingProgress(); - expect(progressBefore.completedSteps.length).toBeGreaterThan(0); - expect(progressBefore.isCompleted).toBe(true); - - // Reset - resetOnboardingProgress(); - - // Verify key is removed - expect(localStorage.getItem(STORAGE_KEYS.ONBOARDING_PROGRESS)).toBeNull(); - - // Verify getOnboardingProgress returns defaults - const progressAfter = getOnboardingProgress(); - expect(progressAfter.completedSteps).toEqual([]); - expect(progressAfter.isCompleted).toBe(false); - expect(progressAfter.isSignupComplete).toBe(false); - expect(progressAfter.isOnboardingSkipped).toBe(false); - expect(progressAfter.isAuthPromptDismissed).toBe(false); - }); - - it("should handle reset when no progress exists", () => { - // Ensure no progress exists - localStorage.removeItem(STORAGE_KEYS.ONBOARDING_PROGRESS); - - // Should not throw - expect(() => resetOnboardingProgress()).not.toThrow(); - - // Should still return defaults - const progress = getOnboardingProgress(); - expect(progress.completedSteps).toEqual([]); - expect(progress.isCompleted).toBe(false); - }); - }); -}); diff --git a/packages/web/src/views/Onboarding/utils/onboarding.storage.util.ts b/packages/web/src/views/Onboarding/utils/onboarding.storage.util.ts deleted file mode 100644 index 3aebf5bd28..0000000000 --- a/packages/web/src/views/Onboarding/utils/onboarding.storage.util.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - DEFAULT_ONBOARDING_PROGRESS, - OnboardingProgress, - OnboardingProgressSchema, -} from "@web/common/constants/onboarding.constants"; -import { STORAGE_KEYS } from "@web/common/constants/storage.constants"; -import type { OnboardingStepName } from "../constants/onboarding.constants"; - -export function getOnboardingProgress(): OnboardingProgress { - if (typeof window === "undefined") return DEFAULT_ONBOARDING_PROGRESS; - - try { - const stored = localStorage.getItem(STORAGE_KEYS.ONBOARDING_PROGRESS); - if (stored) { - const parsed = JSON.parse(stored); - const result = OnboardingProgressSchema.safeParse(parsed); - if (result.success) { - return result.data; - } - } - - return DEFAULT_ONBOARDING_PROGRESS; - } catch { - return DEFAULT_ONBOARDING_PROGRESS; - } -} - -/** - * Update onboarding progress in localStorage - * Merges partial updates into existing progress - */ -export function updateOnboardingProgress( - updates: Partial, -): void { - if (typeof window === "undefined") return; - - try { - const current = getOnboardingProgress(); - const updated: OnboardingProgress = { - ...current, - ...updates, - completedSteps: updates.completedSteps ?? current.completedSteps, - }; - - // Validate with zod schema - const result = OnboardingProgressSchema.safeParse(updated); - if (result.success) { - localStorage.setItem( - STORAGE_KEYS.ONBOARDING_PROGRESS, - JSON.stringify(result.data), - ); - } - } catch { - // Silently fail if localStorage is unavailable - } -} - -/** - * Load completed steps from onboarding progress - * Returns an array of completed step names (e.g., ["createTask", "navigateToNow"]) - */ -export function loadCompletedSteps(): OnboardingStepName[] { - const progress = getOnboardingProgress(); - return progress.completedSteps; -} - -/** - * Save completed steps to onboarding progress - */ -export function saveCompletedSteps(steps: OnboardingStepName[]): void { - updateOnboardingProgress({ - completedSteps: steps, - }); -} - -/** - * Check if a specific step is completed - */ -export function isStepCompleted(step: OnboardingStepName): boolean { - const progress = getOnboardingProgress(); - return progress.completedSteps.includes(step); -} - -/** - * Mark a step as completed - */ -export function markStepCompleted(step: OnboardingStepName): void { - const progress = getOnboardingProgress(); - if (!progress.completedSteps.includes(step)) { - const updated = [...progress.completedSteps, step]; - updateOnboardingProgress({ completedSteps: updated }); - } -} - -/** - * Clear all completed steps (used for skip/reset) - */ -export function clearCompletedSteps(): void { - updateOnboardingProgress({ - completedSteps: [], - }); -} - -/** - * Reset onboarding progress by removing the localStorage key - * This completely clears all onboarding state - */ -export function resetOnboardingProgress(): void { - if (typeof window === "undefined") return; - - try { - localStorage.removeItem(STORAGE_KEYS.ONBOARDING_PROGRESS); - } catch { - // Silently fail if localStorage is unavailable - } -} diff --git a/packages/web/src/views/Onboarding/utils/onboarding.util.ts b/packages/web/src/views/Onboarding/utils/onboarding.util.ts deleted file mode 100644 index 83695f8c2a..0000000000 --- a/packages/web/src/views/Onboarding/utils/onboarding.util.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ONBOARDING_GUIDE_VIEWS } from "../constants/onboarding.constants"; -import { OnboardingGuideView } from "../types/onboarding.guide.types"; - -export const getGuideViewFromPathname = ( - pathname: string, -): OnboardingGuideView => { - for (const view of ONBOARDING_GUIDE_VIEWS) { - if (view.routes.some((route) => route === pathname)) { - return view.id; - } - if ( - view.routePrefixes?.some((prefix) => pathname.startsWith(prefix)) ?? - false - ) { - return view.id; - } - } - return "unknown"; -}; - -export const getGuideWelcomeMessage = (viewId: OnboardingGuideView): string => { - const view = ONBOARDING_GUIDE_VIEWS.find((config) => config.id === viewId); - if (!view || view.id === "unknown") { - return "Welcome to Compass"; - } - return `Welcome to the ${view.label} View`; -};