"use client"; // ============================================================ // Org Settings → Integrations. Today: SMS (Twilio) — a tenant // brings its OWN Twilio credentials, which be-crm seals in IIOS // (per-scope) and resolves at send time. The auth token is // write-only: sealed in IIOS, never read back, so status shows // only masked hints (from-number + SID last-4). Email (SMTP) is // the next provider on the same generic credential registry. // ============================================================ import { useState } from "react"; import { Btn, Field, Icon, PageHead, Pill, useToast } from "./ui"; import { useSmsSettings } from "@/lib/sms-settings-api"; import { useSmtpSettings } from "@/lib/smtp-settings-api"; const SID_RE = /^AC[0-9a-fA-F]{32}$/; const E164_RE = /^\+[1-9]\d{6,14}$/; const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; export function Settings() { return (

Integrations

); } function TwilioCard() { const toast = useToast(); const { status, loading, live, configure } = useSmsSettings(); const [editing, setEditing] = useState(false); const [accountSid, setAccountSid] = useState(""); const [authToken, setAuthToken] = useState(""); const [fromNumber, setFromNumber] = useState(""); const [errors, setErrors] = useState<{ accountSid?: string; authToken?: string; fromNumber?: string }>({}); const [saving, setSaving] = useState(false); const showForm = editing || (!loading && !status.configured); function validate(): boolean { const e: typeof errors = {}; if (!SID_RE.test(accountSid.trim())) e.accountSid = "Must be a Twilio Account SID (AC + 32 hex chars)."; if (!authToken.trim()) e.authToken = "Auth token is required."; if (!E164_RE.test(fromNumber.trim())) e.fromNumber = "Must be E.164, e.g. +15551234567."; setErrors(e); return Object.keys(e).length === 0; } async function save() { if (!validate()) return; setSaving(true); try { await configure({ accountSid: accountSid.trim(), authToken: authToken.trim(), fromNumber: fromNumber.trim() }); toast.push({ tone: "success", title: "Twilio connected", desc: "Your SMS credentials are saved and encrypted." }); setAccountSid(""); setAuthToken(""); setFromNumber(""); setErrors({}); setEditing(false); } catch (err) { toast.push({ tone: "error", title: "Couldn't save credentials", desc: (err as Error).message }); } finally { setSaving(false); } } return (
SMS · Twilio
Send texts from your own Twilio number.
{status.configured ? Connected : Not connected}
{status.configured && !editing ? (
From number
{status.fromNumber ?? "—"}
Account SID
{status.sidLast4 ? `AC ···· ${status.sidLast4}` : "—"}
Status
{status.enabled ? "Active" : "Disabled"}
setEditing(true)}>Update credentials
) : null} {showForm ? (
setAccountSid(e.target.value)} placeholder="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" autoComplete="off" /> setAuthToken(e.target.value)} placeholder="••••••••••••••••••••••••••••••••" autoComplete="off" /> setFromNumber(e.target.value)} placeholder="+15551234567" autoComplete="off" />
{saving ? "Saving…" : status.configured ? "Update" : "Connect Twilio"} {status.configured ? { setEditing(false); setErrors({}); }}>Cancel : null}
) : null} {!live ?
Demo mode — credentials are stored locally and not sent to Twilio.
: null}
); } function SmtpCard() { const toast = useToast(); const { status, loading, live, configure } = useSmtpSettings(); const [editing, setEditing] = useState(false); const [host, setHost] = useState(""); const [port, setPort] = useState("587"); const [secure, setSecure] = useState(false); const [user, setUser] = useState(""); const [pass, setPass] = useState(""); const [fromEmail, setFromEmail] = useState(""); const [fromName, setFromName] = useState(""); const [errors, setErrors] = useState>({}); const [saving, setSaving] = useState(false); const showForm = editing || (!loading && !status.configured); function validate(): boolean { const e: Record = {}; if (!host.trim()) e.host = "SMTP host is required."; const p = Number(port); if (!Number.isInteger(p) || p < 1 || p > 65535) e.port = "Port must be 1–65535."; if (!user.trim()) e.user = "Username is required."; if (!pass.trim()) e.pass = "Password is required."; if (!EMAIL_RE.test(fromEmail.trim())) e.fromEmail = "A valid from-address is required."; setErrors(e); return Object.keys(e).length === 0; } async function save() { if (!validate()) return; setSaving(true); try { await configure({ host: host.trim(), port: Number(port), secure, user: user.trim(), pass: pass.trim(), fromEmail: fromEmail.trim(), ...(fromName.trim() ? { fromName: fromName.trim() } : {}) }); toast.push({ tone: "success", title: "SMTP connected", desc: "Outbound email now sends from your server." }); setHost(""); setPort("587"); setSecure(false); setUser(""); setPass(""); setFromEmail(""); setFromName(""); setErrors({}); setEditing(false); } catch (err) { toast.push({ tone: "error", title: "Couldn't save SMTP settings", desc: (err as Error).message }); } finally { setSaving(false); } } return (
Email · SMTP
Send external email from your own mail server.
{status.configured ? Connected : Not connected}
{status.configured && !editing ? (
From
{status.fromName ? `${status.fromName} · ` : ""}{status.fromEmail ?? "—"}
Server
{status.host ?? "—"}{status.port ? `:${status.port}` : ""}
Status
{status.enabled ? "Active" : "Disabled"}
setEditing(true)}>Update credentials
) : null} {showForm ? (
setHost(e.target.value)} placeholder="smtp.example.com" autoComplete="off" /> setPort(e.target.value)} placeholder="587" autoComplete="off" /> setUser(e.target.value)} placeholder="apikey / user@example.com" autoComplete="off" /> setPass(e.target.value)} placeholder="••••••••••••" autoComplete="off" /> setFromEmail(e.target.value)} placeholder="no-reply@example.com" autoComplete="off" /> setFromName(e.target.value)} placeholder="Acme Roofing" autoComplete="off" />
{saving ? "Saving…" : status.configured ? "Update" : "Connect SMTP"} {status.configured ? { setEditing(false); setErrors({}); }}>Cancel : null}
) : null} {!live ?
Demo mode — credentials are stored locally and no email is sent.
: null}
); }