fix(settings): reachable opt-in toggle for the bookkeeping digest (#2085)

* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122MznXxrLRyT96fGhfyzD4

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-31 17:39:29 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 9fe37b85b5
commit 1d63e0f72b
4 changed files with 113 additions and 0 deletions
+95
View File
@@ -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 (
<SettingsGroup label={t('digest_group')}>
<SettingsRow label={t('digest_label')} help={t('digest_description')}>
<Switch
id="email-digest"
checked={enabled}
onCheckedChange={(value) => void handleChange(value)}
disabled={loading || saving}
/>
<label htmlFor="email-digest" className="cursor-pointer text-sm">
{t('digest_switch_label')}
</label>
</SettingsRow>
</SettingsGroup>
)
}
@@ -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) */}
<SecuritySettings />
{/* Notifications: daily "nytt att bokfora" email digest opt-in */}
<EmailDigestToggle />
{/* Calendar feed (extension-gated) */}
{hasCalendarExtension && <CalendarFeedSettings />}
+7
View File
@@ -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",
+7
View File
@@ -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",