import { useCallback, useEffect, useState } from "react" import { useArcadiaClient } from "@crema/arcadia-core-client" import { useToast } from "@crema/notification-ui" import { TenantSection, Field } from "~/components/tenant-detail/section" import { Button } from "~/components/ui/button" import { Input } from "~/components/ui/input" import { Switch } from "~/components/ui/switch" import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select" import { EMAIL_PROVIDERS, SMS_PROVIDERS, deleteEmailConfig, deleteSmsConfig, getEmailConfig, getSmsConfig, testEmailConfig, testSmsConfig, upsertEmailConfig, upsertSmsConfig, type EmailConfigInput, type SmsConfigInput, } from "~/lib/arcadia/tenants" import { errorMessage } from "~/lib/errors" import type { TenantTabProps } from "~/routes/tenants.$id" /** * Email & SMS delivery. These are two independent configs — each loads, saves, * tests, and deletes on its own — so the tab renders the same * frame twice, parameterised by `kind`. */ export function DeliveryTab({ tenant, reload }: TenantTabProps) { return (
) } type Kind = "email" | "sms" /** A credential input the operator can fill. Values are collected into the * write-only `credentials` object and only sent when actually typed. */ type CredField = { key: string; label: string; type?: string } function credentialFields(kind: Kind, provider: string): CredField[] { if (kind === "email") { if (provider === "smtp") { return [ { key: "host", label: "SMTP host" }, { key: "port", label: "Port" }, { key: "username", label: "Username" }, { key: "password", label: "Password", type: "password" }, ] } return [{ key: "api_key", label: "API key", type: "password" }] } // sms if (provider === "twilio") { return [ { key: "account_sid", label: "Account SID" }, { key: "auth_token", label: "Auth token", type: "password" }, ] } if (provider === "vonage") { return [ { key: "api_key", label: "API key" }, { key: "api_secret", label: "API secret", type: "password" }, ] } return [{ key: "api_key", label: "API key", type: "password" }] } function DeliveryConfig({ tenant, kind }: TenantTabProps & { kind: Kind }) { const arcadia = useArcadiaClient() const toast = useToast() const providers = kind === "email" ? EMAIL_PROVIDERS : SMS_PROVIDERS const prefix = `tenant-detail-${kind}` const label = kind === "email" ? "Email" : "SMS" const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) const [removing, setRemoving] = useState(false) // Whether a config exists server-side (loaded non-null, or just saved). Gates // "Send test" and "Remove", which are meaningless with nothing configured. const [exists, setExists] = useState(false) const [provider, setProvider] = useState(providers[0]) const [enabled, setEnabled] = useState(false) // email-only const [fromEmail, setFromEmail] = useState("") const [fromName, setFromName] = useState("") const [replyTo, setReplyTo] = useState("") // sms-only const [fromNumber, setFromNumber] = useState("") // write-only credentials, keyed by field const [creds, setCreds] = useState>({}) const load = useCallback(async () => { setError(null) setLoading(true) try { if (kind === "email") { const cfg = await getEmailConfig(arcadia, tenant.id) if (cfg) { setExists(true) setProvider(cfg.provider) setEnabled(cfg.enabled) setFromEmail(cfg.from_email ?? "") setFromName(cfg.from_name ?? "") setReplyTo(cfg.reply_to ?? "") } else { setExists(false) } } else { const cfg = await getSmsConfig(arcadia, tenant.id) if (cfg) { setExists(true) setProvider(cfg.provider) setEnabled(cfg.enabled) setFromNumber(cfg.from_number ?? "") } else { setExists(false) } } // Credentials are never returned — always start the write-only fields empty. setCreds({}) } catch (err) { // getEmailConfig/getSmsConfig already map 404 → null, so anything thrown // here is a genuine load failure. setError(err) } finally { setLoading(false) } }, [arcadia, tenant.id, kind]) useEffect(() => { load() }, [load]) const save = async () => { setSaving(true) setError(null) try { // Only send credentials the operator actually typed, scoped to the // current provider — otherwise we'd wipe stored creds with blanks, or // leak a previous provider's fields. const typedCreds: Record = {} for (const f of credentialFields(kind, provider)) { const v = (creds[f.key] ?? "").trim() if (v) typedCreds[f.key] = v } const hasCreds = Object.keys(typedCreds).length > 0 if (kind === "email") { const input: EmailConfigInput = { provider, from_email: fromEmail, from_name: fromName, reply_to: replyTo, enabled, ...(hasCreds ? { credentials: typedCreds } : {}), } await upsertEmailConfig(arcadia, tenant.id, input) toast.success("Email settings saved") } else { const input: SmsConfigInput = { provider, from_number: fromNumber, enabled, ...(hasCreds ? { credentials: typedCreds } : {}), } await upsertSmsConfig(arcadia, tenant.id, input) toast.success("SMS settings saved") } await load() } catch (err) { setError(err) } finally { setSaving(false) } } const runTest = async () => { setTesting(true) try { const res = kind === "email" ? await testEmailConfig(arcadia, tenant.id) : await testSmsConfig(arcadia, tenant.id) if (res.ok) toast.success(res.message || `${label} test succeeded`) else toast.error(res.message || `${label} test failed`) } catch (err) { toast.error(errorMessage(err, `test ${label.toLowerCase()} delivery`)) } finally { setTesting(false) } } const remove = async () => { setRemoving(true) try { if (kind === "email") await deleteEmailConfig(arcadia, tenant.id) else await deleteSmsConfig(arcadia, tenant.id) toast.success(`${label} configuration removed`) await load() } catch (err) { toast.error(errorMessage(err, `remove ${label.toLowerCase()} configuration`)) } finally { setRemoving(false) } } const fields = credentialFields(kind, provider) return ( ) : null } > setProvider(e.target.value)} data-action={`${prefix}-provider`} > {providers.map((p) => ( {p} ))} {kind === "email" ? ( <> setFromEmail(e.target.value)} placeholder="no-reply@example.com" data-action={`${prefix}-from-email`} /> setFromName(e.target.value)} placeholder="Example App" data-action={`${prefix}-from-name`} /> setReplyTo(e.target.value)} placeholder="support@example.com" data-action={`${prefix}-reply-to`} /> ) : ( setFromNumber(e.target.value)} placeholder="+15551234567" data-action={`${prefix}-from-number`} /> )}
Enabled

When off, {label.toLowerCase()} is configured but not sent.

setEnabled(v)} data-action={`${prefix}-enabled`} />
Credentials

{exists ? "Write-only and never shown again. Leave blank to keep the stored credentials; fill in to replace them." : "Write-only and never shown again."}

{fields.map((f) => ( setCreds((c) => ({ ...c, [f.key]: e.target.value }))} placeholder={exists ? "•••••• (unchanged)" : ""} data-action={`${prefix}-cred-${f.key}`} /> ))}
) }