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:
co-authored by
Claude Fable 5
parent
9fe37b85b5
commit
1d63e0f72b
@@ -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 />}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user