import { useEffect, useRef, useState } from 'react' import { useCamera } from '../media/useCamera' import { useRecorder } from '../media/useRecorder' import { fileToDataUri, validateAudioFile, validateImageFile } from '../media/validate' import { useI18n } from '../i18n' import { Button } from './ui' import type { MessageKey } from '../i18n/en' export type Modality = 'face' | 'fingerprint' | 'voice' function toImagePreview(v: string): string { return v.startsWith('data:') ? v : `data:image/jpeg;base64,${v}` } interface Props { modality: Modality id: string value: string | null onChange: (dataUri: string | null) => void demoSample?: string } /** One modality's sample control. Face = camera + upload + demo; Fingerprint = * upload + demo (optical IMAGE, never a device sensor); Voice = record + upload + demo. * Media tracks are always stopped on capture / replace / unmount. */ export function SampleCapture({ modality, id, value, onChange, demoSample }: Props) { const { t } = useI18n() const cam = useCamera() const rec = useRecorder() const [error, setError] = useState(null) const [camOpen, setCamOpen] = useState(false) const fileRef = useRef(null) // Push recorded audio up to the parent when it becomes available. useEffect(() => { if (modality === 'voice' && rec.dataUri) { onChange(rec.dataUri); setError(null) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [rec.dataUri]) async function onFile(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return const err = modality === 'voice' ? validateAudioFile(file) : validateImageFile(file) if (err) { setError(err); return } try { onChange(await fileToDataUri(file)); setError(null) } catch { setError('bio.err.readFailed') } } function useDemo() { if (demoSample) { onChange(demoSample); setError(null); setCamOpen(false); cam.stop() } } function openCamera() { setCamOpen(true); setError(null); void cam.start() } function doCapture() { const shot = cam.capture() if (shot) { onChange(shot); setError(null) } setCamOpen(false); cam.stop() // stop tracks immediately after the still is taken } function clear() { onChange(null); rec.reset(); setError(null) } const label = t(`bio.modality.${modality}` as MessageKey) return (
{label} {/* Existing sample preview */} {value && modality !== 'voice' && (
{t('bio.preview',
)} {value && modality === 'voice' && (
{rec.playbackUrl &&
)} {/* Capture controls (hidden once a sample exists) */} {!value && (
{modality === 'face' && !camOpen && ( )} {modality === 'face' && camOpen && (
{cam.status === 'live' && ( <>
)} {modality === 'voice' && (
{rec.status !== 'recording' ? : } {rec.status === 'denied' &&

{t('bio.micDenied')}

} {rec.status === 'unsupported' &&

{t('bio.micUnsupported')}

}
)} {demoSample && }
)} {modality === 'fingerprint' && (

{t('bio.fingerprintNote')}

)} {error &&

{t(error)}

}
) }