"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { startTransition, 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 { ExternalLink, InfoIcon, XIcon } from "lucide-react"; import { Button } from "./ui/button"; 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 { usePaidStatus } from "@app/hooks/usePaidStatus"; import { build } from "@server/build"; import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; export type AuthPageCustomizationProps = { orgId: string; branding: GetLoginPageBrandingResponse | null; }; const AuthPageFormSchema = z.object({ logoUrl: z.union([ z.string().length(0), 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 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(); toast({ variant: "default", title: t("success"), description: t("authPageBrandingRemoved") }); form.reset(); } } 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" || env.env.flags.useOrgOnlyIdp ? ( <>
{t( "organizationLoginPageTitle" )} {t( "organizationLoginPageDescription" )}
( {t( "brandingOrgTitle" )} )} /> ( {t( "brandingOrgSubtitle" )} )} />
) : null}
{t("resourceLoginPageTitle")} {t("resourceLoginPageDescription")}
( {t("brandingResourceTitle")} )} /> ( {t( "brandingResourceSubtitle" )} )} />
{branding && (
)}
); }