diff --git a/.github/assets/hexclave-rebrand-modal.png b/.github/assets/hexclave-rebrand-modal.png new file mode 100644 index 000000000..407961568 Binary files /dev/null and b/.github/assets/hexclave-rebrand-modal.png differ diff --git a/apps/dashboard/public/hexclave-icon.svg b/apps/dashboard/public/hexclave-icon.svg new file mode 100644 index 000000000..6954a8f3f --- /dev/null +++ b/apps/dashboard/public/hexclave-icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx b/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx index de0da7ce8..19c1b51f0 100644 --- a/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx @@ -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 ( + {children} ); diff --git a/apps/dashboard/src/components/hexclave-rebrand-modal.tsx b/apps/dashboard/src/components/hexclave-rebrand-modal.tsx new file mode 100644 index 000000000..c57000228 --- /dev/null +++ b/apps/dashboard/src/components/hexclave-rebrand-modal.tsx @@ -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 ( + { + if (!next) dismiss(); + }} + > + + + + + Stack Auth is now Hexclave + + + We're rebranding! Same product, same team, new home at{" "} + + app.hexclave.com + + . For more info on how to update your project, read our{" "} + + migration guide + + . + + + + + + + + ); +} + +/** + * Stack Auth mark (faded) → arrow → Hexclave benzene mark. Both logos are + * served from `/public` so they match the canonical brand assets. + */ +function RebrandIllustration() { + return ( + + ); +}