Files
Mattsson 1d63e0f72b 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>
2026-08-31 17:39:29 +02:00

96 lines
3.1 KiB
TypeScript

'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>
)
}