import React, { useCallback, useEffect, useState } from "react"; import { Box, Button, Checkbox, Chip, Divider, FormControlLabel, Paper, Stack, TextField, Typography } from "@mui/material"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import type { GmailStatus, ImapStatus, MicrosoftGraphStatus } from "../types"; // Settings > Account: connect/disconnect each linked-mailbox provider. Gmail and Microsoft use // the same OAuth-popup + postMessage handshake (mirrored server-side in GmailController / // MicrosoftGraphController's BuildPopupHtml); IMAP has no OAuth step, so it's a plain credential // form submitted to POST /api/imap/connect, which verifies the connection before storing it. export default function EmailProviderConnections() { const { toast } = useToast(); const [gmailStatus, setGmailStatus] = useState(null); const [microsoftStatus, setMicrosoftStatus] = useState(null); const [imapStatus, setImapStatus] = useState(null); const [imapForm, setImapForm] = useState({ host: "", port: 993, useSsl: true, username: "", password: "" }); const [imapConnecting, setImapConnecting] = useState(false); const loadGmailStatus = useCallback(async () => { try { const res = await api.get("/gmail/status"); setGmailStatus(res.data); } catch { setGmailStatus({ connected: false }); } }, []); const loadMicrosoftStatus = useCallback(async () => { try { const res = await api.get("/microsoft-graph/status"); setMicrosoftStatus(res.data); } catch { setMicrosoftStatus({ connected: false }); } }, []); const loadImapStatus = useCallback(async () => { try { const res = await api.get("/imap/status"); setImapStatus(res.data); } catch { setImapStatus({ connected: false }); } }, []); useEffect(() => { void loadGmailStatus(); void loadMicrosoftStatus(); void loadImapStatus(); }, [loadGmailStatus, loadMicrosoftStatus, loadImapStatus]); useEffect(() => { const onMessage = (event: MessageEvent) => { const data = event.data as { source?: string; status?: string; message?: string }; if (data?.source === "jobtracker-gmail-oauth") { if (data.status === "connected") { toast(data.message || "Gmail connected.", "success"); void loadGmailStatus(); } else { toast(data.message || "Gmail connection failed.", "error"); } } else if (data?.source === "jobtracker-microsoft-oauth") { if (data.status === "connected") { toast(data.message || "Outlook connected.", "success"); void loadMicrosoftStatus(); } else { toast(data.message || "Outlook connection failed.", "error"); } } }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, [loadGmailStatus, loadMicrosoftStatus, toast]); const connectViaPopup = async (connectUrlPath: string, popupName: string, providerLabel: string) => { try { const res = await api.get<{ url: string }>(connectUrlPath); const popup = window.open(res.data.url, popupName, "width=620,height=760,resizable=yes,scrollbars=yes"); if (!popup) toast("Your browser blocked the connect popup. Allow popups and try again.", "error"); } catch (error) { toast(getApiErrorMessage(error, `Failed to start ${providerLabel} connection.`), "error"); } }; const disconnect = async (path: string, reload: () => Promise, providerLabel: string) => { try { await api.delete(path); await reload(); toast(`${providerLabel} disconnected.`, "success"); } catch (error) { toast(getApiErrorMessage(error, `Failed to disconnect ${providerLabel}.`), "error"); } }; const connectImap = async () => { if (!imapForm.host.trim() || !imapForm.username.trim() || !imapForm.password) { toast("Host, username, and password are required.", "error"); return; } setImapConnecting(true); try { await api.post("/imap/connect", imapForm); setImapForm((prev) => ({ ...prev, password: "" })); await loadImapStatus(); toast("IMAP account connected.", "success"); } catch (error) { toast(getApiErrorMessage(error, "Failed to connect IMAP account."), "error"); } finally { setImapConnecting(false); } }; return ( Linked email accounts Connect a mailbox so recruiter correspondence can be linked to jobs automatically. void connectViaPopup("/gmail/connect-url", "jobtracker-gmail-connect", "Gmail")} onDisconnect={() => void disconnect("/gmail/connection", loadGmailStatus, "Gmail")} /> void connectViaPopup("/microsoft-graph/connect-url", "jobtracker-microsoft-connect", "Outlook")} onDisconnect={() => void disconnect("/microsoft-graph/connection", loadMicrosoftStatus, "Outlook")} /> void disconnect("/imap/connection", loadImapStatus, "IMAP")} /> {!imapStatus?.connected && ( setImapForm((prev) => ({ ...prev, host: e.target.value }))} /> setImapForm((prev) => ({ ...prev, port: Number(e.target.value) || 993 }))} /> setImapForm((prev) => ({ ...prev, username: e.target.value }))} sx={{ gridColumn: "1 / -1" }} /> setImapForm((prev) => ({ ...prev, password: e.target.value }))} sx={{ gridColumn: "1 / -1" }} /> setImapForm((prev) => ({ ...prev, useSsl: e.target.checked }))} />} label="Use SSL/TLS" /> )} ); } function ProviderRow({ label, connected, address, onConnect, onDisconnect, }: { label: string; connected: boolean; address: string | null; onConnect?: () => void; onDisconnect: () => void; }) { return ( {label} {connected ? ( } color="success" variant="outlined" label={address || "Connected"} sx={{ mt: 0.5 }} /> ) : ( Not connected )} {connected ? ( ) : ( onConnect && )} ); }