import { useEffect, useMemo, useState } from 'preact/hooks'; import ConfirmDialog from '@/components/ConfirmDialog'; import ToastHost from '@/components/ToastHost'; import { t } from '@/lib/i18n'; import type { ToastMessage } from '@/lib/types'; export interface AppConfirmState { title: string; message: string; danger?: boolean; showIcon?: boolean; confirmText?: string; cancelText?: string; hideCancel?: boolean; /** When true, dialog shows a master-password field and passes it to onConfirm. */ requireMasterPassword?: boolean; onConfirm: (masterPassword?: string) => void; onCancel?: () => void; } interface AppGlobalOverlaysProps { toasts: ToastMessage[]; onCloseToast: (id: string) => void; confirm: AppConfirmState | null; onCancelConfirm: () => void; pendingTotpOpen: boolean; pendingTotpProviderType?: number; pendingTotpAvailableProviders?: number[]; totpCode: string; rememberDevice: boolean; onTotpCodeChange: (value: string) => void; onRememberDeviceChange: (checked: boolean) => void; onConfirmTotp: () => void; onSelectTotpProvider: (providerType: number) => void; onCancelTotp: () => void; onUseRecoveryCode: () => void; totpSubmitting: boolean; disableTotpOpen: boolean; disableTotpPassword: string; onDisableTotpPasswordChange: (value: string) => void; onConfirmDisableTotp: () => void; onCancelDisableTotp: () => void; disableTotpSubmitting: boolean; } const TWO_FACTOR_PROVIDER_AUTHENTICATOR = 0; const TWO_FACTOR_PROVIDER_YUBIKEY = 3; const TWO_FACTOR_PROVIDER_WEBAUTHN = 7; const TWO_FACTOR_PROVIDER_ORDER = [ TWO_FACTOR_PROVIDER_WEBAUTHN, TWO_FACTOR_PROVIDER_YUBIKEY, TWO_FACTOR_PROVIDER_AUTHENTICATOR, ] as const; function uniqueSupportedProviders(providerTypes: number[] | undefined): number[] { const available = new Set(providerTypes || []); return TWO_FACTOR_PROVIDER_ORDER.filter((provider) => available.has(provider)); } function twoFactorProviderLabel(providerType: number): string { if (providerType === TWO_FACTOR_PROVIDER_WEBAUTHN) return t('txt_passkey'); if (providerType === TWO_FACTOR_PROVIDER_YUBIKEY) return t('txt_otp_from_yubikey'); return t('txt_authenticator_app'); } export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) { const [methodChooserOpen, setMethodChooserOpen] = useState(false); const [confirmPassword, setConfirmPassword] = useState(''); const availableProviders = useMemo( () => uniqueSupportedProviders(props.pendingTotpAvailableProviders), [props.pendingTotpAvailableProviders] ); const alternateProviders = availableProviders.filter((provider) => provider !== props.pendingTotpProviderType); const isYubiKeyOtp = props.pendingTotpProviderType === TWO_FACTOR_PROVIDER_YUBIKEY; const isWebAuthn = props.pendingTotpProviderType === TWO_FACTOR_PROVIDER_WEBAUTHN; const requireMasterPassword = !!props.confirm?.requireMasterPassword; useEffect(() => { setMethodChooserOpen(false); }, [props.pendingTotpOpen, props.pendingTotpProviderType]); useEffect(() => { setConfirmPassword(''); }, [props.confirm?.title, props.confirm?.message, requireMasterPassword]); return ( <> { if (requireMasterPassword && !confirmPassword.trim()) return; props.confirm?.onConfirm(requireMasterPassword ? confirmPassword : undefined); setConfirmPassword(''); }} onCancel={() => { setConfirmPassword(''); (props.confirm?.onCancel || props.onCancelConfirm)(); }} > {requireMasterPassword && ( )} {t('txt_two_step_verification')} {t('txt_passkey')} ) : t('txt_two_step_verification')} message={isYubiKeyOtp ? t('txt_press_yubikey_to_authenticate') : isWebAuthn ? t('txt_use_passkey_to_complete_two_step_verification') : t('txt_password_is_already_verified')} confirmText={t('txt_verify')} hideCancel closeButton showIcon={false} confirmDisabled={props.totpSubmitting} cancelDisabled={props.totpSubmitting} onConfirm={props.onConfirmTotp} onCancel={props.onCancelTotp} afterActions={(
{alternateProviders.length > 0 && (
{methodChooserOpen && (
{t('txt_select_two_step_login_method')}
{alternateProviders.map((providerType) => ( ))}
)}
)}
)} > {isWebAuthn ? (

{t('txt_touch_your_passkey_when_prompted')}

) : ( )} ); }