import {useEffect, useState} from 'react'; import {useForm} from 'react-hook-form'; import {zodResolver} from '@hookform/resolvers/zod'; import {DomainSchemas} from '@plunk/shared'; import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, ConfirmDialog, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, } from '@plunk/ui'; import {AnimatePresence, motion} from 'framer-motion'; import {Check, CheckCircle2, ChevronDown, Copy, Loader2, RefreshCw, Trash2, XCircle} from 'lucide-react'; import {useConfig} from '../lib/hooks/useConfig'; import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains'; function AnimatedCopyIcon({isCopied}: {isCopied: boolean}) { return ( {isCopied ? ( ) : ( )} ); } interface DomainsSettingsProps { projectId: string; } export function DomainsSettings({projectId}: DomainsSettingsProps) { const {domains, mutate: mutateDomains, isLoading} = useDomains(projectId); const {addDomain} = useAddDomain(); const {checkVerification} = useCheckDomainVerification(); const {removeDomain} = useRemoveDomain(); const {data: config} = useConfig(); const [successMessage, setSuccessMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [selectedDomain, setSelectedDomain] = useState(null); const [verificationStatus, setVerificationStatus] = useState<{ [key: string]: boolean | string | {tokens: string[] | null; status: string; verified: boolean}; }>({}); const [checkingVerification, setCheckingVerification] = useState(null); const [copiedToken, setCopiedToken] = useState(null); const [lastVerificationCheck, setLastVerificationCheck] = useState<{[key: string]: number}>({}); const [cooldownSeconds, setCooldownSeconds] = useState<{[key: string]: number}>({}); const [showRemoveDialog, setShowRemoveDialog] = useState(false); const [domainToRemove, setDomainToRemove] = useState<{id: string; name: string} | null>(null); const [expandedDomains, setExpandedDomains] = useState<{[key: string]: boolean}>({}); const form = useForm<{domain: string}>({ resolver: zodResolver(DomainSchemas.create.omit({projectId: true})), defaultValues: { domain: '', }, }); // Handle cooldown timer useEffect(() => { const interval = setInterval(() => { const now = Date.now(); const newCooldowns: {[key: string]: number} = {}; let hasActiveCooldowns = false; Object.keys(lastVerificationCheck).forEach(domainId => { const lastCheck = lastVerificationCheck[domainId]; if (lastCheck === undefined) return; const elapsedSeconds = Math.floor((now - lastCheck) / 1000); const remainingSeconds = 10 - elapsedSeconds; if (remainingSeconds > 0) { newCooldowns[domainId] = remainingSeconds; hasActiveCooldowns = true; } }); setCooldownSeconds(newCooldowns); // Clear interval if no active cooldowns if (!hasActiveCooldowns && Object.keys(newCooldowns).length === 0) { clearInterval(interval); } }, 100); // Update every 100ms for smooth countdown return () => clearInterval(interval); }, [lastVerificationCheck]); // Auto-expand unverified domains on initial load useEffect(() => { if (domains && domains.length > 0) { const unverifiedDomains = domains .filter(d => !d.verified) .reduce( (acc, d) => { acc[d.id] = true; return acc; }, {} as {[key: string]: boolean}, ); if (Object.keys(unverifiedDomains).length > 0) { setExpandedDomains(prev => ({ ...prev, ...unverifiedDomains, })); } } }, [domains]); const showMessage = (type: 'success' | 'error', message: string) => { if (type === 'success') { setSuccessMessage(message); setErrorMessage(null); setTimeout(() => setSuccessMessage(null), 5000); } else { setErrorMessage(message); setSuccessMessage(null); } }; const onSubmit = async (values: {domain: string}) => { try { setErrorMessage(null); const newDomain = await addDomain(projectId, values.domain); // Store DKIM tokens for display if (newDomain.dkimTokens) { setVerificationStatus(prev => ({ ...prev, [newDomain.id]: { tokens: newDomain.dkimTokens as string[] | null, status: 'Pending', verified: false, }, })); setSelectedDomain(newDomain.id); // Auto-expand newly added domain setExpandedDomains(prev => ({ ...prev, [newDomain.id]: true, })); } await mutateDomains(); form.reset(); showMessage('success', `Domain ${values.domain} added successfully. Please configure DNS records.`); } catch (error) { showMessage('error', error instanceof Error ? error.message : 'Failed to add domain'); } }; const handleCheckVerification = async (domainId: string) => { // Check if cooldown is active const now = Date.now(); const lastCheck = lastVerificationCheck[domainId]; if (lastCheck) { const elapsedSeconds = Math.floor((now - lastCheck) / 1000); if (elapsedSeconds < 10) { return; // Still in cooldown, do nothing } } try { setCheckingVerification(domainId); setLastVerificationCheck(prev => ({ ...prev, [domainId]: now, })); const status = await checkVerification(domainId); setVerificationStatus(prev => ({ ...prev, [domainId]: status, })); await mutateDomains(); if (status.verified) { showMessage('success', `Domain ${status.domain} is verified!`); } else { showMessage('error', `Domain ${status.domain} is not yet verified. Please check your DNS records.`); } } catch (error) { showMessage('error', error instanceof Error ? error.message : 'Failed to check verification'); } finally { setCheckingVerification(null); } }; const handleRemoveDomain = async () => { if (!domainToRemove) return; try { await removeDomain(domainToRemove.id); await mutateDomains(); if (selectedDomain === domainToRemove.id) { setSelectedDomain(null); } showMessage('success', `Domain ${domainToRemove.name} removed successfully`); } catch (error) { showMessage('error', error instanceof Error ? error.message : 'Failed to remove domain'); } finally { setDomainToRemove(null); } }; const handleCopyToken = async (token: string, index: number) => { await navigator.clipboard.writeText(token); setCopiedToken(`${token}-${index}`); setTimeout(() => setCopiedToken(null), 2000); }; const getDomainStatus = (domain: { id: string; verified: boolean; dkimTokens: unknown; }): {verified: boolean; tokens: unknown; status: string} => { const status = verificationStatus[domain.id]; if (status && typeof status === 'object' && 'verified' in status) { return status; } return {verified: domain.verified, tokens: domain.dkimTokens, status: domain.verified ? 'Success' : 'Pending'}; }; return (
{/* Add Domain Form */} Add Domain Add a custom domain to send emails from
( Domain )} /> {/* Success/Error Messages */} {successMessage && ( {successMessage} )} {errorMessage && ( {errorMessage} )}
{/* Domains List */} Your Domains Manage your verified domains {isLoading ? (
) : !domains || domains.length === 0 ? (

No domains added yet

) : (
{domains.map(domain => { const status = getDomainStatus(domain); return (

{domain.domain}

{status.verified ? ( Verified ) : ( Pending )}
{Array.isArray(status.tokens) && (status.tokens as string[]).length > 0 && (
{expandedDomains[domain.id] && (
{/* Required DKIM Records for Sending */}

Required for Sending

REQUIRED
{!status.verified && (

Add these DKIM records to verify your domain and send emails. DNS changes can take up to 48 hours to propagate.

)}
{/* DKIM Records */} {status.tokens.map((token: string, index: number) => ( ))}
Type Name Value
CNAME
{token}._domainkey.{domain.domain}
{token}.dkim.amazonses.com
{/* Optional: Custom MAIL FROM Domain */} {config?.aws?.sesRegion && (

Custom MAIL FROM Domain

OPTIONAL

Set up a custom MAIL FROM domain (plunk.{domain.domain}) to improve deliverability and handle bounces/complaints.

{/* MX Record (Bounce/Complaint Handling) */} {/* TXT Record (SPF) */}
Type Name Value
MX
plunk.{domain.domain}
10 feedback-smtp.{config.aws.sesRegion}.amazonses.com
TXT
plunk.{domain.domain}
"v=spf1 include:amazonses.com ~all"
)} {/* Optional: Inbound Email */} {config?.aws?.sesRegion && (

Inbound Email

OPTIONAL

Configure this MX record to receive emails at your domain.

{/* Inbound MX Record */}
Type Name Value
MX
{domain.domain}
10 inbound-smtp.{config.aws.sesRegion}.amazonaws.com
)}

Click the copy icon to copy record values. After adding all records to your DNS provider, use the refresh button above to verify your domain.

)}
)}
); })}
)}
); }