import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { registerUser, loginUser, completeLoginWithRecovery, setLocalPin, hasLocalPin, decryptWithLocalPin, getActiveMasterKey, getKnownUsernames, forgetUsername } from '../services/auth.js' import { KeyRound, ShieldAlert, Languages, HelpCircle, UserRound, X } from 'lucide-react' import RegistrationDisclaimer from './RegistrationDisclaimer.tsx' import BetaBadge from './BetaBadge.tsx' interface AuthOnboardingProps { onAuthenticated: () => void onOpenDemo?: () => void } export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnboardingProps) { const { t, i18n } = useTranslation() const [username, setUsername] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // Accounts that have already authenticated on this device. Used to offer a // one-click login without re-typing the username. const [knownUsers, setKnownUsers] = useState(() => getKnownUsernames()) // Registration recovery phrase flow const [recoveryPhrase, setRecoveryPhrase] = useState(null) const [copied, setCopied] = useState(false) // Login recovery phrase fallback flow const [showRecoveryFallback, setShowRecoveryFallback] = useState(false) const [recoveryInput, setRecoveryInput] = useState('') const [encryptedPayloads, setEncryptedPayloads] = useState(null) // PIN setup flow state const [showPinSetup, setShowPinSetup] = useState(false) const [pinInput, setPinInput] = useState('') const [pinSetupUsername, setPinSetupUsername] = useState('') // PIN login fallback flow state 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 setLoading(true) setError(null) try { const result = await registerUser(username.trim()) if (result.verified) { setIsNewRegistration(true) setRecoveryPhrase(result.recoveryPhrase) } } catch (err: any) { setError(err.message || 'Registration failed') } finally { setLoading(false) } } const handleLogin = async (explicitUsername?: string) => { setLoading(true) setError(null) try { // Pass the username when available so the server returns a concrete // allowCredentials list. A usernameless (discoverable) assertion fails on // some platform authenticators (e.g. Windows Hello); giving the explicit // credential id makes those work. Priority: an explicitly clicked account // > a typed name > the single remembered account > usernameless discovery. const remembered = getKnownUsernames() const target = explicitUsername || username.trim() || (remembered.length === 1 ? remembered[0] : undefined) const result = await loginUser(target) if (result.verified) { if (result.prfSuccess) { // Biometric E2E decryption succeeded onAuthenticated() } else { // Biometrics succeeded but PRF key wasn't supported/available, fall back to PIN or recovery phrase setEncryptedPayloads(result.encryptedPayloads) const resolvedUser = result.username || result.encryptedPayloads?.username || '' if (resolvedUser) { setUsername(resolvedUser) } if (resolvedUser && hasLocalPin(resolvedUser)) { setShowPinLogin(true) } else { setShowRecoveryFallback(true) } } } } catch (err: any) { setError(err.message || 'Login failed') } finally { setLoading(false) } } const handleRecoverySubmit = async (e: React.FormEvent) => { e.preventDefault() if (!recoveryInput.trim() || !encryptedPayloads) return setLoading(true) setError(null) try { const resolvedUser = username.trim() || encryptedPayloads.username const success = await completeLoginWithRecovery(resolvedUser, recoveryInput.trim(), encryptedPayloads) if (success) { // Offer PIN setup to prevent future recovery phrase entries on this device setPinSetupUsername(resolvedUser) setShowRecoveryFallback(false) setShowPinSetup(true) } else { setError(t('auth.error_incorrect_recovery')) } } catch (err: any) { setError(t('auth.error_decryption_failed')) } finally { setLoading(false) } } const handleConfirmRecovery = () => { setPinSetupUsername(username.trim() || encryptedPayloads?.username || '') // Clear the recovery phrase so the PIN-setup screen can render: the // `recoveryPhrase` branch is evaluated before `showPinSetup`, so leaving it // set would keep the user stuck on the recovery-phrase screen. setRecoveryPhrase(null) setShowPinSetup(true) } const handlePinSetupSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!pinInput.trim() || pinInput.length < 4) { setError(t('auth.pin_length_error', 'PIN must be at least 4 digits')) return } setLoading(true) setError(null) try { const activeKey = getActiveMasterKey() if (activeKey) { await setLocalPin(pinInput.trim(), pinSetupUsername, activeKey) finishAuth() } else { setError('No active master key found') } } catch (err: any) { setError(err.message || 'Failed to save PIN') } finally { setLoading(false) } } const handlePinLoginSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!pinLoginInput.trim()) return setLoading(true) setError(null) try { const resolvedUser = username.trim() || encryptedPayloads?.username const key = await decryptWithLocalPin(pinLoginInput.trim(), resolvedUser) if (key) { onAuthenticated() } else { setError(t('auth.error_incorrect_pin')) } } catch (err: any) { setError(t('auth.error_incorrect_pin')) } finally { setLoading(false) } } const handleForgetUser = (name: string) => { forgetUsername(name) setKnownUsers(getKnownUsernames()) } const toggleLanguage = () => { const nextLang = i18n.language.startsWith('de') ? 'en' : 'de' i18n.changeLanguage(nextLang) } const copyToClipboard = () => { if (recoveryPhrase) { navigator.clipboard.writeText(recoveryPhrase) setCopied(true) setTimeout(() => setCopied(false), 2000) } } // Render 0: Registration disclaimer (new accounts only, before app onboarding) if (showDisclaimer) { return } // Render 1: Display new registration recovery phrase if (recoveryPhrase) { return (

{t('auth.recovery_title')}

{t('auth.recovery_warning')}

{recoveryPhrase.split(" ").map((word, idx) => (
{idx + 1} {word}
))}
) } // Render 4: PIN setup screen if (showPinSetup) { return (

{t('auth.setup_pin_title')}

{t('auth.setup_pin_warning')}

setPinInput(e.target.value.replace(/\D/g, ''))} disabled={loading} required style={{ width: '100%', padding: '12px', boxSizing: 'border-box' }} />
{error &&
{error}
}
) } // Render 5: PIN login screen if (showPinLogin) { return (

{t('auth.enter_pin_title')}

{t('auth.enter_pin_warning')}

setPinLoginInput(e.target.value.replace(/\D/g, ''))} disabled={loading} required style={{ width: '100%', padding: '12px', boxSizing: 'border-box' }} />
{error &&
{error}
}
) } // Render 2: Ask for recovery phrase fallback if biometric PRF fails if (showRecoveryFallback) { return (

{t('auth.enter_recovery')}

{t('auth.recovery_fallback_warning')}