diff --git a/.changeset/slimy-dogs-play.md b/.changeset/slimy-dogs-play.md new file mode 100644 index 000000000..e257ff222 --- /dev/null +++ b/.changeset/slimy-dogs-play.md @@ -0,0 +1,7 @@ +--- +"@knocklabs/react-core": patch +"@knocklabs/client": patch +"@knocklabs/react": patch +--- + +[Guide] Remove guide toolbar v1 support diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts index 162138e8b..37bc26a16 100644 --- a/packages/client/src/clients/guide/client.ts +++ b/packages/client/src/clients/guide/client.ts @@ -30,7 +30,6 @@ import { GuideData, GuideGroupAddedEvent, GuideGroupUpdatedEvent, - GuideLivePreviewUpdatedEvent, GuideRemovedEvent, GuideSocketEvent, GuideStepData, @@ -64,14 +63,6 @@ const DEFAULT_COUNTER_INCREMENT_INTERVAL = 30 * 1000; // in milliseconds // Maximum number of retry attempts for channel subscription const SUBSCRIBE_RETRY_LIMIT = 3; -// Debug query param keys -export const DEBUG_QUERY_PARAMS = { - GUIDE_KEY: "knock_guide_key", - PREVIEW_SESSION_ID: "knock_preview_session_id", -}; - -const DEBUG_STORAGE_KEY = "knock_guide_debug"; - // Return the global window object if defined, so to safely guard against SSR. const checkForWindow = () => { if (typeof window !== "undefined") { @@ -82,76 +73,6 @@ const checkForWindow = () => { export const guidesApiRootPath = (userId: string | undefined | null) => `/v1/users/${userId}/guides`; -// Detect debug params from URL or local storage -const detectDebugParams = (): DebugState => { - const win = checkForWindow(); - if (!win || !win.location) { - return { forcedGuideKey: null, previewSessionId: null }; - } - - const urlParams = new URLSearchParams(win.location.search); - const urlGuideKey = urlParams.get(DEBUG_QUERY_PARAMS.GUIDE_KEY); - const urlPreviewSessionId = urlParams.get( - DEBUG_QUERY_PARAMS.PREVIEW_SESSION_ID, - ); - - // If URL params exist, persist them to localStorage and return them - if (urlGuideKey || urlPreviewSessionId) { - if (win.localStorage) { - try { - const debugState = { - forcedGuideKey: urlGuideKey, - previewSessionId: urlPreviewSessionId, - }; - win.localStorage.setItem(DEBUG_STORAGE_KEY, JSON.stringify(debugState)); - } catch { - // Silently fail in privacy mode - } - } - return { - forcedGuideKey: urlGuideKey, - previewSessionId: urlPreviewSessionId, - }; - } - - // Check local storage if no URL params - let storedGuideKey = null; - let storedPreviewSessionId = null; - - if (win.localStorage) { - try { - const storedDebugState = win.localStorage.getItem(DEBUG_STORAGE_KEY); - if (storedDebugState) { - const parsedDebugState = safeJsonParseDebugParams(storedDebugState); - storedGuideKey = parsedDebugState.forcedGuideKey; - storedPreviewSessionId = parsedDebugState.previewSessionId; - } - } catch { - // Silently fail in privacy mode - } - } - - return { - forcedGuideKey: storedGuideKey, - previewSessionId: storedPreviewSessionId, - }; -}; - -const safeJsonParseDebugParams = (value: string): DebugState => { - try { - const parsed = JSON.parse(value); - return { - forcedGuideKey: parsed?.forcedGuideKey ?? null, - previewSessionId: parsed?.previewSessionId ?? null, - }; - } catch { - return { - forcedGuideKey: null, - previewSessionId: null, - }; - } -}; - type SelectQueryMetadata = { limit: SelectQueryLimit; opts: SelectGuideOpts; @@ -239,7 +160,7 @@ const predicate = ( }; export const checkActivatable = ( - guide: KnockGuide, + guide: Pick, location: string | undefined, ) => { const url = location ? newUrl(location) : undefined; @@ -272,7 +193,6 @@ export class KnockGuideClient { "guide.removed", "guide_group.added", "guide_group.updated", - "guide.live_preview_updated", ]; private subscribeRetryCount = 0; @@ -295,15 +215,11 @@ export class KnockGuideClient { ) { const { trackLocationFromWindow = true, - // TODO(KNO-11523): Remove once we ship guide toolbar v2, and offload as - // much debugging specific logic and responsibilities to toolbar. - trackDebugParams = false, throttleCheckInterval = DEFAULT_COUNTER_INCREMENT_INTERVAL, } = options; const win = checkForWindow(); const location = trackLocationFromWindow ? win?.location?.href : undefined; - const debug = trackDebugParams ? detectDebugParams() : undefined; this.store = new Store({ guideGroups: [], @@ -315,7 +231,6 @@ export class KnockGuideClient { location, // Increment to update the state store and trigger re-selection. counter: 0, - debug, }); // In server environments we might not have a socket connection. @@ -530,9 +445,6 @@ export class KnockGuideClient { case "guide_group.updated": return this.addOrReplaceGuideGroup(payload); - case "guide.live_preview_updated": - return this.updatePreviewGuide(payload); - default: return; } @@ -560,45 +472,6 @@ export class KnockGuideClient { }); } - exitDebugMode() { - this.knock.log("[Guide] Exiting debug mode"); - - // Clear localStorage debug params - const win = checkForWindow(); - if (win?.localStorage) { - try { - win.localStorage.removeItem(DEBUG_STORAGE_KEY); - } catch { - // Silently fail in privacy mode - } - } - - // Clear debug state from store - this.store.setState((state) => ({ - ...state, - debug: { - forcedGuideKey: null, - previewSessionId: null, - focusedGuideKeys: {}, - }, - previewGuides: {}, // Clear preview guides when exiting debug mode - })); - - // Remove URL query params if present - // Only update the URL if params need to be cleared to avoid unnecessary navigations - if (win?.location) { - const url = new URL(win.location.href); - if ( - url.searchParams.has(DEBUG_QUERY_PARAMS.GUIDE_KEY) || - url.searchParams.has(DEBUG_QUERY_PARAMS.PREVIEW_SESSION_ID) - ) { - url.searchParams.delete(DEBUG_QUERY_PARAMS.GUIDE_KEY); - url.searchParams.delete(DEBUG_QUERY_PARAMS.PREVIEW_SESSION_ID); - win.location.href = url.toString(); - } - } - } - setDebug(debugOpts?: Omit) { this.knock.log("[Guide] .setDebug()"); @@ -1383,15 +1256,6 @@ export class KnockGuideClient { }); } - private updatePreviewGuide({ data }: GuideLivePreviewUpdatedEvent) { - const guide = this.localCopy(data.guide); - - this.store.setState((state) => { - const previewGuides = { ...state.previewGuides, [guide.key]: guide }; - return { ...state, previewGuides }; - }); - } - // Define as an arrow func property to always bind this to the class instance. private handleLocationChange = () => { this.knock.log(`[Guide] .handleLocationChange`); @@ -1403,43 +1267,9 @@ export class KnockGuideClient { this.knock.log(`[Guide] Detected a location change: ${href}`); - if (!this.options.trackDebugParams) { - this.setLocation(href); - return; - } - - // TODO(KNO-11523): Remove below once we ship toolbar v2. - - // If entering debug mode, fetch all guides. - const currentDebugParams = this.store.state.debug || {}; - const newDebugParams = detectDebugParams(); - - this.setLocation(href, { debug: newDebugParams }); - - // If debug state has changed, refetch guides and resubscribe to the websocket channel - const debugStateChanged = this.checkDebugStateChanged( - currentDebugParams, - newDebugParams, - ); - - if (debugStateChanged) { - this.knock.log( - `[Guide] Debug state changed, refetching guides and resubscribing to the websocket channel`, - ); - this.fetch(); - this.subscribe(); - } + this.setLocation(href); }; - // Returns whether debug params have changed. For guide key, we only check - // presence since the exact value has no impact on fetch/subscribe - private checkDebugStateChanged(a: DebugState, b: DebugState): boolean { - return ( - Boolean(a.forcedGuideKey) !== Boolean(b.forcedGuideKey) || - a.previewSessionId !== b.previewSessionId - ); - } - private listenForLocationChangesFromWindow() { const win = checkForWindow(); if (win?.history && win?.addEventListener) { diff --git a/packages/client/src/clients/guide/index.ts b/packages/client/src/clients/guide/index.ts index 0834dd155..5b2096e4e 100644 --- a/packages/client/src/clients/guide/index.ts +++ b/packages/client/src/clients/guide/index.ts @@ -1,8 +1,4 @@ -export { - KnockGuideClient, - DEBUG_QUERY_PARAMS, - checkActivatable, -} from "./client"; +export { KnockGuideClient, checkActivatable } from "./client"; export { checkStateIfThrottled } from "./helpers"; export type { ToolbarV2RunConfig } from "./helpers"; export type { diff --git a/packages/client/src/clients/guide/types.ts b/packages/client/src/clients/guide/types.ts index 250ed6159..d56dfcc20 100644 --- a/packages/client/src/clients/guide/types.ts +++ b/packages/client/src/clients/guide/types.ts @@ -161,8 +161,7 @@ type SocketEventType = | "guide.updated" | "guide.removed" | "guide_group.added" - | "guide_group.updated" - | "guide.live_preview_updated"; + | "guide_group.updated"; type SocketEventPayload = { topic: string; @@ -195,18 +194,12 @@ export type GuideGroupUpdatedEvent = SocketEventPayload< { guide_group: GuideGroupData } >; -export type GuideLivePreviewUpdatedEvent = SocketEventPayload< - "guide.live_preview_updated", - { guide: GuideData; eligible: boolean } ->; - export type GuideSocketEvent = | GuideAddedEvent | GuideUpdatedEvent | GuideRemovedEvent | GuideGroupAddedEvent - | GuideGroupUpdatedEvent - | GuideLivePreviewUpdatedEvent; + | GuideGroupUpdatedEvent; // // Guide client @@ -285,7 +278,6 @@ export type TargetParams = { export type ConstructorOpts = { trackLocationFromWindow?: boolean; - trackDebugParams?: boolean; orderResolutionDuration?: number; throttleCheckInterval?: number; }; diff --git a/packages/client/test/clients/guide/guide.test.ts b/packages/client/test/clients/guide/guide.test.ts index 0c1a3e234..1895324f9 100644 --- a/packages/client/test/clients/guide/guide.test.ts +++ b/packages/client/test/clients/guide/guide.test.ts @@ -166,7 +166,6 @@ describe("KnockGuideClient", () => { queries: {}, location: undefined, counter: 0, - debug: undefined, }); }); @@ -195,7 +194,6 @@ describe("KnockGuideClient", () => { queries: {}, location: "https://example.com", counter: 0, - debug: undefined, }); }); @@ -212,33 +210,7 @@ describe("KnockGuideClient", () => { queries: {}, location: undefined, counter: 0, - debug: undefined, - }); - }); - - test("handles localStorage errors gracefully during initialization", () => { - const mockLocalStorageWithErrors = { - getItem: vi.fn().mockImplementation(() => { - throw new Error("Privacy mode or quota exceeded"); - }), - setItem: vi.fn().mockImplementation(() => { - throw new Error("Privacy mode or quota exceeded"); - }), - }; - - vi.stubGlobal("window", { - location: { - search: - "?knock_guide_key=test_guide&knock_preview_session_id=session123", - }, - localStorage: mockLocalStorageWithErrors, }); - - expect(() => { - new KnockGuideClient(mockKnock, channelId, {}, { trackDebugParams: true }); - }).not.toThrow(); - - expect(mockLocalStorageWithErrors.setItem).toHaveBeenCalled(); }); test("starts the counter interval clock and sets the interval id", () => { @@ -3714,102 +3686,6 @@ describe("KnockGuideClient", () => { expect(windowWithHistory.history.replaceState).toBe(originalReplaceState); }); - test("handleLocationChange calls subscribe when entering debug mode", () => { - const mockLocalStorage = { - getItem: vi.fn(), - setItem: vi.fn(), - }; - - vi.stubGlobal("window", { - ...mockWindow, - location: { - href: "https://example.com/dashboard?knock_guide_key=test_guide", - search: "?knock_guide_key=test_guide", - }, - localStorage: mockLocalStorage, - }); - - const client = new KnockGuideClient( - mockKnock, - channelId, - {}, - { trackLocationFromWindow: true, trackDebugParams: true }, - ); - - client.store.state.debug = { forcedGuideKey: null }; - client.store.state.location = "https://example.com/dashboard"; - - const subscribeSpy = vi - .spyOn(client, "subscribe") - .mockImplementation(() => {}); - - const fetchSpy = vi - .spyOn(client, "fetch") - .mockImplementation(() => Promise.resolve({ status: "ok" })); - - client["handleLocationChange"](); - - expect(fetchSpy).toHaveBeenCalled(); - expect(subscribeSpy).toHaveBeenCalled(); - - // Should persist debug parameters to localStorage when detected in URL - expect(mockLocalStorage.setItem).toHaveBeenCalledWith( - "knock_guide_debug", - JSON.stringify({ - forcedGuideKey: "test_guide", - previewSessionId: null, - }), - ); - }); - - test("handleLocationChange calls subscribe when exiting debug mode", () => { - const mockLocalStorage = { - getItem: vi.fn().mockImplementation((key: string) => { - // Simulate localStorage having stored debug values from previous session - if (key === "knock_guide_debug") { - return JSON.stringify({ - forcedGuideKey: "stored_guide", - previewSessionId: "stored_session", - }); - } - return null; - }), - setItem: vi.fn(), - }; - - vi.stubGlobal("window", { - ...mockWindow, - location: { - href: "https://example.com/dashboard", - search: "", - }, - localStorage: mockLocalStorage, - }); - - const client = new KnockGuideClient( - mockKnock, - channelId, - {}, - { trackLocationFromWindow: true, trackDebugParams: true }, - ); - - client.store.state.debug = { forcedGuideKey: "test_guide" }; - client.store.state.location = - "https://example.com/dashboard?knock_guide_key=test_guide"; - - const subscribeSpy = vi - .spyOn(client, "subscribe") - .mockImplementation(() => {}); - - client["handleLocationChange"](); - - expect(subscribeSpy).toHaveBeenCalled(); - - // Should read from localStorage when no URL parameters are present - expect(mockLocalStorage.getItem).toHaveBeenCalledWith( - "knock_guide_debug", - ); - }); }); describe("private methods", () => { diff --git a/packages/react-core/src/modules/guide/context/KnockGuideProvider.tsx b/packages/react-core/src/modules/guide/context/KnockGuideProvider.tsx index 17594e70e..eef983345 100644 --- a/packages/react-core/src/modules/guide/context/KnockGuideProvider.tsx +++ b/packages/react-core/src/modules/guide/context/KnockGuideProvider.tsx @@ -23,7 +23,6 @@ export type KnockGuideProviderProps = { colorMode?: ColorMode; targetParams?: KnockGuideTargetParams; trackLocationFromWindow?: boolean; - trackDebugParams?: boolean; orderResolutionDuration?: number; // in milliseconds throttleCheckInterval?: number; // in milliseconds }; @@ -37,10 +36,6 @@ export const KnockGuideProvider: React.FC< colorMode = "light", targetParams = {}, trackLocationFromWindow = true, - // Whether the guide client should look for debug params in url/local storage - // to launch guide toolbar. Set to true if using toolbar v1. - // TODO(KNO-11523): Remove this once we ship v2. - trackDebugParams = false, // Default to 0 which works well for react apps as this "yields" to react for // one render cyle first and close the group stage. orderResolutionDuration = 0, @@ -60,7 +55,6 @@ export const KnockGuideProvider: React.FC< const knockGuideClient = React.useMemo(() => { return new KnockGuideClient(knock, channelId, stableTargetParams, { trackLocationFromWindow, - trackDebugParams, orderResolutionDuration, throttleCheckInterval, }); @@ -69,7 +63,6 @@ export const KnockGuideProvider: React.FC< channelId, stableTargetParams, trackLocationFromWindow, - trackDebugParams, orderResolutionDuration, throttleCheckInterval, ]); diff --git a/packages/react/src/modules/guide/components/Toolbar/V1/V1.tsx b/packages/react/src/modules/guide/components/Toolbar/V1/V1.tsx deleted file mode 100644 index 1f47d17d6..000000000 --- a/packages/react/src/modules/guide/components/Toolbar/V1/V1.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useGuideContext, useStore } from "@knocklabs/react-core"; -import { Button } from "@telegraph/button"; -import { Stack } from "@telegraph/layout"; -import { Tag } from "@telegraph/tag"; -import { Text } from "@telegraph/typography"; -import { Minimize2, Undo2, Wrench } from "lucide-react"; -import { useState } from "react"; - -import { KnockButton } from "../KnockButton"; -import { TOOLBAR_Z_INDEX } from "../shared"; -import "../styles.css"; - -export const V1 = () => { - const [isCollapsed, setIsCollapsed] = useState(false); - - const { client } = useGuideContext(); - const debugState = useStore(client.store, (state) => state.debug); - - if (!debugState?.forcedGuideKey) { - return null; - } - - const handleExit = () => { - client.exitDebugMode(); - }; - - const handleToggleCollapse = () => { - setIsCollapsed(!isCollapsed); - }; - - if (isCollapsed) { - return ; - } - - return ( - - - - Debug - - - - {debugState.forcedGuideKey} - - - - -