mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
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:
parent
663cb5534c
commit
b90998c8da
BIN
.github/assets/hexclave-rebrand-modal.png
vendored
Normal file
BIN
.github/assets/hexclave-rebrand-modal.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
26
apps/dashboard/public/hexclave-icon.svg
Normal file
26
apps/dashboard/public/hexclave-icon.svg
Normal 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 |
@ -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>
|
||||
);
|
||||
|
||||
176
apps/dashboard/src/components/hexclave-rebrand-modal.tsx
Normal file
176
apps/dashboard/src/components/hexclave-rebrand-modal.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user