From a358770862b4c5161d851bb813f4a311b63d0767 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 24 Oct 2025 15:55:15 -0400 Subject: [PATCH 1/6] add LocationSensor for guide --- packages/client/src/clients/guide/client.ts | 5 ++-- packages/react/package.json | 6 ++++ packages/react/src/index.ts | 1 + .../LocationSensor/NextAppRouter.tsx | 22 +++++++++++++++ .../LocationSensor/NextPagesRouter.tsx | 28 +++++++++++++++++++ .../components/LocationSensor/helpers.ts | 9 ++++++ .../guide/components/LocationSensor/index.ts | 7 +++++ .../src/modules/guide/components/index.ts | 1 + packages/react/src/modules/guide/index.ts | 1 + yarn.lock | 4 +++ 10 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx create mode 100644 packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx create mode 100644 packages/react/src/modules/guide/components/LocationSensor/helpers.ts create mode 100644 packages/react/src/modules/guide/components/LocationSensor/index.ts diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts index ec521ad17..e661d7216 100644 --- a/packages/client/src/clients/guide/client.ts +++ b/packages/client/src/clients/guide/client.ts @@ -341,7 +341,7 @@ export class KnockGuideClient { cleanup() { this.unsubscribe(); - this.removeEventListeners(); + this.removeLocationChangeEventListeners(); this.clearGroupStage(); this.clearCounterInterval(); } @@ -1144,6 +1144,7 @@ export class KnockGuideClient { // Define as an arrow func property to always bind this to the class instance. private handleLocationChange = () => { + this.knock.log(`[Guide] .handleLocationChange`); const win = checkForWindow(); if (!win?.location) return; @@ -1223,7 +1224,7 @@ export class KnockGuideClient { } } - private removeEventListeners() { + removeLocationChangeEventListeners() { const win = checkForWindow(); if (!win?.history) return; diff --git a/packages/react/package.json b/packages/react/package.json index 2f5940c8a..f95b06588 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -46,9 +46,15 @@ "url": "https://github.com/knocklabs/javascript/issues" }, "peerDependencies": { + "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, + "peerDependenciesMeta": { + "next": { + "optional": true + } + }, "dependencies": { "@knocklabs/client": "workspace:^", "@knocklabs/react-core": "workspace:^", diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 80214ade0..9cf7266f0 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -44,6 +44,7 @@ export { CardView, KnockGuideProvider, GuideToolbar as KnockGuideToolbar, + GuideLocationSensor as KnockGuideLocationSensor, Modal, ModalView, } from "./modules/guide"; diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx new file mode 100644 index 000000000..2d9935a16 --- /dev/null +++ b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect } from 'react'; +import { usePathname } from 'next/navigation'; +import { useGuideContext } from "@knocklabs/react-core"; + +import { setLocation } from "./helpers"; + +export const NextAppRouter = () => { + const pathname = usePathname(); + const { client } = useGuideContext(); + + useEffect(() => { + client.removeLocationChangeEventListeners(); + }, [client]); + + useEffect(() => { + setLocation(client, pathname) + }, [client, pathname]); + + return null; +} diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx new file mode 100644 index 000000000..630c8a22e --- /dev/null +++ b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useGuideContext } from "@knocklabs/react-core"; + +import { setLocation } from "./helpers"; + +export const NextPagesRouter = () => { + const router = useRouter(); + const { client } = useGuideContext(); + + useEffect(() => { + client.removeLocationChangeEventListeners(); + }, [client]); + + useEffect(() => { + const handleRouteChangeComplete = (pathname: string) => { + setLocation(client, pathname) + }; + + router.events.on('routeChangeComplete', handleRouteChangeComplete); + + return () => { + router.events.off('routeChangeComplete', handleRouteChangeComplete); + }; + }, [client]); + + return null; +} diff --git a/packages/react/src/modules/guide/components/LocationSensor/helpers.ts b/packages/react/src/modules/guide/components/LocationSensor/helpers.ts new file mode 100644 index 000000000..541164d34 --- /dev/null +++ b/packages/react/src/modules/guide/components/LocationSensor/helpers.ts @@ -0,0 +1,9 @@ +import { KnockGuideClient } from "@knocklabs/client"; + +import { checkForWindow } from "../../../core/utils"; + +export const setLocation = (client: KnockGuideClient, pathname: string) => { + const win = checkForWindow(); + if (!win) return; + client.setLocation(win.location.origin + pathname) +} diff --git a/packages/react/src/modules/guide/components/LocationSensor/index.ts b/packages/react/src/modules/guide/components/LocationSensor/index.ts new file mode 100644 index 000000000..d21f4c2bf --- /dev/null +++ b/packages/react/src/modules/guide/components/LocationSensor/index.ts @@ -0,0 +1,7 @@ +import { NextAppRouter } from "./NextAppRouter"; +import { NextPagesRouter } from "./NextPagesRouter"; + +export const GuideLocationSensor = { + NextAppRouter, + NextPagesRouter +} diff --git a/packages/react/src/modules/guide/components/index.ts b/packages/react/src/modules/guide/components/index.ts index 8ebbde6b6..30970b02e 100644 --- a/packages/react/src/modules/guide/components/index.ts +++ b/packages/react/src/modules/guide/components/index.ts @@ -1,4 +1,5 @@ export { Banner, BannerView } from "./Banner"; export { Card, CardView } from "./Card"; export { GuideToolbar } from "./GuideToolbar"; +export { GuideLocationSensor } from "./LocationSensor"; export { Modal, ModalView } from "./Modal"; diff --git a/packages/react/src/modules/guide/index.ts b/packages/react/src/modules/guide/index.ts index ab8455a62..9be1581b2 100644 --- a/packages/react/src/modules/guide/index.ts +++ b/packages/react/src/modules/guide/index.ts @@ -6,5 +6,6 @@ export { Modal, ModalView, GuideToolbar, + GuideLocationSensor } from "./components"; export { KnockGuideProvider } from "./providers"; diff --git a/yarn.lock b/yarn.lock index 8d9c32e57..086366535 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4240,8 +4240,12 @@ __metadata: vitest: "npm:^3.1.1" vitest-axe: "npm:^0.1.0" peerDependencies: + next: ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + next: + optional: true languageName: unknown linkType: soft From f395c33b436e392c762659a00d26734e9c54790b Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 24 Oct 2025 16:48:07 -0400 Subject: [PATCH 2/6] format and lint fixes --- .../components/LocationSensor/NextAppRouter.tsx | 10 +++++----- .../components/LocationSensor/NextPagesRouter.tsx | 14 +++++++------- .../guide/components/LocationSensor/helpers.ts | 4 ++-- .../guide/components/LocationSensor/index.ts | 4 ++-- packages/react/src/modules/guide/index.ts | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx index 2d9935a16..02359fe03 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { useEffect } from 'react'; -import { usePathname } from 'next/navigation'; import { useGuideContext } from "@knocklabs/react-core"; +import { usePathname } from "next/navigation"; +import { useEffect } from "react"; import { setLocation } from "./helpers"; @@ -15,8 +15,8 @@ export const NextAppRouter = () => { }, [client]); useEffect(() => { - setLocation(client, pathname) + setLocation(client, pathname); }, [client, pathname]); return null; -} +}; diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx index 630c8a22e..d2a9e60ff 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react'; -import { useRouter } from 'next/router'; import { useGuideContext } from "@knocklabs/react-core"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; import { setLocation } from "./helpers"; @@ -14,15 +14,15 @@ export const NextPagesRouter = () => { useEffect(() => { const handleRouteChangeComplete = (pathname: string) => { - setLocation(client, pathname) + setLocation(client, pathname); }; - router.events.on('routeChangeComplete', handleRouteChangeComplete); + router.events.on("routeChangeComplete", handleRouteChangeComplete); return () => { - router.events.off('routeChangeComplete', handleRouteChangeComplete); + router.events.off("routeChangeComplete", handleRouteChangeComplete); }; - }, [client]); + }, [client, router]); return null; -} +}; diff --git a/packages/react/src/modules/guide/components/LocationSensor/helpers.ts b/packages/react/src/modules/guide/components/LocationSensor/helpers.ts index 541164d34..7ec737726 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/helpers.ts +++ b/packages/react/src/modules/guide/components/LocationSensor/helpers.ts @@ -5,5 +5,5 @@ import { checkForWindow } from "../../../core/utils"; export const setLocation = (client: KnockGuideClient, pathname: string) => { const win = checkForWindow(); if (!win) return; - client.setLocation(win.location.origin + pathname) -} + client.setLocation(win.location.origin + pathname); +}; diff --git a/packages/react/src/modules/guide/components/LocationSensor/index.ts b/packages/react/src/modules/guide/components/LocationSensor/index.ts index d21f4c2bf..0aa7ca9df 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/index.ts +++ b/packages/react/src/modules/guide/components/LocationSensor/index.ts @@ -3,5 +3,5 @@ import { NextPagesRouter } from "./NextPagesRouter"; export const GuideLocationSensor = { NextAppRouter, - NextPagesRouter -} + NextPagesRouter, +}; diff --git a/packages/react/src/modules/guide/index.ts b/packages/react/src/modules/guide/index.ts index 9be1581b2..c72dadb8c 100644 --- a/packages/react/src/modules/guide/index.ts +++ b/packages/react/src/modules/guide/index.ts @@ -6,6 +6,6 @@ export { Modal, ModalView, GuideToolbar, - GuideLocationSensor + GuideLocationSensor, } from "./components"; export { KnockGuideProvider } from "./providers"; From e55a61a89202df8ec273038cf3bc7c29470d9221 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 24 Oct 2025 17:10:15 -0400 Subject: [PATCH 3/6] fix build error --- packages/react/package.json | 1 + .../modules/guide/components/LocationSensor/NextPagesRouter.tsx | 2 +- yarn.lock | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index f95b06588..ab33419b3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -85,6 +85,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.14", "jsdom": "^27.0.0", + "next": "15.3.3", "react": "^19.0.0", "react-dom": "^19.0.0", "rimraf": "^6.0.1", diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx index d2a9e60ff..2fdd1cf15 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx @@ -22,7 +22,7 @@ export const NextPagesRouter = () => { return () => { router.events.off("routeChangeComplete", handleRouteChangeComplete); }; - }, [client, router]); + }, [client, router.events]); return null; }; diff --git a/yarn.lock b/yarn.lock index 086366535..4cca5f5b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4228,6 +4228,7 @@ __metadata: eslint-plugin-react-refresh: "npm:^0.4.14" jsdom: "npm:^27.0.0" lodash.debounce: "npm:^4.0.8" + next: "npm:15.3.3" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" rimraf: "npm:^6.0.1" From a513b039681801cedb262b15da3605101a74445d Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 24 Oct 2025 18:21:43 -0400 Subject: [PATCH 4/6] set initial location in LocationSensorNextPagesRouter --- packages/react/src/index.ts | 3 ++- .../LocationSensor/NextAppRouter.tsx | 2 +- .../LocationSensor/NextPagesRouter.tsx | 18 +++++++++++++----- .../guide/components/LocationSensor/index.ts | 9 ++------- .../src/modules/guide/components/index.ts | 5 ++++- packages/react/src/modules/guide/index.ts | 3 ++- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 9cf7266f0..18932e17a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -44,7 +44,8 @@ export { CardView, KnockGuideProvider, GuideToolbar as KnockGuideToolbar, - GuideLocationSensor as KnockGuideLocationSensor, + LocationSensorNextPagesRouter as KnockGuideLocationSensorNextPagesRouter, + LocationSensorNextAppRouter as KnockGuideLocationSensorNextAppRouter, Modal, ModalView, } from "./modules/guide"; diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx index 02359fe03..c4a28191d 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx @@ -6,7 +6,7 @@ import { useEffect } from "react"; import { setLocation } from "./helpers"; -export const NextAppRouter = () => { +export const LocationSensorNextAppRouter = () => { const pathname = usePathname(); const { client } = useGuideContext(); diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx index 2fdd1cf15..b91a0b649 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx @@ -4,25 +4,33 @@ import { useEffect } from "react"; import { setLocation } from "./helpers"; -export const NextPagesRouter = () => { +export const LocationSensorNextPagesRouter = () => { const router = useRouter(); const { client } = useGuideContext(); useEffect(() => { + // Set the initial location if not yet set. + if (!client.store.state.location) { + setLocation(client, router.pathname); + } + + // Remove any location chagne event listeners on the window object in case + // they are attached. client.removeLocationChangeEventListeners(); - }, [client]); - useEffect(() => { + // Attach a location change event listener from nextjs router. const handleRouteChangeComplete = (pathname: string) => { setLocation(client, pathname); }; - router.events.on("routeChangeComplete", handleRouteChangeComplete); return () => { router.events.off("routeChangeComplete", handleRouteChangeComplete); }; - }, [client, router.events]); + // We want to run this effect once per client instance and `router` is not + // guaranteed to be referentially stable. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [client]); return null; }; diff --git a/packages/react/src/modules/guide/components/LocationSensor/index.ts b/packages/react/src/modules/guide/components/LocationSensor/index.ts index 0aa7ca9df..2cfa1c531 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/index.ts +++ b/packages/react/src/modules/guide/components/LocationSensor/index.ts @@ -1,7 +1,2 @@ -import { NextAppRouter } from "./NextAppRouter"; -import { NextPagesRouter } from "./NextPagesRouter"; - -export const GuideLocationSensor = { - NextAppRouter, - NextPagesRouter, -}; +export { LocationSensorNextAppRouter } from "./NextAppRouter"; +export { LocationSensorNextPagesRouter } from "./NextPagesRouter"; diff --git a/packages/react/src/modules/guide/components/index.ts b/packages/react/src/modules/guide/components/index.ts index 30970b02e..dc019553e 100644 --- a/packages/react/src/modules/guide/components/index.ts +++ b/packages/react/src/modules/guide/components/index.ts @@ -1,5 +1,8 @@ export { Banner, BannerView } from "./Banner"; export { Card, CardView } from "./Card"; export { GuideToolbar } from "./GuideToolbar"; -export { GuideLocationSensor } from "./LocationSensor"; +export { + LocationSensorNextAppRouter, + LocationSensorNextPagesRouter, +} from "./LocationSensor"; export { Modal, ModalView } from "./Modal"; diff --git a/packages/react/src/modules/guide/index.ts b/packages/react/src/modules/guide/index.ts index c72dadb8c..a9517776d 100644 --- a/packages/react/src/modules/guide/index.ts +++ b/packages/react/src/modules/guide/index.ts @@ -6,6 +6,7 @@ export { Modal, ModalView, GuideToolbar, - GuideLocationSensor, + LocationSensorNextAppRouter, + LocationSensorNextPagesRouter, } from "./components"; export { KnockGuideProvider } from "./providers"; From 2e1568a2c7b448cdc320ad23eb929e4014c00c29 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 28 Oct 2025 17:20:08 -0400 Subject: [PATCH 5/6] account for query params and simplify the setLocation logic --- .../components/LocationSensor/NextAppRouter.tsx | 14 ++++++++++---- .../components/LocationSensor/NextPagesRouter.tsx | 15 ++++++++++----- .../guide/components/LocationSensor/helpers.ts | 9 --------- 3 files changed, 20 insertions(+), 18 deletions(-) delete mode 100644 packages/react/src/modules/guide/components/LocationSensor/helpers.ts diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx index c4a28191d..9a20a88c1 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextAppRouter.tsx @@ -1,13 +1,16 @@ "use client"; import { useGuideContext } from "@knocklabs/react-core"; -import { usePathname } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import { useEffect } from "react"; -import { setLocation } from "./helpers"; +import { checkForWindow } from "../../../core/utils"; export const LocationSensorNextAppRouter = () => { const pathname = usePathname(); + const searchParams = useSearchParams(); + const queryStr = searchParams.toString(); + const { client } = useGuideContext(); useEffect(() => { @@ -15,8 +18,11 @@ export const LocationSensorNextAppRouter = () => { }, [client]); useEffect(() => { - setLocation(client, pathname); - }, [client, pathname]); + const win = checkForWindow(); + if (!win) return; + + client.setLocation(win.location.href); + }, [client, pathname, queryStr]); return null; }; diff --git a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx index b91a0b649..986a67c3f 100644 --- a/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx +++ b/packages/react/src/modules/guide/components/LocationSensor/NextPagesRouter.tsx @@ -2,25 +2,30 @@ import { useGuideContext } from "@knocklabs/react-core"; import { useRouter } from "next/router"; import { useEffect } from "react"; -import { setLocation } from "./helpers"; +import { checkForWindow } from "../../../core/utils"; export const LocationSensorNextPagesRouter = () => { const router = useRouter(); const { client } = useGuideContext(); useEffect(() => { + const win = checkForWindow(); + if (!win) return; + // Set the initial location if not yet set. if (!client.store.state.location) { - setLocation(client, router.pathname); + client.setLocation(win.location.href); } // Remove any location chagne event listeners on the window object in case // they are attached. client.removeLocationChangeEventListeners(); - // Attach a location change event listener from nextjs router. - const handleRouteChangeComplete = (pathname: string) => { - setLocation(client, pathname); + // Attach a route change event listener to the nextjs router. Note, here url + // is the pathname and any query parameters of the new route but does not + // include the domain or origin. + const handleRouteChangeComplete = (url: string) => { + client.setLocation(win.location.origin + url); }; router.events.on("routeChangeComplete", handleRouteChangeComplete); diff --git a/packages/react/src/modules/guide/components/LocationSensor/helpers.ts b/packages/react/src/modules/guide/components/LocationSensor/helpers.ts deleted file mode 100644 index 7ec737726..000000000 --- a/packages/react/src/modules/guide/components/LocationSensor/helpers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { KnockGuideClient } from "@knocklabs/client"; - -import { checkForWindow } from "../../../core/utils"; - -export const setLocation = (client: KnockGuideClient, pathname: string) => { - const win = checkForWindow(); - if (!win) return; - client.setLocation(win.location.origin + pathname); -}; From d795cc6d77482d44871d5bd1598addfbb4440ada Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 28 Oct 2025 17:38:13 -0400 Subject: [PATCH 6/6] changeset --- .changeset/early-results-sip.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/early-results-sip.md diff --git a/.changeset/early-results-sip.md b/.changeset/early-results-sip.md new file mode 100644 index 000000000..8499f99af --- /dev/null +++ b/.changeset/early-results-sip.md @@ -0,0 +1,6 @@ +--- +"@knocklabs/client": patch +"@knocklabs/react": patch +--- + +[guides] add dedicated nextjs helper components for detecting location changes