feat: Registrierungs-Disclaimer und Header-Zugang

Neue Accounts sehen vor dem Onboarding Hinweise zu E2E, PWA, Sync und Haftung;
bestehende Nutzer können den Disclaimer jederzeit über einen Header-Button öffnen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 18:18:08 +02:00
co-authored by Cursor
parent 51ffc33f32
commit 819d84eaee
9 changed files with 295 additions and 4 deletions
+26 -2
View File
@@ -12,6 +12,7 @@ import {
forgetUsername
} from '../services/auth.js'
import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react'
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
interface AuthOnboardingProps {
onAuthenticated: () => void
@@ -45,6 +46,23 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
const [showPinLogin, setShowPinLogin] = useState(false)
const [pinLoginInput, setPinLoginInput] = useState('')
const [isNewRegistration, setIsNewRegistration] = useState(false)
const [showDisclaimer, setShowDisclaimer] = useState(false)
const finishAuth = () => {
if (isNewRegistration) {
setShowDisclaimer(true)
return
}
onAuthenticated()
}
const handleDisclaimerAccept = () => {
setIsNewRegistration(false)
setShowDisclaimer(false)
onAuthenticated()
}
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault()
if (!username.trim()) return
@@ -54,6 +72,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
try {
const result = await registerUser(username.trim())
if (result.verified) {
setIsNewRegistration(true)
setRecoveryPhrase(result.recoveryPhrase)
}
} catch (err: any) {
@@ -148,7 +167,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
const activeKey = getActiveMasterKey()
if (activeKey) {
await setLocalPin(pinInput.trim(), pinSetupUsername, activeKey)
onAuthenticated()
finishAuth()
} else {
setError('No active master key found')
}
@@ -198,6 +217,11 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
}
}
// Render 0: Registration disclaimer (new accounts only, before app onboarding)
if (showDisclaimer) {
return <RegistrationDisclaimer variant="accept" onDismiss={handleDisclaimerAccept} />
}
// Render 1: Display new registration recovery phrase
if (recoveryPhrase) {
return (
@@ -266,7 +290,7 @@ export default function AuthOnboarding({ onAuthenticated }: AuthOnboardingProps)
<button
type="button"
className="btn secondary"
onClick={onAuthenticated}
onClick={finishAuth}
disabled={loading}
>
{t('auth.skip_pin')}
@@ -0,0 +1,24 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ScrollText } from 'lucide-react'
import DisclaimerModal from './DisclaimerModal.tsx'
export default function DisclaimerHeaderButton() {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<>
<button
type="button"
className="btn-icon"
onClick={() => setOpen(true)}
title={t('disclaimer.button_title')}
aria-label={t('disclaimer.button_title')}
>
<ScrollText size={18} />
</button>
<DisclaimerModal open={open} onClose={() => setOpen(false)} />
</>
)
}
+28
View File
@@ -0,0 +1,28 @@
import { useEffect } from 'react'
import RegistrationDisclaimer from './RegistrationDisclaimer.tsx'
interface DisclaimerModalProps {
open: boolean
onClose: () => void
}
export default function DisclaimerModal({ open, onClose }: DisclaimerModalProps) {
useEffect(() => {
if (!open) return
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [open, onClose])
if (!open) return null
return (
<div className="disclaimer-modal-overlay" onClick={onClose}>
<div className="disclaimer-modal-panel" onClick={(event) => event.stopPropagation()}>
<RegistrationDisclaimer variant="view" onDismiss={onClose} />
</div>
</div>
)
}
@@ -7,6 +7,7 @@ import { logoutUser } from '../services/auth.js'
import { useDialog } from './ModalDialog.tsx'
import AccountDangerZone from './AccountDangerZone.tsx'
import { BookOpen, Plus, Trash2, LogOut, Languages, RefreshCw, Ship, User, Wifi, WifiOff } from 'lucide-react'
import DisclaimerHeaderButton from './DisclaimerHeaderButton.tsx'
interface LogbookDashboardProps {
onSelectLogbook: (id: string, title: string) => void
@@ -149,6 +150,8 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout }: LogbookD
<Languages size={18} />
</button>
<DisclaimerHeaderButton />
{/* Logout */}
<button className="btn-icon logout" onClick={handleLogout} title={t('dashboard.logout')}>
<LogOut size={18} />
@@ -0,0 +1,66 @@
import { useTranslation } from 'react-i18next'
import { ScrollText, X } from 'lucide-react'
export type DisclaimerVariant = 'accept' | 'view'
interface RegistrationDisclaimerProps {
onDismiss: () => void
variant?: DisclaimerVariant
}
export default function RegistrationDisclaimer({
onDismiss,
variant = 'accept'
}: RegistrationDisclaimerProps) {
const { t } = useTranslation()
const sections = [
{ title: t('disclaimer.e2e_title'), body: t('disclaimer.e2e_body') },
{ title: t('disclaimer.pwa_title'), body: t('disclaimer.pwa_body') },
{ title: t('disclaimer.storage_title'), body: t('disclaimer.storage_body') },
{ title: t('disclaimer.free_title'), body: t('disclaimer.free_body') },
{ title: t('disclaimer.liability_title'), body: t('disclaimer.liability_body') },
{ title: t('disclaimer.warranty_title'), body: t('disclaimer.warranty_body') }
]
return (
<div
className={`auth-card glass registration-disclaimer${variant === 'view' ? ' registration-disclaimer--modal' : ''}`}
role="document"
>
<div className="auth-header">
<ScrollText className="auth-icon accent" size={48} />
<h2>{t('disclaimer.title')}</h2>
{variant === 'view' && (
<button
type="button"
className="registration-disclaimer__close"
onClick={onDismiss}
aria-label={t('disclaimer.close')}
>
<X size={18} />
</button>
)}
</div>
<p className="registration-disclaimer__intro">{t('disclaimer.intro')}</p>
<div className="registration-disclaimer__sections">
{sections.map((section) => (
<section key={section.title} className="registration-disclaimer__section">
<h3>{section.title}</h3>
<p>{section.body}</p>
</section>
))}
</div>
<p className="registration-disclaimer__copyright">{t('disclaimer.copyright')}</p>
<div className="auth-actions">
<button type="button" className="btn primary" onClick={onDismiss}>
{variant === 'accept' ? t('disclaimer.accept') : t('disclaimer.close')}
</button>
</div>
</div>
)
}