feat(settings): Email (SMTP) BYO card in Settings → Integrations
Replace the SMTP 'coming soon' stub with a real card: host/port/SSL/user/password/ from-address/from-name → crm.settings.smtp.configure, masked status from crm.settings.smtp.status. Mirrors the Twilio SMS card; demo mode stores hints locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
// SMTP settings data layer — a tenant's own outbound email server (BYO). Serves the local mock
|
||||
// (Shell not configured) or the live be-crm data door (crm.settings.smtp.*).
|
||||
//
|
||||
// Live contract (be-crm → IIOS BYO credential store):
|
||||
// query crm.settings.smtp.status {} -> { configured, enabled?, hints? }
|
||||
// cmd crm.settings.smtp.configure { host, port, secure, user, pass, fromEmail, fromName? } -> masked status
|
||||
// The password is write-only: sealed in IIOS, never returned — status carries only non-secret hints.
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useAppShell, useQuery } from "@abe-kap/appshell-sdk/react";
|
||||
import { isShellConfigured } from "./appshell";
|
||||
|
||||
export interface SmtpCredentials {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
user: string;
|
||||
pass: string;
|
||||
fromEmail: string;
|
||||
fromName?: string;
|
||||
}
|
||||
|
||||
export interface SmtpStatus {
|
||||
configured: boolean;
|
||||
enabled: boolean;
|
||||
host?: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
fromEmail?: string;
|
||||
fromName?: string;
|
||||
}
|
||||
|
||||
export interface SmtpSettingsData {
|
||||
live: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
status: SmtpStatus;
|
||||
configure: (input: SmtpCredentials) => Promise<void>;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
interface StatusDTO { configured: boolean; enabled?: boolean; hints?: { host?: string; port?: number; user?: string; fromEmail?: string; fromName?: string } }
|
||||
|
||||
function toStatus(dto?: StatusDTO | null): SmtpStatus {
|
||||
return {
|
||||
configured: !!dto?.configured,
|
||||
enabled: dto?.enabled ?? false,
|
||||
host: dto?.hints?.host,
|
||||
port: dto?.hints?.port,
|
||||
user: dto?.hints?.user,
|
||||
fromEmail: dto?.hints?.fromEmail,
|
||||
fromName: dto?.hints?.fromName,
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- Mock (demo mode) — stores only the non-secret hints ---- */
|
||||
function useMockSmtp(): SmtpSettingsData {
|
||||
const [status, setStatus] = useState<SmtpStatus>({ configured: false, enabled: false });
|
||||
const configure = useCallback(async ({ host, port, user, fromEmail, fromName }: SmtpCredentials) => {
|
||||
setStatus({ configured: true, enabled: true, host, port, user, fromEmail, ...(fromName ? { fromName } : {}) });
|
||||
}, []);
|
||||
return { live: false, loading: false, error: null, status, configure, refetch: () => {} };
|
||||
}
|
||||
|
||||
/* ---- Live (be-crm data door) ---- */
|
||||
function useLiveSmtp(): SmtpSettingsData {
|
||||
const { sdk } = useAppShell();
|
||||
const q = useQuery<StatusDTO>("crm.settings.smtp.status", {});
|
||||
const configure = useCallback(async (input: SmtpCredentials) => {
|
||||
await sdk.command("crm.settings.smtp.configure", { ...input });
|
||||
q.refetch();
|
||||
}, [sdk, q]);
|
||||
return {
|
||||
live: true,
|
||||
loading: q.loading,
|
||||
error: q.error ? String(q.error) : null,
|
||||
status: toStatus(q.data),
|
||||
configure,
|
||||
refetch: q.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
const SHELL = isShellConfigured();
|
||||
|
||||
export function useSmtpSettings(): SmtpSettingsData {
|
||||
return SHELL ? useLiveSmtp() : useMockSmtp();
|
||||
}
|
||||
Reference in New Issue
Block a user