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 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-10-14 20:01:10 +02:00
parent 8e000f1849
commit 58e9f19f82
@@ -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. */}
<Suspense fallback={null}>
<WhatsNewDeepLink onPublicPage={onPublicPage} />
</Suspense>
{panelOpen && <WhatsNewPanel rootClassName={onPublicPage ? "landing-v2" : undefined} />}
</>
);
}
/**
* 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 <WhatsNewPanel rootClassName={onPublicPage ? "landing-v2" : undefined} />;
return null;
}