From 58e9f19f822e57acb367f4f0bb69544be1ec0c05 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 14 Oct 2026 20:01:10 +0200 Subject: [PATCH] fix(web): a What's new link opens the panel after the sign-in round trip too A portal link with ?whats-new= opened without a session flashed the panel, went through the sign-in, and came back without it: MSAL lands on the origin root and AuthGate reaches the saved link by a client-side navigation, while WhatsNewPanelHost (root layout, stays mounted) read the parameter only once on mount. It now follows the search parameter and opens once per arrival; a protected page waits for the session, so the panel no longer flashes before the redirect or counts the open twice. Co-Authored-By: Claude Opus 5.5 --- .../components/WhatsNewPanelHost.tsx | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Web/autopilot-monitor-web/components/WhatsNewPanelHost.tsx b/src/Web/autopilot-monitor-web/components/WhatsNewPanelHost.tsx index 4863aa05..da60e5b2 100644 --- a/src/Web/autopilot-monitor-web/components/WhatsNewPanelHost.tsx +++ b/src/Web/autopilot-monitor-web/components/WhatsNewPanelHost.tsx @@ -1,12 +1,13 @@ "use client"; -import { useEffect } from "react"; -import { usePathname } from "next/navigation"; +import { Suspense, useEffect, useRef } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; import { WhatsNewPanel } from "./WhatsNewPanel"; import { useWhatsNewState, openWhatsNew } from "@/lib/whatsNewStore"; import { trackEvent } from "@/lib/appInsights"; import { isPublicPath } from "@/lib/hostRouting"; import { WHATS_NEW_CHANNELS, type WhatsNewChannel } from "@/lib/whatsNew"; +import { useAuth } from "@/contexts/AuthContext"; /** * Mounts the What's new panel once, in the root layout, so the portal navbar, the landing @@ -24,16 +25,43 @@ export function WhatsNewPanelHost() { const pathname = usePathname(); const onPublicPage = isPublicPath(pathname); + return ( + <> + {/* useSearchParams() needs a Suspense boundary for the static prerender. */} + + + + {panelOpen && } + + ); +} + +/** + * Opens the panel once each time `?whats-new=` arrives, not only on the first mount: a portal + * link opened without a session goes through the sign-in, which lands on the origin root and + * reaches the link again by a client-side navigation (AuthGate), where this layout stays + * mounted. A protected page waits for the session, so the panel neither flashes before the + * sign-in redirect nor counts the open twice. + */ +function WhatsNewDeepLink({ onPublicPage }: { onPublicPage: boolean }) { + const requested = useSearchParams()?.get("whats-new") ?? null; + const { isAuthenticated, isLoading } = useAuth(); + const ready = onPublicPage || (isAuthenticated && !isLoading); + const handled = useRef(false); + useEffect(() => { - const requested = new URLSearchParams(window.location.search).get("whats-new"); - if (requested === null) return; + if (requested === null) { + handled.current = false; + return; + } + if (handled.current || !ready) return; + handled.current = true; const channel = (WHATS_NEW_CHANNELS as readonly string[]).includes(requested) ? (requested as WhatsNewChannel) : undefined; trackEvent("whats_new_opened", { source: "deeplink", channel: channel ?? "default" }); openWhatsNew("deeplink", channel); - }, []); + }, [requested, ready]); - if (!panelOpen) return null; - return ; + return null; }