'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { useLocale, useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Sun, Moon, Monitor, LogOut, ExternalLink } from 'lucide-react' import { useTheme } from 'next-themes' import { createClient } from '@/lib/supabase/client' import { SecuritySettings } from '@/components/settings/SecuritySettings' import { EmailDigestToggle } from '@/components/settings/EmailDigestToggle' import { InstallAppSection } from '@/components/settings/InstallAppSection' import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings' import { AccountDangerZone } from '@/components/settings/AccountDangerZone' import { SettingsGroup, SettingsInput, SettingsRow, SettingsRowEnd, SettingsSectionHeader, SettingsSeg, } from '@/components/settings/SettingsRows' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { useSettings } from '@/components/settings/useSettings' import { resetAnalyticsIdentity } from '@/lib/analytics/reset' import { getErrorMessage } from '@/lib/errors/get-error-message' import { useToast } from '@/components/ui/use-toast' import { SUPPORTED_LOCALES, type Locale } from '@/i18n/config' import { PalettePicker } from '@/components/settings/PalettePicker' import { usePalette } from '@/components/providers/PaletteProvider' import type { Palette } from '@/lib/theme/palettes' export function AccountSettingsContent() { const router = useRouter() const supabase = createClient() const { theme, setTheme } = useTheme() const { palette, setPalette } = usePalette() const [mounted, setMounted] = useState(false) const hasCalendarExtension = ENABLED_EXTENSION_IDS.has('calendar') const { settings } = useSettings() const { toast } = useToast() const activeLocale = useLocale() as Locale const tCommon = useTranslations('common') const tSettings = useTranslations('settings') const tNav = useTranslations('settings_nav') const tIntro = useTranslations('settings_intro') const [savingLocale, setSavingLocale] = useState(false) const [fullName, setFullName] = useState('') const [initialName, setInitialName] = useState('') const [nameLoading, setNameLoading] = useState(true) const [savingName, setSavingName] = useState(false) const [email, setEmail] = useState('') const [currentEmail, setCurrentEmail] = useState('') const [savingEmail, setSavingEmail] = useState(false) const [pendingEmail, setPendingEmail] = useState(null) useEffect(() => { setMounted(true) }, []) // Pre-fill the name field from profiles.full_name. Self-contained client // fetch: mirrors BankIdSettings. useEffect(() => { let active = true ;(async () => { const { data: { user } } = await supabase.auth.getUser() if (!user) { if (active) setNameLoading(false); return } if (active && user.email) { setEmail(user.email) setCurrentEmail(user.email) } // A change already awaiting confirmation survives a page reload. if (active && user.new_email) setPendingEmail(user.new_email.toLowerCase()) const { data } = await supabase .from('profiles') .select('full_name') .eq('id', user.id) .maybeSingle() if (!active) return setFullName(data?.full_name ?? '') setInitialName(data?.full_name ?? '') setNameLoading(false) })() return () => { active = false } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) async function handleSaveName() { const trimmed = fullName.trim() if (!trimmed || trimmed === initialName || savingName) return setSavingName(true) try { const res = await fetch('/api/user/profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ full_name: trimmed }), }) if (!res.ok) throw new Error('Could not save') setFullName(trimmed) setInitialName(trimmed) toast({ title: tSettings('name_saved') }) router.refresh() } catch { toast({ title: tSettings('name_save_failed'), variant: 'destructive' }) } finally { setSavingName(false) } } async function handleSaveEmail() { const trimmed = email.trim().toLowerCase() if (!trimmed || trimmed === currentEmail.toLowerCase() || savingEmail) return setSavingEmail(true) try { const res = await fetch('/api/account/email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: trimmed }), }) const json = await res.json().catch(() => null) if (!res.ok) { toast({ title: tSettings('email_change_failed'), description: getErrorMessage(json, { statusCode: res.status, locale: activeLocale, }), variant: 'destructive', }) return } setPendingEmail(trimmed) const resent = (json as { data?: { resent?: boolean } } | null)?.data?.resent toast({ title: resent === false ? tSettings('email_change_already_pending') : tSettings('email_change_requested'), description: tSettings('email_change_requested_help'), }) } catch (err) { toast({ title: tSettings('email_change_failed'), description: getErrorMessage(err, { locale: activeLocale }), variant: 'destructive', }) } finally { setSavingEmail(false) } } async function handleLogout() { resetAnalyticsIdentity() await supabase.auth.signOut() router.push('/login') } async function handleLocaleChange(next: Locale) { if (next === activeLocale || savingLocale) return setSavingLocale(true) try { const res = await fetch('/api/user/locale', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ locale: next }), }) if (!res.ok) throw new Error('Could not save') toast({ title: tSettings('language_saved') }) router.refresh() } catch { toast({ title: tSettings('language_save_failed'), variant: 'destructive', }) } finally { setSavingLocale(false) } } const localeLabels: Record = { sv: tCommon('language_swedish'), en: tCommon('language_english'), } const paletteLabels: Record = { neutral: tSettings('palette_neutral'), indigo: tSettings('palette_indigo'), forest: tSettings('palette_forest'), sand: tSettings('palette_sand'), } const nameUnchanged = !fullName.trim() || fullName.trim() === initialName return (
{/* Profile: name, appearance, language, install-as-app */} setFullName(e.target.value)} placeholder={tSettings('name_placeholder')} disabled={nameLoading || savingName} maxLength={100} /> setEmail(e.target.value)} disabled={!currentEmail || savingEmail} maxLength={320} /> {mounted && ( {tCommon('theme_light')} ), }, { value: 'dark', label: ( {tCommon('theme_dark')} ), }, { value: 'system', label: ( {tCommon('theme_system')} ), }, ]} /> )} {mounted && ( )} void handleLocaleChange(next)} disabled={savingLocale} aria-label={tSettings('section_language')} options={SUPPORTED_LOCALES.map((value) => ({ value, label: localeLabels[value], }))} /> {/* Install as app: renders nothing when already running installed */} {/* Security: BankID, password, 2FA (renders its own group) */} {/* Notifications: daily "nytt att bokfora" email digest opt-in */} {/* Calendar feed (extension-gated) */} {hasCalendarExtension && } {/* Privacy & agreements: surface the otherwise-unlinked DPA + privacy policy */} {/* Sign out */} {/* Delete account: only for non-sandbox */} {!settings?.is_sandbox && }
) }