"use client"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { authFetch, clearToken, getApiBase, getToken } from "../../lib/auth"; import { useEffectiveRole } from "../../lib/viewMode"; import InviteDeliveryChoice from "../../ui/InviteDeliveryChoice"; import PageHeading from "../../ui/PageHeading"; type ProfileInfo = { username: string; role: string; invite_management_enabled?: boolean }; type OwnedInvite = { id: number; code: string; label?: string | null; description?: string | null; code_available?: boolean; recipient_email?: string | null; max_uses?: number | null; use_count: number; remaining_uses?: number | null; enabled: boolean; expires_at?: string | null; is_usable?: boolean; created_at?: string | null; }; type OwnedInvitesResponse = { invites?: OwnedInvite[]; invite_access?: { enabled?: boolean; managed_by_master?: boolean }; master_invite?: { id: number; code: string; label?: string | null; max_uses?: number | null; expires_at?: string | null; } | null; }; type InviteForm = { code: string; label: string; description: string; recipient_email: string; enabled: boolean; message: string; }; type DeliveryMethod = "" | "manual" | "email"; const defaultInviteForm = (): InviteForm => ({ code: "", label: "", description: "", recipient_email: "", enabled: true, message: "", }); const formatDate = (value?: string | null) => { if (!value) return "Never"; const date = new Date(value); return Number.isNaN(date.valueOf()) ? value : date.toLocaleString(); }; const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()); export default function ProfileInvitesPage() { const router = useRouter(); const [profile, setProfile] = useState(null); const [invites, setInvites] = useState([]); const [inviteAccessEnabled, setInviteAccessEnabled] = useState(false); const [inviteManagedByMaster, setInviteManagedByMaster] = useState(false); const [masterInvite, setMasterInvite] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [status, setStatus] = useState(null); const [editingId, setEditingId] = useState(null); const [flowStep, setFlowStep] = useState(1); const [useCustomCode, setUseCustomCode] = useState(false); const [deliveryMethod, setDeliveryMethod] = useState(""); const [inviteForm, setInviteForm] = useState(defaultInviteForm()); const [createdInvite, setCreatedInvite] = useState(null); const effectiveRole = useEffectiveRole(profile?.role); const canManageInvites = effectiveRole === "admin" || (profile?.role === "admin" ? Boolean(profile.invite_management_enabled) : inviteAccessEnabled); const signupBaseUrl = useMemo(() => { if (typeof window === "undefined") return "/signup"; return `${window.location.origin}/signup`; }, []); const loadInvites = useCallback(async () => { const response = await authFetch(`${getApiBase()}/auth/profile/invites`); if (!response.ok) { if (response.status === 401) { clearToken(); router.push("/login"); return; } throw new Error("Could not load your invite workspace."); } const data = (await response.json()) as OwnedInvitesResponse; setInvites(Array.isArray(data.invites) ? data.invites : []); setInviteAccessEnabled(Boolean(data.invite_access?.enabled)); setInviteManagedByMaster(Boolean(data.invite_access?.managed_by_master)); setMasterInvite(data.master_invite ?? null); }, [router]); useEffect(() => { if (!getToken()) { router.push("/login"); return; } const load = async () => { try { const profileResponse = await authFetch(`${getApiBase()}/auth/profile`); if (!profileResponse.ok) { if (profileResponse.status === 401) { clearToken(); router.push("/login"); return; } throw new Error("Could not load your profile."); } const profileData = await profileResponse.json(); setProfile(profileData?.user ?? null); await loadInvites(); } catch (err) { console.error(err); setError(err instanceof Error ? err.message : "Could not load your invite workspace."); } finally { setLoading(false); } }; void load(); }, [loadInvites, router]); const resetFlow = () => { setEditingId(null); setFlowStep(1); setUseCustomCode(false); setDeliveryMethod(""); setInviteForm(defaultInviteForm()); }; const editInvite = (invite: OwnedInvite) => { setEditingId(invite.id); setCreatedInvite(null); setFlowStep(4); setUseCustomCode(true); setDeliveryMethod(invite.recipient_email ? "email" : "manual"); setInviteForm({ code: invite.code, label: invite.label ?? "", description: invite.description ?? "", recipient_email: invite.recipient_email ?? "", enabled: invite.enabled !== false, message: "", }); setError(null); setStatus(null); window.scrollTo({ top: 0, behavior: "smooth" }); }; const saveInvite = async (event: React.FormEvent) => { event.preventDefault(); if (!canManageInvites) return; const inviteName = inviteForm.label.trim(); const recipientEmail = inviteForm.recipient_email.trim(); if (!inviteName) { setError("Give this invite a name so you can recognise it later."); return; } if (!deliveryMethod) { setError("Choose how you want to deliver the invite."); return; } if (deliveryMethod === "email" && !isValidEmail(recipientEmail)) { setError("Enter a valid recipient email address."); return; } setSaving(true); setError(null); setStatus(null); try { const response = await authFetch( editingId == null ? `${getApiBase()}/auth/profile/invites` : `${getApiBase()}/auth/profile/invites/${editingId}`, { method: editingId == null ? "POST" : "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: useCustomCode ? inviteForm.code || null : null, label: inviteName, description: inviteForm.description || null, recipient_email: deliveryMethod === "email" ? recipientEmail : null, enabled: inviteForm.enabled, send_email: editingId == null && deliveryMethod === "email", message: inviteForm.message || null, }), }, ); if (!response.ok) { if (response.status === 401) { clearToken(); router.push("/login"); return; } throw new Error((await response.text()) || "Could not save the invite."); } const data = await response.json(); const savedInvite = data?.invite as OwnedInvite | undefined; setStatus( data?.email?.status === "ok" ? `Invite created and emailed to ${data.email.recipient_email}.` : data?.email?.status === "error" ? `Invite created, but the email could not be sent: ${data.email.detail}` : editingId == null ? "Invite link created and ready to share." : "Invite updated.", ); resetFlow(); if (editingId == null && savedInvite) setCreatedInvite(savedInvite); await loadInvites(); } catch (err) { console.error(err); setError(err instanceof Error ? err.message : "Could not save the invite."); } finally { setSaving(false); } }; const deleteInvite = async (invite: OwnedInvite) => { if (!canManageInvites) return; if (!window.confirm(`Delete invite “${invite.label || invite.code}”?`)) return; setError(null); try { const response = await authFetch(`${getApiBase()}/auth/profile/invites/${invite.id}`, { method: "DELETE" }); if (!response.ok) throw new Error((await response.text()) || "Could not delete the invite."); if (editingId === invite.id) resetFlow(); setStatus(`Deleted ${invite.label || invite.code}.`); await loadInvites(); } catch (err) { console.error(err); setError(err instanceof Error ? err.message : "Could not delete the invite."); } }; const copyInviteLink = async (invite: OwnedInvite) => { if (!canManageInvites) return; try { let usableInvite = invite; if (!invite.code_available) { const response = await authFetch(`${getApiBase()}/auth/profile/invites/${invite.id}/rotate`, { method: "POST", }); if (!response.ok) throw new Error((await response.text()) || "Could not generate a replacement link."); const data = await response.json(); usableInvite = data.invite as OwnedInvite; setInvites((current) => current.map((item) => (item.id === invite.id ? usableInvite : item))); } const url = `${signupBaseUrl}?code=${encodeURIComponent(usableInvite.code)}`; await navigator.clipboard.writeText(url); setStatus( `Copied the link for ${invite.label || usableInvite.code}. Keep it safe; Magent will not display it again after this page reloads.`, ); } catch { setError("Could not generate or copy the invite link."); } }; const codeCharacters = inviteForm.code.replace(/[^a-z0-9]/gi, ""); const identityReady = Boolean(inviteForm.label.trim() && (!useCustomCode || codeCharacters.length >= 6)); const createdInviteUrl = createdInvite ? `${signupBaseUrl}?code=${encodeURIComponent(createdInvite.code)}` : ""; if (loading) return
Loading invite workspace…
; return (
{error &&
{error}
} {status &&
{status}
} {!canManageInvites ? (

Invites are not enabled for your account

Ask an administrator if you need permission to invite someone.

) : (
Invite flow

{editingId == null ? "Create an invite" : `Edit ${inviteForm.label || "invite"}`}

Set up the invite one decision at a time.

{editingId != null && ( )}
{createdInvite && editingId == null ? (
Invite ready

{createdInvite.label || "Your invite"}

{createdInvite.recipient_email ? `The invite was emailed to ${createdInvite.recipient_email}.` : "Copy this link and send it to the person you are inviting."}

) : (
    {["Identity", "Description", "Access", "Delivery"].map((label, index) => { const step = index + 1; return (
  1. {String(step).padStart(2, "0")} {label}
  2. ); })}
1 ? "is-complete" : "is-active"}`}>
01
Identity

Who is this invite for?

Give it a name that will make sense when you return later.

{useCustomCode && ( )} {flowStep === 1 && (
)}
{flowStep >= 2 && (
2 ? "is-complete" : "is-active"}`}>
02
Description

Add a welcome note

This optional message is shown on the sign-up page.