import { createPortal } from 'preact/compat'; import { useEffect, useMemo, useState } from 'preact/hooks'; import { Archive, Clipboard, Download, Eye, EyeOff, ExternalLink, Folder, Paperclip, Pencil, RotateCcw, Trash2, X } from 'lucide-preact'; import { useDialogLifecycle } from '@/components/ConfirmDialog'; import type { TotpCodeResult } from '@/lib/crypto'; import type { Cipher } from '@/lib/types'; import { t } from '@/lib/i18n'; import { CardBrandIcon, TOTP_RING_CIRCUMFERENCE, VaultListIcon, copyToClipboard, displayCardBrand, formatAttachmentSize, formatHistoryTime, formatTotp, isCipherDeleted, maskSecret, openUri, parseFieldType, toBooleanFieldValue, } from '@/components/vault/vault-page-helpers'; interface VaultDetailViewProps { selectedCipher: Cipher; repromptApprovedCipherId: string | null; showPassword: boolean; totpLive: TotpCodeResult | null; passkeyCreatedAt: string | null; hiddenFieldVisibleMap: Record; folderName: (id: string | null | undefined) => string; downloadingAttachmentKey: string; attachmentDownloadPercent: number | null; onOpenReprompt: () => void; onToggleShowPassword: () => void; onToggleHiddenField: (index: number) => void; onDownloadAttachment: (cipher: Cipher, attachmentId: string) => void; onStartEdit: () => void; onDelete: (cipher: Cipher) => void; onRestore: (cipher: Cipher) => void | Promise; onArchive: (cipher: Cipher) => void | Promise; onUnarchive: (cipher: Cipher) => void | Promise; } function totpProgress(live: TotpCodeResult | null): number { const period = Math.max(1, live?.period || 30); return live ? Math.max(0, Math.min(period, live.remain)) / period : 0; } function PasswordHistoryDialog(props: { open: boolean; entries: Array<{ password: string; lastUsedDate: string | null }>; onClose: () => void; }) { useDialogLifecycle(props.open, props.onClose); if (!props.open || typeof document === 'undefined') return null; return createPortal(
event.target === event.currentTarget && props.onClose()}>

{t('txt_password_history')}

{props.entries.map((entry, index) => (
{entry.password}
{formatHistoryTime(entry.lastUsedDate)}
))}
, document.body ); } export default function VaultDetailView(props: VaultDetailViewProps) { const selectedAttachments = Array.isArray(props.selectedCipher.attachments) ? props.selectedCipher.attachments : []; const [showSshPrivateKey, setShowSshPrivateKey] = useState(false); const [passwordHistoryOpen, setPasswordHistoryOpen] = useState(false); const isArchived = !!(props.selectedCipher.archivedDate || (props.selectedCipher as { archivedAt?: string | null }).archivedAt); const isDeleted = isCipherDeleted(props.selectedCipher); const passwordHistoryEntries = useMemo( () => (props.selectedCipher.passwordHistory || []) .map((entry) => ({ password: String(entry?.decPassword || entry?.password || ''), lastUsedDate: entry?.lastUsedDate ?? null, })) .filter((entry) => entry.password.trim()), [props.selectedCipher.passwordHistory] ); useEffect(() => { setShowSshPrivateKey(false); setPasswordHistoryOpen(false); }, [props.selectedCipher.id]); const formatDownloadLabel = (attachmentId: string) => { const downloadKey = `${props.selectedCipher.id}:${attachmentId}`; if (props.downloadingAttachmentKey !== downloadKey) return t('txt_download'); return props.attachmentDownloadPercent == null ? t('txt_downloading') : t('txt_downloading_percent', { percent: props.attachmentDownloadPercent }); }; return ( <> {Number(props.selectedCipher.reprompt || 0) === 1 && props.repromptApprovedCipherId !== props.selectedCipher.id && (

{t('txt_master_password_reprompt_2')}

{t('txt_this_item_requires_master_password_every_time_before_viewing_details')}
)} {(Number(props.selectedCipher.reprompt || 0) !== 1 || props.repromptApprovedCipherId === props.selectedCipher.id) && ( <>

{props.selectedCipher.decName || t('txt_no_name')}

{isArchived &&
{t('txt_archived')}
}
{props.selectedCipher.login && (

{t('txt_login_credentials')}

{t('txt_username')}
{props.selectedCipher.login.decUsername || ''}
{t('txt_password')}
{props.showPassword ? props.selectedCipher.login.decPassword || '' : maskSecret(props.selectedCipher.login.decPassword || '')}
{!!props.selectedCipher.login.decTotp && (
{t('txt_totp')}
{props.totpLive ? formatTotp(props.totpLive.code) : t('txt_text_3')}
{props.totpLive ? props.totpLive.remain : 0}
)} {!!props.passkeyCreatedAt && (
{t('txt_passkey')}
{t('txt_passkey_created_at_value', { value: formatHistoryTime(props.passkeyCreatedAt) })}
)}
)} {(props.selectedCipher.login?.uris || []).length > 0 && (

{t('txt_autofill_options')}

{(props.selectedCipher.login?.uris || []).map((uri, index) => { const value = uri.decUri || uri.uri || ''; if (!value.trim()) return null; return (
{t('txt_website')}
{value}
); })}
)} {props.selectedCipher.card && (

{t('txt_card_details')}

{t('txt_cardholder_name')}{props.selectedCipher.card.decCardholderName || ''}
{t('txt_number')}{props.selectedCipher.card.decNumber || ''}
{t('txt_brand')} {displayCardBrand(props.selectedCipher.card.decBrand)}
{t('txt_expiry')}{`${props.selectedCipher.card.decExpMonth || ''}/${props.selectedCipher.card.decExpYear || ''}`}
{t('txt_security_code')}{props.selectedCipher.card.decCode || ''}
)} {props.selectedCipher.identity && (

{t('txt_identity_details')}

{t('txt_name')}{`${props.selectedCipher.identity.decFirstName || ''} ${props.selectedCipher.identity.decLastName || ''}`.trim()}
{t('txt_username')}{props.selectedCipher.identity.decUsername || ''}
{t('txt_email')}{props.selectedCipher.identity.decEmail || ''}
{t('txt_phone')}{props.selectedCipher.identity.decPhone || ''}
{t('txt_company')}{props.selectedCipher.identity.decCompany || ''}
{t('txt_address')}{[props.selectedCipher.identity.decAddress1, props.selectedCipher.identity.decAddress2, props.selectedCipher.identity.decAddress3, props.selectedCipher.identity.decCity, props.selectedCipher.identity.decState, props.selectedCipher.identity.decPostalCode, props.selectedCipher.identity.decCountry].filter(Boolean).join(', ')}
)} {props.selectedCipher.sshKey && (

{t('txt_ssh_key')}

{t('txt_private_key')}
{showSshPrivateKey ? props.selectedCipher.sshKey.decPrivateKey || '' : maskSecret(props.selectedCipher.sshKey.decPrivateKey || '')}
{t('txt_public_key')}
{props.selectedCipher.sshKey.decPublicKey || ''}
{t('txt_fingerprint')}
{props.selectedCipher.sshKey.decFingerprint || ''}
)} {props.selectedCipher.bankAccount && (

{t('txt_bank_account_details')}

{t('txt_bank_name')}{props.selectedCipher.bankAccount.decBankName || ''}
{t('txt_name_on_account')}{props.selectedCipher.bankAccount.decNameOnAccount || ''}
{t('txt_account_type')}{props.selectedCipher.bankAccount.decAccountType || ''}
{t('txt_account_number')}{props.selectedCipher.bankAccount.decAccountNumber || ''}
{t('txt_routing_number')}{props.selectedCipher.bankAccount.decRoutingNumber || ''}
{t('txt_branch_number')}{props.selectedCipher.bankAccount.decBranchNumber || ''}
{t('txt_pin')}{props.selectedCipher.bankAccount.decPin || ''}
{t('txt_swift_code')}{props.selectedCipher.bankAccount.decSwiftCode || ''}
{t('txt_iban')}{props.selectedCipher.bankAccount.decIban || ''}
{t('txt_bank_contact_phone')}{props.selectedCipher.bankAccount.decBankContactPhone || ''}
)} {props.selectedCipher.driversLicense && (

{t('txt_drivers_license_details')}

{t('txt_name')}{[props.selectedCipher.driversLicense.decFirstName, props.selectedCipher.driversLicense.decMiddleName, props.selectedCipher.driversLicense.decLastName].filter(Boolean).join(' ')}
{t('txt_date_of_birth')}{props.selectedCipher.driversLicense.decDateOfBirth || ''}
{t('txt_license_number')}{props.selectedCipher.driversLicense.decLicenseNumber || ''}
{t('txt_issuing_country')}{props.selectedCipher.driversLicense.decIssuingCountry || ''}
{t('txt_issuing_state')}{props.selectedCipher.driversLicense.decIssuingState || ''}
{t('txt_issue_date')}{props.selectedCipher.driversLicense.decIssueDate || ''}
{t('txt_expiration_date')}{props.selectedCipher.driversLicense.decExpirationDate || ''}
{t('txt_issuing_authority')}{props.selectedCipher.driversLicense.decIssuingAuthority || ''}
{t('txt_license_class')}{props.selectedCipher.driversLicense.decLicenseClass || ''}
)} {props.selectedCipher.passport && (

{t('txt_passport_details')}

{t('txt_name')}{[props.selectedCipher.passport.decGivenName, props.selectedCipher.passport.decSurname].filter(Boolean).join(' ')}
{t('txt_date_of_birth')}{props.selectedCipher.passport.decDateOfBirth || ''}
{t('txt_sex')}{props.selectedCipher.passport.decSex || ''}
{t('txt_birth_place')}{props.selectedCipher.passport.decBirthPlace || ''}
{t('txt_nationality')}{props.selectedCipher.passport.decNationality || ''}
{t('txt_issuing_country')}{props.selectedCipher.passport.decIssuingCountry || ''}
{t('txt_passport_number')}{props.selectedCipher.passport.decPassportNumber || ''}
{t('txt_passport_type')}{props.selectedCipher.passport.decPassportType || ''}
{t('txt_national_id_number')}{props.selectedCipher.passport.decNationalIdentificationNumber || ''}
{t('txt_issuing_authority')}{props.selectedCipher.passport.decIssuingAuthority || ''}
{t('txt_issue_date')}{props.selectedCipher.passport.decIssueDate || ''}
{t('txt_expiration_date')}{props.selectedCipher.passport.decExpirationDate || ''}
)} {!!(props.selectedCipher.decNotes || '').trim() && (

{t('txt_notes')}

{props.selectedCipher.decNotes || ''}
)} {(props.selectedCipher.fields || []).some((x) => parseFieldType(x.type) !== 3) && (

{t('txt_custom_fields')}

{(props.selectedCipher.fields || []) .filter((x) => parseFieldType(x.type) !== 3) .map((field, index) => { const fieldType = parseFieldType(field.type); const fieldName = field.decName || t('txt_field'); const rawValue = field.decValue || ''; const isHiddenVisible = !!props.hiddenFieldVisibleMap[index]; if (fieldType === 2) { const checked = toBooleanFieldValue(rawValue); return (
{fieldName}
); } return (
{fieldName}
{fieldType === 1 && !isHiddenVisible ? maskSecret(rawValue) : rawValue}
{fieldType === 1 && ( )}
); })}
)} {selectedAttachments.some((attachment) => String(attachment?.id || '').trim()) && (

{t('txt_attachments')}

{selectedAttachments.map((attachment) => { const attachmentId = String(attachment?.id || '').trim(); if (!attachmentId) return null; const fileName = String(attachment.decFileName || attachment.fileName || attachmentId).trim() || attachmentId; return (
{fileName} {formatAttachmentSize(attachment)}
); })}
)} {(props.selectedCipher.creationDate || props.selectedCipher.revisionDate) && (

{t('txt_item_history')}

{t('txt_last_edited_value', { value: formatHistoryTime(props.selectedCipher.revisionDate) })}
{t('txt_created_value', { value: formatHistoryTime(props.selectedCipher.creationDate) })}
{!!props.selectedCipher.login?.passwordRevisionDate && (
{t('txt_password_updated_value', { value: formatHistoryTime(props.selectedCipher.login.passwordRevisionDate) })}
)} {passwordHistoryEntries.length > 0 && ( )}
)}
{isDeleted ? ( ) : ( <> {isArchived ? ( ) : ( )} )}
)} setPasswordHistoryOpen(false)} /> ); }