import { useState } from 'react'; import { ActivityIndicator, Linking, ScrollView, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import * as WebBrowser from 'expo-web-browser'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useMailConnect, type MailProvider } from '../../hooks/useMailConnect'; import { humanizeMailError } from '../../lib/mailErrors'; import { apiFetch } from '../../lib/api'; import { useColors } from '../../lib/theme'; import { FormSheet } from '../FormSheet'; import { useMailConnectDraft } from '../../stores/mailConnectDraft'; const CONSENT_VERSION = 'art9-mail-v1-2026-05-13'; const PRIVACY_URL = 'https://rebreak.org/datenschutz'; type Props = { visible: boolean; onClose: () => void; onSuccess: () => void; }; type ProviderConfig = { id: MailProvider; labelKey: string; icon: React.ComponentProps['name']; color: string; guideKey: string; guideUrl: string; disabled?: boolean; disabledLabelKey?: string; authMethod?: 'imap' | 'oauth_microsoft' | 'oauth_google'; }; const PROVIDERS: ProviderConfig[] = [ { id: 'gmail', labelKey: 'mail.provider_gmail', icon: 'mail', color: '#EA4335', guideKey: 'mail.app_password_guide_gmail', guideUrl: 'https://myaccount.google.com/apppasswords', authMethod: 'oauth_google', }, { id: 'icloud', labelKey: 'mail.provider_icloud', icon: 'cloud', color: '#007AFF', guideKey: 'mail.app_password_guide_icloud', guideUrl: 'https://appleid.apple.com/account/manage', }, { id: 'outlook', labelKey: 'mail.provider_outlook', icon: 'mail-open', color: '#0078D4', guideKey: 'mail.app_password_guide_outlook', guideUrl: 'https://account.microsoft.com/security', authMethod: 'oauth_microsoft', }, { id: 'yahoo', labelKey: 'mail.provider_yahoo', icon: 'at', color: '#7C3AED', guideKey: 'mail.app_password_guide_yahoo', guideUrl: 'https://login.yahoo.com/account/security', }, { id: 'gmx', labelKey: 'mail.provider_gmx', icon: 'mail-unread', color: '#E87A22', guideKey: 'mail.app_password_guide_gmx', guideUrl: 'https://www.gmx.net/mail/security', }, { id: 'other', labelKey: 'mail.provider_other', icon: 'server', color: '#737373', guideKey: 'mail.app_password_guide_other', guideUrl: '', }, ]; /** * Bottom-Sheet zum Verbinden eines Postfachs. * * Drei Ansichten im selben Sheet (kein Navigations-Header): * 1. Consent-Screen (Art. 9 DSGVO) — MUSS zuerst bestätigt werden * 2. Provider-Grid (6 Tiles) — nach Consent-Bestätigung freigeschaltet * 3. Formular: Email → App-Passwort → Bezeichnung (eine ScrollView) */ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) { const { t } = useTranslation(); const colors = useColors(); const { connect, connecting, error: connectError } = useMailConnect(); const { view, consentGiven, selectedProvider, email, title, pendingOAuthConnectionId, setView, setConsentGiven, setSelectedProvider, setEmail, setTitle, setPendingOAuthConnectionId, reset: resetDraft, } = useMailConnectDraft(); const [password, setPassword] = useState(''); const [passwordVisible, setPasswordVisible] = useState(false); const [formError, setFormError] = useState(null); const [oauthRunning, setOauthRunning] = useState(false); const [oauthError, setOauthError] = useState(null); function handleClose() { resetDraft(); setPassword(''); setPasswordVisible(false); setFormError(null); setOauthRunning(false); setOauthError(null); onClose(); } function defaultTitleForProvider(provider: ProviderConfig | null): string { if (!provider) return ''; const labelMap: Record = { gmail: 'Mein Gmail', icloud: 'Mein iCloud', outlook: 'Mein Outlook', yahoo: 'Mein Yahoo', gmx: 'Mein GMX', other: '', }; return labelMap[provider.id] ?? ''; } function handleConsentNext() { setView('grid'); } function handleProviderSelect(provider: ProviderConfig) { setSelectedProvider(provider); setEmail(''); setPassword(''); setTitle(defaultTitleForProvider(provider)); setFormError(null); setOauthError(null); if (provider.authMethod === 'oauth_microsoft' || provider.authMethod === 'oauth_google') { setView('oauth_warning'); } else { setView('form'); } } async function handleOAuthStart() { const isGoogle = selectedProvider?.authMethod === 'oauth_google'; const providerPath = isGoogle ? 'google' : 'microsoft'; // Google iOS-OAuth-Client verlangt Reverse-Client-ID-Redirect-URI (statt rebreak://-Scheme). // Muss exakt mit Backend GOOGLE_REDIRECT_URI in google-oauth.ts matchen. const returnUrl = isGoogle ? 'com.googleusercontent.apps.864178840836-i09oblmcel5q4rgggq9dids17mv9560u:/oauth2redirect' : 'rebreak://auth/mail-oauth-callback'; setOauthRunning(true); setOauthError(null); setView('oauth_pending'); try { const { authorizationUrl } = await apiFetch<{ authorizationUrl: string }>( `/api/mail/oauth/${providerPath}/init`, { method: 'POST', body: email.trim() ? { email: email.trim() } : {} } ); try { await WebBrowser.dismissAuthSession(); } catch {} const result = await WebBrowser.openAuthSessionAsync( authorizationUrl, returnUrl ); console.log('[oauth] WebBrowser result.type=', result.type); if (result.type !== 'success') { setOauthError(t('mail.oauth.error_aborted')); setView('oauth_warning'); setOauthRunning(false); return; } console.log('[oauth] result.url=', (result as any).url); const url = new URL((result as any).url); const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); const oauthError = url.searchParams.get('error'); const oauthErrorDescription = url.searchParams.get('error_description'); console.log('[oauth] code?=', !!code, 'state?=', !!state, 'error=', oauthError, 'desc=', oauthErrorDescription); if (oauthError) { const providerLabel = isGoogle ? 'Google' : 'Microsoft'; setOauthError(`${providerLabel}: ${oauthError}${oauthErrorDescription ? ` — ${oauthErrorDescription}` : ''}`); setView('oauth_warning'); setOauthRunning(false); return; } if (!code || !state) { setOauthError(t('mail.oauth.error_no_code')); setView('oauth_warning'); setOauthRunning(false); return; } const conn = await apiFetch<{ connectionId: string; email: string; provider: string; title: null }>( `/api/mail/oauth/${providerPath}/callback`, { method: 'POST', body: { code, state } } ); setPendingOAuthConnectionId(conn.connectionId); setOauthRunning(false); handleClose(); onSuccess(); } catch (e: any) { const detail = (e?.message ?? String(e)) || 'unknown'; console.log('[oauth] callback API call failed — error=', detail); setOauthError(`${t('mail.oauth.error_callback_failed')}\n${detail}`); setView('oauth_warning'); setOauthRunning(false); } } async function handleConnect() { setFormError(null); try { await apiFetch('/api/mail-connections/consent', { method: 'POST', body: { consentVersion: CONSENT_VERSION }, }); } catch { // Backend macht Consent atomar beim Connect-Endpoint — Fehler hier ignorieren. } const result = await connect({ email: email.trim(), password }); if (result.ok) { if (title.trim()) { try { const connections = await apiFetch<{ id: string; email: string }[]>('/api/mail-connections'); const match = connections.find((c) => c.email === email.trim()); if (match) { await apiFetch(`/api/mail-connections/${match.id}`, { method: 'PATCH', body: { title: title.trim() }, }); } } catch { // Title-PATCH ist best-effort — Connection selbst ist OK } } handleClose(); onSuccess(); } else if (result.error?.includes('412') || result.error?.includes('consent_required')) { setView('consent'); setConsentGiven(false); } else { setFormError(t(humanizeMailError(result.error))); } } const sheetTitle = (view === 'form' || view === 'oauth_warning' || view === 'oauth_pending') && selectedProvider ? t(selectedProvider.labelKey) : t('mail.connect_sheet_title'); return ( {view === 'consent' ? ( ) : view === 'grid' ? ( ) : view === 'oauth_warning' ? ( setView('grid')} t={t} colors={colors} /> ) : view === 'oauth_pending' ? ( ) : ( )} ); } // --------------------------------------------------------------------------- // Sub-View: Form (email → password → title, eine ScrollView) // --------------------------------------------------------------------------- function FormView({ selectedProvider, email, setEmail, password, setPassword, passwordVisible, setPasswordVisible, title, setTitle, formError, setFormError, connectError, connecting, onConnect, t, colors, }: { selectedProvider: { id: string; guideKey: string; guideUrl: string } | null; email: string; setEmail: (v: string) => void; password: string; setPassword: (v: string) => void; passwordVisible: boolean; setPasswordVisible: (v: boolean) => void; title: string; setTitle: (v: string) => void; formError: string | null; setFormError: (v: string | null) => void; connectError: string | null; connecting: boolean; onConnect: () => void; t: (key: string) => string; colors: ReturnType; }) { const errorText = formError ?? (connectError ? t(humanizeMailError(connectError)) : null); return ( {/* App-Password-Banner — provider-spezifisch, nicht für 'other' */} {selectedProvider && selectedProvider.id !== 'other' && ( {t('mail.app_password_required_title')} {t(selectedProvider.guideKey)} {selectedProvider.guideUrl.length > 0 && ( Linking.openURL(selectedProvider.guideUrl)} > {t('mail.app_password_open_link')} → )} )} {/* E-Mail */} {t('mail.form_email_label')} { setEmail(v); setFormError(null); }} placeholder={t('mail.form_email_placeholder')} placeholderTextColor={colors.textMuted} keyboardType="email-address" autoCapitalize="none" autoCorrect={false} style={{ paddingVertical: 12, fontSize: 15, fontFamily: 'Nunito_400Regular', color: colors.text, }} /> {/* App-Passwort */} {t('mail.form_password_label')} { setPassword(v); setFormError(null); }} placeholder={t('mail.form_password_placeholder')} placeholderTextColor={colors.textMuted} secureTextEntry={!passwordVisible} autoCapitalize="none" autoCorrect={false} style={{ flex: 1, paddingVertical: 12, fontSize: 15, fontFamily: 'Nunito_400Regular', color: colors.text, }} /> setPasswordVisible(!passwordVisible)} hitSlop={8} > {/* AES-Verschlüsselungs-Hinweis als Footnote */} {t('mail.form_privacy_note')} {/* Bezeichnung */} {t('mail.title_label')} {/* Fehler */} {errorText && ( {errorText} )} {/* Connect-Button */} {connecting ? ( ) : ( {t('mail.form_connect_btn')} )} ); } // --------------------------------------------------------------------------- // Sub-View: Consent (Art. 9 DSGVO) — muss als erster Schritt bestätigt werden // --------------------------------------------------------------------------- function ConsentStep({ consentGiven, onToggleConsent, onNext, t, colors, }: { consentGiven: boolean; onToggleConsent: (v: boolean) => void; onNext: () => void; t: (key: string) => string; colors: ReturnType; }) { return ( {t('mail.consent.intro')} {t('mail.consent.legal_text')} onToggleConsent(!consentGiven)} style={{ flex: 1 }} > {t('mail.consent.checkbox_label')} Linking.openURL(PRIVACY_URL)} > {t('mail.consent.more_link')} → {t('mail.consent.cta_next')} ); } // --------------------------------------------------------------------------- // Sub-View: OAuth Warning (DSB Memo Section 6.1 — Outing-Effekt-Hinweis) // --------------------------------------------------------------------------- function OAuthWarningStep({ isGoogle, error, onContinue, onCancel, t, colors, }: { isGoogle?: boolean; error: string | null; onContinue: () => void; onCancel: () => void; t: (key: string) => string; colors: ReturnType; }) { const accentColor = isGoogle ? '#EA4335' : '#0078D4'; const warningTitleKey = isGoogle ? 'mail.oauth.google_warning_title' : 'mail.oauth.warning_title'; const warningBodyKey = isGoogle ? 'mail.oauth.google_warning_body' : 'mail.oauth.warning_body'; const continueKey = isGoogle ? 'mail.oauth.google_warning_continue' : 'mail.oauth.warning_continue'; return ( {t(warningTitleKey)} {t(warningBodyKey)} {error ? ( {error} ) : null} {t(continueKey)} {t('mail.oauth.warning_cancel')} ); } // --------------------------------------------------------------------------- // Sub-View: OAuth Pending (Browser läuft gerade) // --------------------------------------------------------------------------- function OAuthPendingStep({ isGoogle, t, colors, }: { isGoogle?: boolean; t: (key: string) => string; colors: ReturnType; }) { const accentColor = isGoogle ? '#EA4335' : '#0078D4'; const pendingLabelKey = isGoogle ? 'mail.oauth.google_pending_label' : 'mail.oauth.pending_label'; return ( {t(pendingLabelKey)} {t('mail.oauth.pending_hint')} ); } // --------------------------------------------------------------------------- // Sub-View: Provider-Grid // --------------------------------------------------------------------------- function ProviderGrid({ providers, onSelect, t, }: { providers: ProviderConfig[]; onSelect: (p: ProviderConfig) => void; t: (key: string) => string; }) { const colors = useColors(); return ( {t('mail.connect_sheet_subtitle')} {providers.map((p) => ( onSelect(p)} activeOpacity={p.disabled ? 1 : 0.7} disabled={p.disabled} style={{ width: '47%', opacity: p.disabled ? 0.45 : 1 }} > {t(p.labelKey)} {p.disabled && p.disabledLabelKey && ( {t(p.disabledLabelKey)} )} {!p.disabled && ( )} ))} ); }