"use client"; import { useState } from "react"; import { useSession } from "next-auth/react"; import { useTranslations, useLocale } from "next-intl"; import { Card } from "@/components/ui/card"; interface Props { initial: { firstName: string; lastName: string; email: string; /** Current ZITADEL preferredLanguage; "" if never set. */ language: string; }; /** * Personal-account flag. Drives a small hint about how the ZITADEL * name relates (or doesn't) to invoice identity — see the page * server component for the long explanation. */ isPersonal: boolean; /** * For company accounts: the display org name. Shown in a small * read-only "Member of " hint so the user understands which * identity they're editing. Ignored for personals (orgName is an * opaque "personal-XXXX" string in that case). */ orgName: string; } /** * Edits first/last name in ZITADEL via PUT /api/settings/profile. * Email is shown read-only — changing email requires verification * flow that ZITADEL's own self-service UI handles. * * On save, we trigger NextAuth's `update()` from useSession() with * the new display name. That routes through our jwt callback * (trigger='update' branch) which overlays token.name without a * logout/login. After the cookie is updated we trigger a full page * reload — every server-rendered surface (nav-shell, dashboard * welcome, instance cards) re-reads the cookie on the next request * and renders with the new name. router.refresh() alone wasn't * enough: it re-runs only the current route's server components, * leaving outer-tree segments stale until the user navigates. */ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) { const t = useTranslations("settingsProfile"); const locale = useLocale(); const { update } = useSession(); const [form, setForm] = useState({ firstName: initial.firstName, lastName: initial.lastName, // Fall back to the current UI locale when the profile has no stored // preference yet (older accounts), so the selector shows something // sensible rather than blank. language: initial.language || locale, }); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [savedFlash, setSavedFlash] = useState(false); const submit = async () => { setError(null); setSavedFlash(false); if (!form.firstName.trim() || !form.lastName.trim()) { setError(t("missingRequired")); return; } setBusy(true); try { const res = await fetch("/api/settings/profile", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ firstName: form.firstName.trim(), lastName: form.lastName.trim(), language: form.language, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.error ?? `HTTP ${res.status}`); } // Phase 6 fix5: push the new display name into the session // token. The jwt callback handles trigger='update' and overlays // token.name; the next session callback maps token.name back // to session.user.name. No re-login needed. await update({ name: data.displayName }); setSavedFlash(true); // If the language changed, land the user on the new locale (a // full navigation so every server-rendered surface re-renders in // the new language). Otherwise just reload so the new name // propagates. The 800ms delay lets the "Saved" flash show first. const localeChanged = form.language && form.language !== locale; const target = localeChanged ? localePath(form.language) : null; setTimeout(() => { if (target) window.location.assign(target); else window.location.reload(); }, 800); } catch (e: any) { setError(e?.message ?? String(e)); } finally { setBusy(false); } }; return (
setForm((f) => ({ ...f, firstName: e.target.value })) } maxLength={100} className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border focus:border-accent focus:outline-none text-sm" /> setForm((f) => ({ ...f, lastName: e.target.value })) } maxLength={100} className="w-full px-3 py-2 rounded-md bg-surface-2 border border-border focus:border-accent focus:outline-none text-sm" />
{/* Personal vs company hint. Personals get the "this won't change your invoice name" warning since their ZITADEL name and their invoice identity are intentionally decoupled. Company accounts get a benign "member of" context line so they know which org's identity they're editing. */} {isPersonal ? (

{t("personalAccountHint")}

) : (

{t("companyAccountHint", { orgName })}

)} {error &&

{error}

} {savedFlash &&

{t("saved")}

}
); } // Build the as-needed-prefixed path for a target locale from the // current URL (default locale `de` is unprefixed). Client-only — uses // window; called from the save handler. function localePath(lang: string): string { const p = window.location.pathname.replace(/^\/(de|fr|it|en)(?=\/|$)/, "") || "/"; return lang === "de" ? p : `/${lang}${p === "/" ? "" : p}`; } function Field({ label, required, hint, children, }: { label: string; required?: boolean; hint?: string; children: React.ReactNode; }) { return (
{children} {hint &&

{hint}

}
); }