From 1d63e0f72b3d0987c188a48bb8eb8ce07aee70dc Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:39:29 +0200 Subject: [PATCH] fix(settings): reachable opt-in toggle for the bookkeeping digest (#2085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(settings): reachable opt-in toggle for the bookkeeping digest The digest toggle shipped in PR #2078 inside the push-notifications extension's settings panel, but that extension is not enabled on hosted (extensions.config.json), so the panel never renders and nobody could opt in: the cron and email path were live with an unreachable switch. Adds an "Aviseringar" group on the core account settings tab with the email_digest_enabled toggle (upsert on notification_settings, own-row RLS), localized in both sv and en. The extension panel keeps its copy for installs that enable push-notifications. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0122MznXxrLRyT96fGhfyzD4 * fix(i18n): natural English label for the digest toggle Skeptic note: "New to bookkeep" is awkward; "New items to book" mirrors the Swedish "Nytt att bokföra". Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0122MznXxrLRyT96fGhfyzD4 * fix(settings): log digest-toggle read/write failures Compliance swarm (ASVS V16.2, SOC 2 CC7.2): the catch blocks swallowed errors silently; surface them via console.error like the sibling notification settings component. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0122MznXxrLRyT96fGhfyzD4 --------- Co-authored-by: Claude Fable 5 --- components/settings/EmailDigestToggle.tsx | 95 +++++++++++++++++++ .../sections/AccountSettingsContent.tsx | 4 + messages/en.json | 7 ++ messages/sv.json | 7 ++ 4 files changed, 113 insertions(+) create mode 100644 components/settings/EmailDigestToggle.tsx diff --git a/components/settings/EmailDigestToggle.tsx b/components/settings/EmailDigestToggle.tsx new file mode 100644 index 00000000..0abc0ca0 --- /dev/null +++ b/components/settings/EmailDigestToggle.tsx @@ -0,0 +1,95 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useTranslations } from 'next-intl' +import { Switch } from '@/components/ui/switch' +import { useToast } from '@/components/ui/use-toast' +import { SettingsGroup, SettingsRow } from '@/components/settings/SettingsRows' +import { createClient } from '@/lib/supabase/client' + +/** + * Per-user opt-in for the daily "nytt att bokfora" email digest + * (notification_settings.email_digest_enabled, default false; consumed by + * the bookkeeping-digest cron). Lives on the core account tab: the + * push-notifications extension has its own settings panel with the same + * toggle, but that extension is not enabled on hosted, so this is the + * reachable switch. + */ +export function EmailDigestToggle() { + const t = useTranslations('settings') + const { toast } = useToast() + const supabase = createClient() + const [enabled, setEnabled] = useState(false) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + let active = true + ;(async () => { + try { + const { data: { user } } = await supabase.auth.getUser() + if (!user) return + const { data } = await supabase + .from('notification_settings') + .select('email_digest_enabled') + .eq('user_id', user.id) + .maybeSingle() + if (active) setEnabled(data?.email_digest_enabled === true) + } catch (err) { + // Leave the default (off); a failed read must not block the page. + console.error('Could not read email digest setting:', err) + } finally { + if (active) setLoading(false) + } + })() + return () => { + active = false + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + async function handleChange(next: boolean) { + setEnabled(next) + setSaving(true) + try { + const { data: { user } } = await supabase.auth.getUser() + if (!user) throw new Error('Not signed in') + // Upsert on user_id: most users have no notification_settings row + // until they touch a notification preference for the first time. + const { error } = await supabase + .from('notification_settings') + .upsert( + { user_id: user.id, email_digest_enabled: next }, + { onConflict: 'user_id' }, + ) + if (error) throw new Error(error.message) + toast({ + title: next + ? t('digest_enabled_toast') + : t('digest_disabled_toast'), + }) + } catch (err) { + console.error('Could not save email digest setting:', err) + setEnabled(!next) + toast({ title: t('digest_save_failed'), variant: 'destructive' }) + } finally { + setSaving(false) + } + } + + return ( + + + void handleChange(value)} + disabled={loading || saving} + /> + + + + ) +} diff --git a/components/settings/sections/AccountSettingsContent.tsx b/components/settings/sections/AccountSettingsContent.tsx index 6d339758..9d6a089c 100644 --- a/components/settings/sections/AccountSettingsContent.tsx +++ b/components/settings/sections/AccountSettingsContent.tsx @@ -9,6 +9,7 @@ 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' @@ -335,6 +336,9 @@ export function AccountSettingsContent() { {/* Security: BankID, password, 2FA (renders its own group) */} + {/* Notifications: daily "nytt att bokfora" email digest opt-in */} + + {/* Calendar feed (extension-gated) */} {hasCalendarExtension && } diff --git a/messages/en.json b/messages/en.json index 2b033409..5f437869 100644 --- a/messages/en.json +++ b/messages/en.json @@ -582,6 +582,13 @@ "settings": { "group_profile": "Profile", "section_name": "Name", + "digest_group": "Notifications", + "digest_label": "New items to book", + "digest_description": "A daily email when new bank transactions or inbox documents are waiting to be booked. Counts only, never amounts.", + "digest_switch_label": "Daily summary by email", + "digest_enabled_toast": "Daily summary enabled", + "digest_disabled_toast": "Daily summary disabled", + "digest_save_failed": "Could not save the setting", "name_label": "Your name", "name_description": "Used when we address you and shown as the contact person on some documents.", "name_placeholder": "First name Last name", diff --git a/messages/sv.json b/messages/sv.json index 588ac569..ec92336f 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -582,6 +582,13 @@ "settings": { "group_profile": "Profil", "section_name": "Namn", + "digest_group": "Aviseringar", + "digest_label": "Nytt att bokföra", + "digest_description": "Ett dagligt mejl när nya banktransaktioner eller underlag i inkorgen väntar på bokföring. Endast antal, aldrig belopp.", + "digest_switch_label": "Daglig sammanfattning via e-post", + "digest_enabled_toast": "Daglig sammanfattning aktiverad", + "digest_disabled_toast": "Daglig sammanfattning avstängd", + "digest_save_failed": "Kunde inte spara inställningen", "name_label": "Ditt namn", "name_description": "Används när vi tilltalar dig och visas som kontaktperson på vissa underlag.", "name_placeholder": "Förnamn Efternamn",