import { useState } from 'react'; import { ActivityIndicator, LayoutAnimation, Linking, Modal, Platform, TouchableOpacity, Text, UIManager, View, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { ConfirmAlert } from '../ConfirmAlert'; import { MailAccountSettingsSheet } from './MailAccountSettingsSheet'; import { MailBlockedByDayChart } from './MailBlockedByDayChart'; import { useMailConnectionStats } from '../../hooks/useMailStats'; import { apiFetch } from '../../lib/api'; import type { MailAccount } from '../../hooks/useMailStatus'; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } type ScanResult = { ok: boolean; scanned: number; blocked: number }; type Props = { account: MailAccount; plan: 'free' | 'pro' | 'legend'; expanded: boolean; onToggle: () => void; onDisconnect: (id: string) => Promise; onIntervalChanged: () => void; onEditSuccess: () => void; disconnecting?: boolean; blockedLast30d?: number; onScanSuccess?: () => void; }; function OAuthDisconnectHintModal({ visible, onClose, t, }: { visible: boolean; onClose: () => void; t: (key: string) => string; }) { return ( {}} style={{ width: '88%', maxWidth: 340 }}> {t('mail.oauth.disconnect_hint_title')} {t('mail.oauth.disconnect_hint_body')} Linking.openURL('https://account.microsoft.com/consent').catch(() => {})} style={{ flex: 1, paddingVertical: 10, borderRadius: 10, backgroundColor: '#eff6ff', borderWidth: 1, borderColor: '#bfdbfe', alignItems: 'center', }} > {t('mail.oauth.disconnect_hint_open_ms')} OK ); } function resolveProviderIcon(provider: string): { icon: React.ComponentProps['name']; color: string; } { const p = provider.toLowerCase(); if (p.includes('gmail') || p.includes('google')) return { icon: 'mail', color: '#EA4335' }; if (p.includes('icloud') || p.includes('apple')) return { icon: 'cloud', color: '#007AFF' }; if (p.includes('outlook') || p.includes('hotmail') || p.includes('microsoft')) return { icon: 'mail-open', color: '#0078D4' }; if (p.includes('yahoo')) return { icon: 'at', color: '#7C3AED' }; if (p.includes('gmx') || p.includes('web.de')) return { icon: 'mail-unread', color: '#E87A22' }; return { icon: 'server', color: '#737373' }; } function isOAuthProvider(provider: string): boolean { return provider === 'outlook_oauth'; } const STALE_THRESHOLD_MS = 5 * 60 * 1_000; const IDLE_HEARTBEAT_STALE_MS = STALE_THRESHOLD_MS; function idleHeartbeatAlive(lastIdleHeartbeatAt: string | null | undefined): boolean { if (!lastIdleHeartbeatAt) return false; return Date.now() - new Date(lastIdleHeartbeatAt).getTime() < IDLE_HEARTBEAT_STALE_MS; } function domainFromEmail(email: string): string { return email.split('@')[1] ?? email; } type StatusDot = 'live' | 'stale' | 'error' | 'waiting'; function resolveStatusDot(account: MailAccount): StatusDot { if (account.lastConnectError) return 'error'; // 'waiting' nur wenn weder ein lebendiger Heartbeat noch ein vergangener Scan // existiert. Bei frisch verbundenen Connections (z.B. OAuth-Outlook nach // ersten 5-30s) hat der Daemon schon einen Heartbeat geschrieben, aber // lastScannedAt bleibt NULL bis die erste Gambling-Mail trifft. Wir wollen // dann 'live' anzeigen, nicht 'waiting'. const heartbeatAlive = idleHeartbeatAlive(account.lastIdleHeartbeatAt); if (!account.lastScannedAt && !heartbeatAlive) return 'waiting'; if (account.lastScannedAt) { const scannedAgo = Date.now() - new Date(account.lastScannedAt).getTime(); if (!heartbeatAlive && scannedAgo > STALE_THRESHOLD_MS) return 'stale'; } return 'live'; } function StatusDotRow({ account, isLegend, blockedLast30d, t, }: { account: MailAccount; isLegend: boolean; blockedLast30d: number | undefined; t: (k: string) => string; }) { const dot = resolveStatusDot(account); const dotColor = dot === 'live' ? '#16a34a' : dot === 'stale' ? '#d97706' : dot === 'error' ? '#dc2626' : '#a3a3a3'; const label = dot === 'live' ? (isLegend ? t('mail.live') : t('mail.account_active')) : dot === 'stale' ? t('mail.status_stale') : dot === 'error' ? t('mail.status_auth_error') : t('mail.status_waiting_first_connect'); const blockedLabel = blockedLast30d !== undefined ? `${blockedLast30d}` : account.totalBlocked > 0 ? `${account.totalBlocked}` : '0'; return ( {label} {blockedLabel} ); } export function MailAccountCard({ account, plan, expanded, onToggle, onDisconnect, onIntervalChanged, onEditSuccess, disconnecting, blockedLast30d, onScanSuccess, }: Props) { const { t } = useTranslation(); const [settingsVisible, setSettingsVisible] = useState(false); const [confirmVisible, setConfirmVisible] = useState(false); const [oauthDisconnectHintVisible, setOauthDisconnectHintVisible] = useState(false); const [localTitle, setLocalTitle] = useState(account.title ?? null); const [scanning, setScanning] = useState(false); const [scanFeedback, setScanFeedback] = useState<{ blocked: number } | null>(null); const { icon, color } = resolveProviderIcon(account.provider); const { data: connStats, granularity, loading: statsLoading, refresh: refreshStats } = useMailConnectionStats( account.id, account.createdAt ?? null, expanded, ); const isOAuth = isOAuthProvider(account.provider); const isLegend = plan === 'legend'; const isPaused = account.paused === true; const hasError = !!account.lastConnectError; const displayTitle = localTitle ?? domainFromEmail(account.email); function handleToggle() { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); onToggle(); } async function handleScan() { setScanning(true); setScanFeedback(null); try { const result = await apiFetch('/api/mail/scan', { method: 'POST', body: { connectionId: account.id }, }); setScanFeedback({ blocked: result.blocked }); refreshStats(); onScanSuccess?.(); } catch { setScanFeedback({ blocked: -1 }); } finally { setScanning(false); } } function handleTitleSaved(newTitle: string | null) { setLocalTitle(newTitle); onEditSuccess(); } return ( <> {/* Header */} {displayTitle} {isPaused ? ( {t('plan_limit.mail_account_paused')} ) : ( )} {/* Expanded body */} {expanded && ( {/* Per-connection bar chart */} {granularity === 'too-new' ? ( {t('mail.account_chart_collecting_title')} {t('mail.account_chart_collecting_body')} ) : statsLoading && connStats.length === 0 ? ( {t('mail.loading')} ) : ( )} {/* Scan-Button + Einstellungen — horizontal nebeneinander */} {scanning ? ( ) : ( )} {scanning ? t('mail.scan_running') : scanFeedback?.blocked === -1 ? t('mail.scan_error') : scanFeedback?.blocked !== undefined ? t('mail.scan_done', { count: scanFeedback.blocked }) : t('mail.scan_now')} setSettingsVisible(true)} style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 13, gap: 4, }} > {t('mail.settings_section_label')} )} {/* Settings sub-sheet — inline edit, no nested sheets */} setSettingsVisible(false)} onTitleSaved={handleTitleSaved} onPasswordSaved={onEditSuccess} onDisconnectRequest={() => { setSettingsVisible(false); setConfirmVisible(true); }} onIntervalChanged={onIntervalChanged} /> { setConfirmVisible(false); await onDisconnect(account.id); if (isOAuth) setOauthDisconnectHintVisible(true); }} onCancel={() => setConfirmVisible(false)} /> setOauthDisconnectHintVisible(false)} t={t} /> ); }