From 81e7869bbea52adaf775164f178bef1d79b09f76 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 18 Feb 2026 15:46:26 -0800 Subject: [PATCH 1/2] refactor(onboarding): remove onboarding-related code and constants This commit removes the onboarding state management and related components, including hooks and constants, to simplify the codebase. The onboarding guide and its associated UI elements have been eliminated, streamlining the user experience and reducing complexity in the application. Additionally, references to onboarding in various components and tests have been cleaned up. --- e2e/utils/event-test-utils.ts | 5 - e2e/utils/oauth-test-utils.ts | 9 +- e2e/utils/test-constants.ts | 8 - .../auth/hooks/oauth/useGoogleAuth.test.ts | 76 ---- .../web/src/auth/hooks/oauth/useGoogleAuth.ts | 11 - .../onboarding/useIsSignupComplete.test.ts | 69 ---- .../hooks/onboarding/useIsSignupComplete.ts | 30 -- .../hooks/onboarding/useSkipOnboarding.ts | 32 -- .../common/constants/onboarding.constants.ts | 30 -- .../src/common/constants/storage.constants.ts | 12 +- .../utils/auth/google-auth.util.test.ts | 35 +- .../src/common/utils/auth/google-auth.util.ts | 18 - .../AuthenticatedLayout.tsx | 2 - packages/web/src/routers/index.tsx | 4 +- packages/web/src/routers/loaders.ts | 11 - .../web/src/views/CmdPalette/CmdPalette.tsx | 11 - .../Day/components/DayCmdPalette.test.tsx | 29 -- .../views/Day/components/DayCmdPalette.tsx | 11 - .../onboarding/useCmdPaletteTutorial.test.tsx | 104 ------ .../hooks/onboarding/useCmdPaletteTutorial.ts | 46 --- .../Now/components/NowCmdPalette.test.tsx | 22 -- .../views/Now/components/NowCmdPalette.tsx | 11 - .../components/GuideInstructionContent.tsx | 97 ----- .../components/GuideProgressIndicator.tsx | 54 --- .../Onboarding/components/GuideSkipButton.tsx | 45 --- .../Onboarding/components/OnboardingGuide.tsx | 148 -------- .../constants/onboarding.constants.ts | 145 ------- .../hooks/useCmdPaletteGuide.test.ts | 327 ---------------- .../Onboarding/hooks/useCmdPaletteGuide.ts | 144 ------- .../hooks/useGuideOverlayState.test.ts | 353 ------------------ .../Onboarding/hooks/useGuideOverlayState.ts | 140 ------- .../Onboarding/hooks/useOnboardingProgress.ts | 19 - .../Onboarding/hooks/useStepDetection.ts | 150 -------- .../types/onboarding.guide.types.ts | 58 --- .../utils/onboarding.storage.util.test.ts | 282 -------------- .../utils/onboarding.storage.util.ts | 116 ------ .../views/Onboarding/utils/onboarding.util.ts | 27 -- 37 files changed, 8 insertions(+), 2683 deletions(-) delete mode 100644 packages/web/src/auth/hooks/onboarding/useIsSignupComplete.test.ts delete mode 100644 packages/web/src/auth/hooks/onboarding/useIsSignupComplete.ts delete mode 100644 packages/web/src/auth/hooks/onboarding/useSkipOnboarding.ts delete mode 100644 packages/web/src/common/constants/onboarding.constants.ts delete mode 100644 packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.test.tsx delete mode 100644 packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.ts delete mode 100644 packages/web/src/views/Onboarding/components/GuideInstructionContent.tsx delete mode 100644 packages/web/src/views/Onboarding/components/GuideProgressIndicator.tsx delete mode 100644 packages/web/src/views/Onboarding/components/GuideSkipButton.tsx delete mode 100644 packages/web/src/views/Onboarding/components/OnboardingGuide.tsx delete mode 100644 packages/web/src/views/Onboarding/constants/onboarding.constants.ts delete mode 100644 packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.test.ts delete mode 100644 packages/web/src/views/Onboarding/hooks/useCmdPaletteGuide.ts delete mode 100644 packages/web/src/views/Onboarding/hooks/useGuideOverlayState.test.ts delete mode 100644 packages/web/src/views/Onboarding/hooks/useGuideOverlayState.ts delete mode 100644 packages/web/src/views/Onboarding/hooks/useOnboardingProgress.ts delete mode 100644 packages/web/src/views/Onboarding/hooks/useStepDetection.ts delete mode 100644 packages/web/src/views/Onboarding/types/onboarding.guide.types.ts delete mode 100644 packages/web/src/views/Onboarding/utils/onboarding.storage.util.test.ts delete mode 100644 packages/web/src/views/Onboarding/utils/onboarding.storage.util.ts delete mode 100644 packages/web/src/views/Onboarding/utils/onboarding.util.ts 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/components/AuthenticatedLayout/AuthenticatedLayout.tsx b/packages/web/src/components/AuthenticatedLayout/AuthenticatedLayout.tsx index 4c2a19c70e..c62f311872 100644 --- a/packages/web/src/components/AuthenticatedLayout/AuthenticatedLayout.tsx +++ b/packages/web/src/components/AuthenticatedLayout/AuthenticatedLayout.tsx @@ -1,7 +1,6 @@ import { Outlet } from "react-router-dom"; import { SyncEventsOverlay } from "@web/components/SyncEventsOverlay/SyncEventsOverlay"; import { useGlobalShortcuts } from "@web/views/Calendar/hooks/shortcuts/useGlobalShortcuts"; -import { OnboardingGuide } from "@web/views/Onboarding/components/OnboardingGuide"; /** * Layout component for authenticated routes @@ -14,7 +13,6 @@ export const AuthenticatedLayout = () => { return ( <> - ); diff --git a/packages/web/src/routers/index.tsx b/packages/web/src/routers/index.tsx index e0a0c586ac..8fc0404dd5 100644 --- a/packages/web/src/routers/index.tsx +++ b/packages/web/src/routers/index.tsx @@ -8,8 +8,8 @@ import { IS_DEV } from "@web/common/constants/env.constants"; import { ROOT_ROUTES } from "@web/common/constants/routes"; import { AbsoluteOverflowLoader } from "@web/components/AbsoluteOverflowLoader"; import { + loadAuthenticated, loadDayData, - loadOnboardingStatus, loadRootData, loadSpecificDayData, } from "@web/routers/loaders"; @@ -37,7 +37,7 @@ export const router = createBrowserRouter( Component: module.RootView, }), ), - loader: loadOnboardingStatus, + loader: loadAuthenticated, children: [ { path: ROOT_ROUTES.NOW, diff --git a/packages/web/src/routers/loaders.ts b/packages/web/src/routers/loaders.ts index 2543f3e6f5..a02b94a6f2 100644 --- a/packages/web/src/routers/loaders.ts +++ b/packages/web/src/routers/loaders.ts @@ -2,7 +2,6 @@ import { LoaderFunctionArgs, redirect } from "react-router-dom"; import { zYearMonthDayString } from "@core/types/type.utils"; import dayjs, { Dayjs } from "@core/util/date/dayjs"; import { ROOT_ROUTES } from "@web/common/constants/routes"; -import { getOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; export interface DayLoaderData { dateInView: Dayjs; // in UTC @@ -23,16 +22,6 @@ export async function loadLogoutData() { return { authenticated }; } -export async function loadOnboardingStatus() { - const { authenticated } = await loadAuthenticated(); - const { - isOnboardingSkipped: skipOnboarding, - isSignupComplete: hasCompletedSignup, - } = getOnboardingProgress(); - - return { authenticated, skipOnboarding, hasCompletedSignup }; -} - export function loadTodayData(): DayLoaderData { const dateInView = dayjs(); const dateFormat = dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT; diff --git a/packages/web/src/views/CmdPalette/CmdPalette.tsx b/packages/web/src/views/CmdPalette/CmdPalette.tsx index 147920c6a2..fcc8e97e6a 100644 --- a/packages/web/src/views/CmdPalette/CmdPalette.tsx +++ b/packages/web/src/views/CmdPalette/CmdPalette.tsx @@ -25,8 +25,6 @@ import { selectIsCmdPaletteOpen } from "@web/ducks/settings/selectors/settings.s import { settingsSlice } from "@web/ducks/settings/slices/settings.slice"; import { useAppDispatch, useAppSelector } from "@web/store/store.hooks"; import { ShortcutProps } from "@web/views/Calendar/hooks/shortcuts/useWeekShortcuts"; -import { ONBOARDING_RESTART_EVENT } from "@web/views/Onboarding/constants/onboarding.constants"; -import { resetOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; const CmdPalette = ({ today, @@ -148,15 +146,6 @@ const CmdPalette = ({ ? undefined : onConnectGoogleCalendar, }, - { - id: "redo-onboarding", - children: "Re-do onboarding", - icon: "ArrowPathIcon", - onClick: () => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }, - }, { id: "log-out", children: "Log Out [z]", diff --git a/packages/web/src/views/Day/components/DayCmdPalette.test.tsx b/packages/web/src/views/Day/components/DayCmdPalette.test.tsx index 8dd9147aa8..20dbf77e88 100644 --- a/packages/web/src/views/Day/components/DayCmdPalette.test.tsx +++ b/packages/web/src/views/Day/components/DayCmdPalette.test.tsx @@ -11,8 +11,6 @@ import { getModifierKey } from "@web/common/utils/shortcut/shortcut.util"; import { settingsSlice } from "@web/ducks/settings/slices/settings.slice"; import { useGlobalShortcuts } from "@web/views/Calendar/hooks/shortcuts/useGlobalShortcuts"; import { DayCmdPalette } from "@web/views/Day/components/DayCmdPalette"; -import { ONBOARDING_RESTART_EVENT } from "@web/views/Onboarding/constants/onboarding.constants"; -import { resetOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; // Mock react-router-dom const mockNavigate = jest.fn(); @@ -33,12 +31,6 @@ jest.mock("@core/util/date/dayjs", () => ({ })), })); -// Mock resetOnboardingProgress -jest.mock("@web/views/Onboarding/utils/onboarding.storage.util", () => ({ - ...jest.requireActual("@web/views/Onboarding/utils/onboarding.storage.util"), - resetOnboardingProgress: jest.fn(), -})); - // Mock dispatch const mockDispatch = jest.fn(); @@ -98,7 +90,6 @@ describe("DayCmdPalette", () => { expect( screen.getByText("Go to Today (Monday, November 24) [t]"), ).toBeInTheDocument(); - expect(screen.getByText("Re-do onboarding")).toBeInTheDocument(); expect(screen.getByText("Log Out [z]")).toBeInTheDocument(); }); @@ -177,26 +168,6 @@ describe("DayCmdPalette", () => { expect(mockOnGoToToday).toHaveBeenCalled(); }); - it("resets onboarding and dispatches restart event when Re-do onboarding is clicked", async () => { - const user = userEvent.setup(); - const mockDispatchEvent = jest.spyOn(window, "dispatchEvent"); - await act(() => - render(, { - state: { settings: { isCmdPaletteOpen: true } }, - }), - ); - - await act(() => user.click(screen.getByText("Re-do onboarding"))); - - expect(resetOnboardingProgress).toHaveBeenCalled(); - expect(mockDispatchEvent).toHaveBeenCalledWith( - expect.objectContaining({ - type: ONBOARDING_RESTART_EVENT, - }), - ); - mockDispatchEvent.mockRestore(); - }); - it("navigates to logout when Log Out is clicked", async () => { const user = userEvent.setup(); await act(() => diff --git a/packages/web/src/views/Day/components/DayCmdPalette.tsx b/packages/web/src/views/Day/components/DayCmdPalette.tsx index 5e4b71b472..1b35dc2b1c 100644 --- a/packages/web/src/views/Day/components/DayCmdPalette.tsx +++ b/packages/web/src/views/Day/components/DayCmdPalette.tsx @@ -13,8 +13,6 @@ import { import { selectIsCmdPaletteOpen } from "@web/ducks/settings/selectors/settings.selectors"; import { settingsSlice } from "@web/ducks/settings/slices/settings.slice"; import { useAppDispatch, useAppSelector } from "@web/store/store.hooks"; -import { ONBOARDING_RESTART_EVENT } from "@web/views/Onboarding/constants/onboarding.constants"; -import { resetOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; interface DayCmdPaletteProps { onGoToToday?: () => void; @@ -85,15 +83,6 @@ export const DayCmdPalette = ({ onGoToToday }: DayCmdPaletteProps) => { ? undefined : onConnectGoogleCalendar, }, - { - id: "redo-onboarding", - children: "Re-do onboarding", - icon: "ArrowPathIcon", - onClick: () => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }, - }, { id: "log-out", children: "Log Out [z]", diff --git a/packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.test.tsx b/packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.test.tsx deleted file mode 100644 index 3310d5e7b2..0000000000 --- a/packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Provider } from "react-redux"; -import { configureStore } from "@reduxjs/toolkit"; -import { renderHook, waitFor } from "@testing-library/react"; -import { settingsSlice } from "@web/ducks/settings/slices/settings.slice"; -import { useCmdPaletteTutorial } from "./useCmdPaletteTutorial"; - -// Mock useSession -jest.mock("@web/auth/hooks/session/useSession", () => ({ - useSession: jest.fn(() => ({ - authenticated: false, - setAuthenticated: jest.fn(), - })), -})); - -const createTestStore = (isCmdPaletteOpen = false) => { - return configureStore({ - reducer: { - settings: settingsSlice.reducer, - }, - preloadedState: { - settings: { - isCmdPaletteOpen: isCmdPaletteOpen, - }, - }, - }); -}; - -describe("useCmdPaletteTutorial", () => { - beforeEach(() => { - localStorage.clear(); - }); - - it("should show cmd palette tutorial after onboarding overlay is dismissed", async () => { - const store = createTestStore(); - - const { result } = renderHook( - () => useCmdPaletteTutorial({ showOnboardingOverlay: false }), - { - wrapper: ({ children }) => ( - {children} - ), - }, - ); - - await waitFor( - () => { - expect(result.current.showCmdPaletteTutorial).toBe(true); - }, - { timeout: 2000 }, - ); - }); - - it("should not show cmd palette tutorial if onboarding overlay is still showing", () => { - const store = createTestStore(); - - const { result } = renderHook( - () => useCmdPaletteTutorial({ showOnboardingOverlay: true }), - { - wrapper: ({ children }) => ( - {children} - ), - }, - ); - - expect(result.current.showCmdPaletteTutorial).toBe(false); - }); - - it("should not show cmd palette tutorial for authenticated users", () => { - const { useSession } = require("@web/auth/hooks/session/useSession"); - const mockSession = { - authenticated: true, - setAuthenticated: jest.fn(), - }; - useSession.mockReturnValue(mockSession); - const store = createTestStore(); - - const { result } = renderHook( - () => useCmdPaletteTutorial({ showOnboardingOverlay: false }), - { - wrapper: ({ children }) => ( - {children} - ), - }, - ); - - expect(result.current.showCmdPaletteTutorial).toBe(false); - }); - - it("should dismiss cmd palette tutorial", () => { - const store = createTestStore(); - - const { result } = renderHook( - () => useCmdPaletteTutorial({ showOnboardingOverlay: false }), - { - wrapper: ({ children }) => ( - {children} - ), - }, - ); - - result.current.dismissCmdPaletteTutorial(); - expect(result.current.showCmdPaletteTutorial).toBe(false); - }); -}); diff --git a/packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.ts b/packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.ts deleted file mode 100644 index bc9411cb76..0000000000 --- a/packages/web/src/views/Day/hooks/onboarding/useCmdPaletteTutorial.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSession } from "@web/auth/hooks/session/useSession"; - -interface UseCmdPaletteTutorialProps { - showOnboardingOverlay: boolean; -} - -interface UseCmdPaletteTutorialReturn { - showCmdPaletteTutorial: boolean; - dismissCmdPaletteTutorial: () => void; -} - -/** - * Hook to manage the cmd palette tutorial visibility - * Shows tutorial after onboarding overlay is dismissed for unauthenticated users - * Automatically marks tutorial as seen when cmd palette is opened - */ -export function useCmdPaletteTutorial({ - showOnboardingOverlay, -}: UseCmdPaletteTutorialProps): UseCmdPaletteTutorialReturn { - const { authenticated } = useSession(); - const [showCmdPaletteTutorial, setShowCmdPaletteTutorial] = useState(false); - - useEffect(() => { - if (typeof window === "undefined") return; - - // Show tutorial after onboarding overlay is dismissed, for unauthenticated users - if (!authenticated && !showOnboardingOverlay) { - // Small delay to show tutorial after overlay - const timer = setTimeout(() => { - setShowCmdPaletteTutorial(true); - }, 1000); - - return () => clearTimeout(timer); - } - }, [authenticated, showOnboardingOverlay]); - - const dismissCmdPaletteTutorial = () => { - setShowCmdPaletteTutorial(false); - }; - - return { - showCmdPaletteTutorial, - dismissCmdPaletteTutorial, - }; -} diff --git a/packages/web/src/views/Now/components/NowCmdPalette.test.tsx b/packages/web/src/views/Now/components/NowCmdPalette.test.tsx index 7f7bf87ae6..6f310ba42f 100644 --- a/packages/web/src/views/Now/components/NowCmdPalette.test.tsx +++ b/packages/web/src/views/Now/components/NowCmdPalette.test.tsx @@ -2,8 +2,6 @@ import { fireEvent, screen } from "@testing-library/react"; import { render } from "@web/__tests__/__mocks__/mock.render"; import { pressKey } from "@web/common/utils/dom/event-emitter.util"; import { NowCmdPalette } from "@web/views/Now/components/NowCmdPalette"; -import { ONBOARDING_RESTART_EVENT } from "@web/views/Onboarding/constants/onboarding.constants"; -import { resetOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; // Mock pressKey jest.mock("@web/common/utils/dom/event-emitter.util", () => ({ @@ -15,12 +13,6 @@ jest.mock("@web/common/utils/dom/event-target-visibility.util", () => ({ onEventTargetVisibility: (cb: () => void) => () => cb(), })); -// Mock resetOnboardingProgress -jest.mock("@web/views/Onboarding/utils/onboarding.storage.util", () => ({ - ...jest.requireActual("@web/views/Onboarding/utils/onboarding.storage.util"), - resetOnboardingProgress: jest.fn(), -})); - // Mock useSession for auth state tests const mockSetAuthenticated = jest.fn(); let mockAuthenticated = false; @@ -75,7 +67,6 @@ describe("NowCmdPalette", () => { expect(screen.getByText("Go to Day [d]")).toBeInTheDocument(); expect(screen.getByText("Go to Week [w]")).toBeInTheDocument(); expect(screen.getByText("Edit Reminder [r]")).toBeInTheDocument(); - expect(screen.getByText("Re-do onboarding")).toBeInTheDocument(); expect(screen.getByText("Log Out [z]")).toBeInTheDocument(); }); @@ -103,19 +94,6 @@ describe("NowCmdPalette", () => { expect(pressKey).toHaveBeenCalledWith("z"); }); - it("should reset onboarding and dispatch restart event when 'Re-do onboarding' is clicked", () => { - const mockDispatchEvent = jest.spyOn(window, "dispatchEvent"); - render(, { state: initialState }); - fireEvent.click(screen.getByText("Re-do onboarding")); - expect(resetOnboardingProgress).toHaveBeenCalled(); - expect(mockDispatchEvent).toHaveBeenCalledWith( - expect.objectContaining({ - type: ONBOARDING_RESTART_EVENT, - }), - ); - mockDispatchEvent.mockRestore(); - }); - describe("Google Calendar authentication status", () => { beforeEach(() => { mockLogin.mockClear(); diff --git a/packages/web/src/views/Now/components/NowCmdPalette.tsx b/packages/web/src/views/Now/components/NowCmdPalette.tsx index 8c51a5654b..28cae9df01 100644 --- a/packages/web/src/views/Now/components/NowCmdPalette.tsx +++ b/packages/web/src/views/Now/components/NowCmdPalette.tsx @@ -9,8 +9,6 @@ import { onEventTargetVisibility } from "@web/common/utils/dom/event-target-visi import { selectIsCmdPaletteOpen } from "@web/ducks/settings/selectors/settings.selectors"; import { settingsSlice } from "@web/ducks/settings/slices/settings.slice"; import { useAppDispatch, useAppSelector } from "@web/store/store.hooks"; -import { ONBOARDING_RESTART_EVENT } from "@web/views/Onboarding/constants/onboarding.constants"; -import { resetOnboardingProgress } from "@web/views/Onboarding/utils/onboarding.storage.util"; export const NowCmdPalette = () => { const dispatch = useAppDispatch(); @@ -62,15 +60,6 @@ export const NowCmdPalette = () => { ? undefined : onConnectGoogleCalendar, }, - { - id: "redo-onboarding", - children: "Re-do onboarding", - icon: "ArrowPathIcon", - onClick: () => { - resetOnboardingProgress(); - window.dispatchEvent(new CustomEvent(ONBOARDING_RESTART_EVENT)); - }, - }, { id: "log-out", children: "Log Out [z]", diff --git a/packages/web/src/views/Onboarding/components/GuideInstructionContent.tsx b/packages/web/src/views/Onboarding/components/GuideInstructionContent.tsx deleted file mode 100644 index 1fda00e3f6..0000000000 --- a/packages/web/src/views/Onboarding/components/GuideInstructionContent.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { FC } from "react"; -import { DesktopOS, getDesktopOS } from "@web/common/utils/device/device.util"; -import { ImportResults } from "@web/ducks/events/slices/sync.slice"; -import { OnboardingInstructionPart } from "../types/onboarding.guide.types"; - -const KBD_STYLES = - "bg-bg-secondary text-text-light border-border-primary rounded border px-1.5 py-0.5 font-mono text-xs"; - -interface GuideInstructionContentProps { - instructionParts: OnboardingInstructionPart[]; -} - -interface GuideSuccessMessageProps { - importResults?: ImportResults | null; -} - -const getMetaKeyLabel = (): string => { - const os = getDesktopOS(); - return os === DesktopOS.MacOS ? "⌘" : "Ctrl"; -}; - -export const GuideInstructionContent: FC = ({ - instructionParts, -}) => { - return ( - <> - {instructionParts.map((part, index) => { - if (part.type === "kbd") { - return ( - - {part.value} - - ); - } - if (part.type === "meta-key") { - return ( - - {getMetaKeyLabel()} - - ); - } - return {part.value}; - })} - - ); -}; - -const formatImportMessage = ( - importResults: ImportResults, -): { mainLine: string | null; secondaryLine: string | null } => { - const { eventsCount, calendarsCount, localEventsSynced } = importResults; - let mainLine: string | null = null; - let secondaryLine: string | null = null; - - // Google Calendar import line - const importParts = []; - if (eventsCount !== undefined) { - importParts.push(`${eventsCount} event${eventsCount !== 1 ? "s" : ""}`); - } - if (calendarsCount !== undefined) { - importParts.push( - `${calendarsCount} calendar${calendarsCount !== 1 ? "s" : ""}`, - ); - } - if (importParts.length > 0) { - mainLine = `Imported ${importParts.join(" from ")}`; - } - - // Local events sync line - if (localEventsSynced !== undefined && localEventsSynced > 0) { - secondaryLine = `${localEventsSynced} local event${localEventsSynced !== 1 ? "s" : ""} synced to the cloud`; - } - - return { mainLine, secondaryLine }; -}; - -export const GuideSuccessMessage: FC = ({ - importResults, -}) => { - if (importResults) { - const { mainLine, secondaryLine } = formatImportMessage(importResults); - - if (!mainLine && !secondaryLine) { - return <>Your calendar has been synced successfully!; - } - - return ( - <> - {mainLine && {mainLine}} - {mainLine && secondaryLine &&
} - {secondaryLine && {secondaryLine}} - - ); - } - - return <>You're all set!; -}; diff --git a/packages/web/src/views/Onboarding/components/GuideProgressIndicator.tsx b/packages/web/src/views/Onboarding/components/GuideProgressIndicator.tsx deleted file mode 100644 index c8a596c488..0000000000 --- a/packages/web/src/views/Onboarding/components/GuideProgressIndicator.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FC } from "react"; -import { - ONBOARDING_STEP_CONFIGS, - OnboardingStepName, -} from "../constants/onboarding.constants"; -import { isStepCompleted } from "../utils/onboarding.storage.util"; - -interface GuideProgressIndicatorProps { - actualStep: OnboardingStepName | null; - showSuccessMessage: boolean; - stepText?: string; -} - -export const GuideProgressIndicator: FC = ({ - actualStep, - showSuccessMessage, - stepText, -}) => { - if (!stepText) { - return null; - } - - const actualStepIndex = actualStep - ? (ONBOARDING_STEP_CONFIGS.find((config) => config.id === actualStep) - ?.order ?? -1) - : -1; - - return ( -
- {ONBOARDING_STEP_CONFIGS.map((stepConfig) => { - const stepIndex = stepConfig.order; - const isCompleted = - showSuccessMessage || - stepIndex < actualStepIndex || - isStepCompleted(stepConfig.id); - const isCurrent = !showSuccessMessage && stepConfig.id === actualStep; - - return ( -
- ); - })} - {stepText} -
- ); -}; diff --git a/packages/web/src/views/Onboarding/components/GuideSkipButton.tsx b/packages/web/src/views/Onboarding/components/GuideSkipButton.tsx deleted file mode 100644 index 94f52903bb..0000000000 --- a/packages/web/src/views/Onboarding/components/GuideSkipButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { FC } from "react"; - -interface GuideSkipButtonProps { - onClick: () => void; - showSuccessMessage: boolean; - isNowViewOverlay: boolean; -} - -export const GuideSkipButton: FC = ({ - onClick, - showSuccessMessage, - isNowViewOverlay, -}) => { - const ariaLabel = showSuccessMessage ? "Dismiss" : "Skip guide"; - - return ( - - ); -}; 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`; -}; From 71edb384fcf06567564718c448f62169e308751e Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Wed, 18 Feb 2026 15:55:33 -0800 Subject: [PATCH 2/2] fix(toast): update session expired message for clarity This commit modifies the text displayed in the SessionExpiredToast component to provide a clearer message regarding the Google Calendar connection status. The test for the SessionExpiredToast has also been updated to reflect this change. --- .../web/src/common/utils/toast/session-expired.toast.test.tsx | 4 +--- packages/web/src/common/utils/toast/session-expired.toast.tsx | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) 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.