'use client' import { useState, useCallback, useEffect } from 'react' import { useTranslations } from 'next-intl' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import { useToast } from '@/components/ui/use-toast' import { AlertTriangle, Check, Copy, Loader2, RefreshCw, Trash2 } from 'lucide-react' import type { CompanyInboundDomain, InboundDomainDnsRecord } from '@/types' import { getErrorMessage as getUserErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' import { useFormat } from '@/lib/hooks/use-format' import { useCompany } from '@/contexts/CompanyContext' import { copyToClipboard } from '@/lib/browser/copy-to-clipboard' import { useBranding } from '@/lib/branding/brand-context' const BASE = '/api/extensions/ext/invoice-inbox/inbox/domain' const STATUS_VARIANT: Record< CompanyInboundDomain['status'], 'secondary' | 'success' | 'destructive' > = { pending: 'secondary', verified: 'success', failed: 'destructive', } interface Props { open: boolean onOpenChange: (open: boolean) => void } // Settings dialog for a company's own inbound domain. Claims the domain via // the extension API, renders the DNS records the user must publish, and // re-checks verification on demand. Everything mail-routing happens // server-side: this surface only manages the claim lifecycle. export default function InboxCustomDomainDialog({ open, onOpenChange }: Props) { const { toast } = useToast() const { appName } = useBranding() const t = useTranslations('inbox_custom_domain') const { locale, formatDateLong } = useFormat() const errorLocale = locale as ErrorLocale const { role } = useCompany() const canManage = role === 'owner' || role === 'admin' const [isLoading, setIsLoading] = useState(true) const [loadFailed, setLoadFailed] = useState(false) const [domain, setDomain] = useState(null) const [domainInput, setDomainInput] = useState('') const [isClaiming, setIsClaiming] = useState(false) const [isChecking, setIsChecking] = useState(false) const [isRemoving, setIsRemoving] = useState(false) const fetchDomain = useCallback(async () => { setIsLoading(true) setLoadFailed(false) try { const res = await fetch(BASE) if (!res.ok) { setLoadFailed(true) return } const json = await res.json() setDomain(json.data ?? null) } catch { setLoadFailed(true) } finally { setIsLoading(false) } }, []) useEffect(() => { if (open) fetchDomain() }, [open, fetchDomain]) const handleClaim = useCallback(async () => { if (!domainInput.trim()) return setIsClaiming(true) try { const res = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain: domainInput }), }) const json = await res.json() if (!res.ok) throw new Error(json.error ?? t('claim_error_title')) setDomain(json.data) setDomainInput('') toast({ title: t('claim_success_title'), description: t('claim_success_description'), }) } catch (err) { toast({ title: t('claim_error_title'), description: err instanceof Error ? getUserErrorMessage(err, { locale: errorLocale }) : t('try_again'), variant: 'destructive', }) } finally { setIsClaiming(false) } }, [domainInput, errorLocale, t, toast]) const handleVerify = useCallback(async () => { setIsChecking(true) try { const res = await fetch(`${BASE}/verify`, { method: 'POST' }) const json = await res.json() if (!res.ok) throw new Error(json.error ?? t('verify_error_title')) setDomain(json.data) toast( json.data.status === 'verified' ? { title: t('verify_success_title'), description: t('verify_success_description') } : { title: t('verify_pending_title'), description: t('verify_pending_description') } ) } catch (err) { toast({ title: t('verify_error_title'), description: err instanceof Error ? getUserErrorMessage(err, { locale: errorLocale }) : t('try_again'), variant: 'destructive', }) } finally { setIsChecking(false) } }, [errorLocale, t, toast]) const handleRemove = useCallback(async () => { if (!domain) return if (!confirm(t('remove_confirm', { domain: domain.domain, appName }))) return setIsRemoving(true) try { const res = await fetch(BASE, { method: 'DELETE' }) const json = await res.json() if (!res.ok) throw new Error(json.error ?? t('remove_error_title')) setDomain(null) toast({ title: t('remove_success_title') }) } catch (err) { toast({ title: t('remove_error_title'), description: err instanceof Error ? getUserErrorMessage(err, { locale: errorLocale }) : t('try_again'), variant: 'destructive', }) } finally { setIsRemoving(false) } }, [domain, errorLocale, t, toast, appName]) const handleCopy = useCallback( async (value: string) => { const result = await copyToClipboard(value) toast( result === 'copied' ? { title: t('copied') } : { title: t('copy_failed_title'), description: t('copy_failed_description'), variant: 'destructive', } ) }, [t, toast] ) const records: InboundDomainDnsRecord[] = domain?.dns_records ?? [] const statusLabels: Record = { pending: t('status_pending'), verified: t('status_verified'), failed: t('status_failed'), } return ( {t('title')} {t('description')} {isLoading ? (
) : loadFailed ? (

{t('load_error')}

) : !domain ? (

{t('warning_title')}

{t('warning_before_subdomain', { appName })}{' '} faktura.dittbolag.se{' '} {t('warning_after_subdomain')}

{canManage ? (
setDomainInput(e.target.value)} placeholder="faktura.dittbolag.se" aria-label={t('domain_input_aria')} onKeyDown={(e) => { if (e.key === 'Enter') handleClaim() }} />
) : (

{t('manage_permission')}

)}
) : (
{domain.domain} {statusLabels[domain.status]}
{canManage ? (
) : null}
{domain.status === 'verified' ? (

{t('verified_title')}{' '} faktura@{domain.domain}

{domain.verified_at ? t('verified_description_with_date', { date: formatDateLong(domain.verified_at), }) : t('verified_description')}

) : (

{t('dns_instructions')}

{records.length > 0 ? (
{records.map((r, i) => ( ))}
{t('dns_type')} {t('dns_name')} {t('dns_value')} {t('dns_priority')}
{r.type} {r.name} {r.value} {r.priority ?? '-'}
) : (

{t('dns_empty')}

)}
)}
)}
) }