'use client' import { useCallback, useEffect, useState } from 'react' import { useTranslations } from 'next-intl' import { Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { SettingsGroup, SettingsRow, SettingsRowEnd, SettingsRowNote, } from '@/components/settings/SettingsRows' import { ConfirmDialog } from '@/components/ui/confirm-dialog' import { GoogleMark, MicrosoftMark } from '@/components/ui/provider-marks' import { formatDateLong } from '@/lib/utils' interface HuntResult { searched: number fetched: number proposed: number remaining: number failed?: boolean } interface MailConnection { id: string provider: 'gmail' | 'microsoft' emailAddress: string scopeLabel: string | null status: 'active' | 'needs_reconsent' | 'revoked' lastSearchedAt: string | null lastErrorCode: string | null } const BASE = '/api/extensions/ext/mail' export function MailConnectionsPanel() { const t = useTranslations('mail') const [connections, setConnections] = useState([]) const [configured, setConfigured] = useState(true) const [loading, setLoading] = useState(true) const [connecting, setConnecting] = useState(false) const [pendingDisconnect, setPendingDisconnect] = useState(null) const [hunting, setHunting] = useState(false) const [huntResult, setHuntResult] = useState(null) const load = useCallback(async () => { try { const response = await fetch(`${BASE}/connections`) if (!response.ok) return const body = (await response.json()) as { data: { connections: MailConnection[]; configured: boolean } } setConnections(body.data.connections) setConfigured(body.data.configured) } finally { setLoading(false) } }, []) useEffect(() => { void load() }, [load]) async function connect() { setConnecting(true) try { // The consent screen must open from the user's own gesture, so the tab is // opened first and its location set once the URL is known: opening it // after the await is what popup blockers stop. const tab = window.open('', '_blank') const response = await fetch(`${BASE}/oauth/start`, { method: 'POST' }) if (!response.ok) { tab?.close() return } const body = (await response.json()) as { url: string } if (tab) tab.location.href = body.url else window.location.href = body.url } finally { setConnecting(false) } } /** * One bounded pass, on request. * * Deliberately not a background job: a sweep of a real mailbox runs longer * than a serverless function may live, so the honest shape is a pass that * ends, says what it found and what is left, and can be pressed again. */ async function hunt() { setHunting(true) setHuntResult(null) try { const response = await fetch('/api/receipt-hunt/run', { method: 'POST' }) if (!response.ok) { setHuntResult({ searched: 0, fetched: 0, proposed: 0, remaining: 0, failed: true }) return } const body = (await response.json()) as { data: HuntResult } setHuntResult(body.data) void load() } catch { setHuntResult({ searched: 0, fetched: 0, proposed: 0, remaining: 0, failed: true }) } finally { setHunting(false) } } async function disconnect(connection: MailConnection) { await fetch(`${BASE}/connections?id=${encodeURIComponent(connection.id)}`, { method: 'DELETE' }) setPendingDisconnect(null) void load() } if (loading) return null return (
{connections.length === 0 ? ( {t('none')} ) : ( connections.map((connection) => ( {connection.provider === 'gmail' ? ( ) : ( )} {connection.provider === 'gmail' ? 'Gmail' : 'Microsoft 365'} } > {connection.emailAddress} {connection.status === 'needs_reconsent' ? ( {t('needs_reconsent')} ) : null} {connection.lastSearchedAt ? ( {t('last_searched', { date: formatDateLong(connection.lastSearchedAt, 'sv') })} ) : null} )) )} {connections.length > 0 ? ( {huntResult ? ( {huntResult.failed ? t('hunt_failed') : huntResult.fetched > 0 ? t('hunt_found', { count: huntResult.fetched, left: huntResult.remaining }) : t('hunt_none', { left: huntResult.remaining })} ) : null} ) : null}
{!configured ? {t('not_configured')} : null}

{t('promise')}

!open && setPendingDisconnect(null)} title={t('disconnect_title')} description={t('disconnect_body', { address: pendingDisconnect?.emailAddress ?? '' })} confirmLabel={t('disconnect')} onConfirm={async () => { if (pendingDisconnect) await disconnect(pendingDisconnect) }} />
) }