"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useActionState, useState } from "react"; import { useForm } from "react-hook-form"; import z from "zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "./Settings"; import { useTranslations } from "next-intl"; import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; import { Input } from "./ui/input"; import { Trash2, XIcon } from "lucide-react"; import { Button } from "./ui/button"; import { Separator } from "./ui/separator"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useRouter } from "next/navigation"; import { toast } from "@app/hooks/useToast"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "./Credenza"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { build } from "@server/build"; import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; export type AuthPageCustomizationProps = { orgId: string; branding: GetLoginPageBrandingResponse | null; }; const AuthPageFormSchema = z.object({ logoUrl: z.url().refine( async (url) => { try { const response = await fetch(url); return ( response.status === 200 && (response.headers.get("content-type") ?? "").startsWith( "image/" ) ); } catch (error) { return false; } }, { error: "Invalid logo URL, must be a valid image URL" } ), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), orgTitle: z.string().optional(), orgSubtitle: z.string().optional(), resourceTitle: z.string(), resourceSubtitle: z.string().optional(), primaryColor: z .string() .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) .optional() }); export default function AuthPageBrandingForm({ orgId, branding }: AuthPageCustomizationProps) { const env = useEnvContext(); const api = createApiClient(env); const { isPaidUser } = usePaidStatus(); const router = useRouter(); const [, updateFormAction, isUpdatingBranding] = useActionState( updateBranding, null ); const [, deleteFormAction, isDeletingBranding] = useActionState( deleteBranding, null ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const t = useTranslations(); const form = useForm({ resolver: zodResolver(AuthPageFormSchema), defaultValues: { logoUrl: branding?.logoUrl ?? "", logoWidth: branding?.logoWidth ?? 100, logoHeight: branding?.logoHeight ?? 100, orgTitle: branding?.orgTitle ?? `Log in to {{orgName}}`, orgSubtitle: branding?.orgSubtitle ?? `Log in to {{orgName}}`, resourceTitle: branding?.resourceTitle ?? `Authenticate to access {{resourceName}}`, resourceSubtitle: branding?.resourceSubtitle ?? `Choose your preferred authentication method for {{resourceName}}`, primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color }, disabled: !isPaidUser }); async function updateBranding() { const isValid = await form.trigger(); const brandingData = form.getValues(); if (!isValid || !isPaidUser) return; try { const updateRes = await api.put( `/org/${orgId}/login-page-branding`, { ...brandingData } ); if (updateRes.status === 200 || updateRes.status === 201) { router.refresh(); toast({ variant: "default", title: t("success"), description: t("authPageBrandingUpdated") }); } } catch (error) { toast({ variant: "destructive", title: t("authPageErrorUpdate"), description: formatAxiosError( error, t("authPageErrorUpdateMessage") ) }); } } async function deleteBranding() { if (!isPaidUser) return; try { const updateRes = await api.delete( `/org/${orgId}/login-page-branding` ); if (updateRes.status === 200) { router.refresh(); form.reset(); setIsDeleteModalOpen(false); toast({ variant: "default", title: t("success"), description: t("authPageBrandingRemoved") }); } } catch (error) { toast({ variant: "destructive", title: t("authPageErrorUpdate"), description: formatAxiosError( error, t("authPageErrorUpdateMessage") ) }); } } return ( <> {t("authPageBranding")} {t("authPageBrandingDescription")}
( {t("brandingPrimaryColor")}
)} />
( {t("brandingLogoURL")} )} />
( {t("brandingLogoWidth")} )} /> ( {t( "brandingLogoHeight" )} )} />
{build === "saas" && ( <>
( {t( "brandingOrgTitle" )} {t( "brandingOrgDescription", { orgName: "{{orgName}}" } )} )} /> ( {t( "brandingOrgSubtitle" )} {t( "brandingOrgDescription", { orgName: "{{orgName}}" } )} )} />
)}
( {t("brandingResourceTitle")} {t( "brandingResourceDescription", { resourceName: "{{resourceName}}" } )} )} /> ( {t( "brandingResourceSubtitle" )} {t( "brandingResourceDescription", { resourceName: "{{resourceName}}" } )} )} />
{t("authPageBrandingRemoveTitle")}

{t("authPageBrandingQuestionRemove")}

{t("cannotbeUndone")}
{branding && ( )}
); }