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:
@@ -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)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user