feat(dashboard): add one-time Stack Auth → Hexclave rebrand modal

Announces the rebrand to authenticated dashboard users whose
signedUpAt predates 2026-05-27 UTC (the rebrand cutoff). Brand-new
users signing up after the cutoff already land on Hexclave-branded
surfaces and don't see the modal.

- Modal lives in the protected dashboard layout via
  apps/dashboard/src/app/(main)/(protected)/layout-client.tsx so it
  surfaces on any logged-in route once per browser.
- Gated on useUser({ or: "return-null" }) + signedUpAt < cutoff, so
  it never triggers a sign-in redirect for guests and never shows for
  post-rebrand signups.
- Dismissal (button, X, overlay, Escape) routes through onOpenChange
  and writes "hexclave-rebrand-modal-dismissed" to localStorage so it
  never re-appears for that browser.
- Illustration uses the existing Stack Auth logo (logo.svg /
  logo-bright.svg for dark mode) and the Hexclave benzene mark saved
  to public/hexclave-icon.svg.
- Copy links app.hexclave.com and docs.hexclave.com/migration.

Screenshot: .github/assets/hexclave-rebrand-modal.png
This commit is contained in:
Bilal Godil 2026-05-26 15:37:45 -07:00
parent 663cb5534c
commit b90998c8da
4 changed files with 204 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1,26 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="neon-grad" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00FFFF" />
<stop offset="50%" stop-color="#3B82F6" />
<stop offset="100%" stop-color="#8B5CF6" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.5" result="blur1" />
<feGaussianBlur stdDeviation="3.5" result="blur2" />
<feMerge>
<feMergeNode in="blur2" />
<feMergeNode in="blur1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<g id="benzene-mark">
<path d="M 24 4 L 41.32 14 L 41.32 34 L 24 44 L 6.68 34 L 6.68 14 Z" fill="none" stroke="url(#neon-grad)" stroke-width="3" stroke-linejoin="miter" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(120 24 24)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(240 24 24)" />
</g>
</defs>
<use href="#benzene-mark" filter="url(#glow)" opacity="0.75" />
<use href="#benzene-mark" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -3,6 +3,7 @@
import Loading from "@/app/loading";
import { CursorBlastEffect } from "@stackframe/dashboard-ui-components";
import { ConfigUpdateDialogProvider } from "@/lib/config-update";
import { HexclaveRebrandModal } from "@/components/hexclave-rebrand-modal";
import { getPublicEnvVar } from '@/lib/env';
import { useStackApp, useUser } from "@stackframe/stack";
import { LOCAL_EMULATOR_ADMIN_EMAIL, LOCAL_EMULATOR_ADMIN_PASSWORD } from "@stackframe/stack-shared/dist/local-emulator";
@ -60,6 +61,7 @@ export default function LayoutClient({ children }: { children: React.ReactNode }
return (
<ConfigUpdateDialogProvider>
<CursorBlastEffect />
<HexclaveRebrandModal />
{children}
</ConfigUpdateDialogProvider>
);

View File

@ -0,0 +1,176 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useUser } from "@stackframe/stack";
import Image from "next/image";
import { useEffect, useState } from "react";
const STORAGE_KEY = "hexclave-rebrand-modal-dismissed";
const MIGRATION_DOCS_URL = "https://docs.hexclave.com/migration";
// Users who signed up before this instant predate the Stack Auth → Hexclave
// rebrand and are the only ones who benefit from the announcement. Anyone
// signing up after this already lands on a Hexclave-branded experience and
// has no "Stack Auth" mental model to update — no point telling them.
const REBRAND_CUTOFF = new Date("2026-05-27T00:00:00.000Z");
/**
* One-time informational modal announcing the Stack Auth Hexclave rebrand.
*
* Only renders for a logged-in user who signed up before {@link REBRAND_CUTOFF}.
* On any dismissal (confirm button, close button, overlay click, or Escape)
* writes `STORAGE_KEY` to localStorage so the modal never re-appears for that
* browser.
*/
export function HexclaveRebrandModal() {
// `or: "return-null"` keeps this from triggering the sign-in redirect when
// it's rendered above the auth boundary — we simply opt out for guests.
const user = useUser({ or: "return-null" });
const isPreRebrandUser = user != null && user.signedUpAt < REBRAND_CUTOFF;
const [open, setOpen] = useState(false);
// Read localStorage after hydration to avoid SSR mismatch — render closed
// on the server and only open if we know the user hasn't dismissed it.
useEffect(() => {
if (!isPreRebrandUser) return;
try {
const dismissed = localStorage.getItem(STORAGE_KEY);
if (dismissed !== "true") {
setOpen(true);
}
} catch {
// localStorage can throw in private-mode / sandboxed iframes; treat
// unavailable storage as "already dismissed" so we don't spam users
// who can't persist the dismissal anyway.
}
}, [isPreRebrandUser]);
const dismiss = () => {
try {
localStorage.setItem(STORAGE_KEY, "true");
} catch {
// see above — best-effort write
}
setOpen(false);
};
if (!isPreRebrandUser) return null;
return (
<Dialog
open={open}
onOpenChange={(next) => {
if (!next) dismiss();
}}
>
<DialogContent className="max-w-md">
<DialogHeader>
<RebrandIllustration />
<DialogTitle className="text-center text-xl pt-2">
Stack Auth is now Hexclave
</DialogTitle>
<DialogDescription className="text-center">
We&apos;re rebranding! Same product, same team, new home at{" "}
<a
href="https://app.hexclave.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-2 hover:text-foreground"
>
app.hexclave.com
</a>
. For more info on how to update your project, read our{" "}
<a
href={MIGRATION_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-2 hover:text-foreground"
>
migration guide
</a>
.
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-center pt-6">
<Button onClick={dismiss} className="min-w-32">
Got it
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/**
* Stack Auth mark (faded) arrow Hexclave benzene mark. Both logos are
* served from `/public` so they match the canonical brand assets.
*/
function RebrandIllustration() {
return (
<div
className="flex justify-center items-center gap-4 pb-2"
aria-hidden="true"
>
{/* Stack Auth: served light & dark variants depending on theme */}
<Image
src="/logo.svg"
alt=""
width={48}
height={48}
aria-hidden
className="h-12 w-auto opacity-50 block dark:hidden"
/>
<Image
src="/logo-bright.svg"
alt=""
width={48}
height={48}
aria-hidden
className="h-12 w-auto opacity-60 hidden dark:block"
/>
{/* Arrow — bridge between the two marks */}
<svg
width="40"
height="14"
viewBox="0 0 40 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-muted-foreground"
>
<path
d="M2 7 L34 7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M28 1 L34 7 L28 13"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
{/* Hexclave benzene mark — gradient + glow filter, theme-agnostic */}
<Image
src="/hexclave-icon.svg"
alt=""
width={56}
height={56}
aria-hidden
className="h-14 w-14"
/>
</div>
);
}