'use client'; import { FunctionComponent } from 'react'; import { useTranslations } from 'next-intl'; import { Box, Skeleton, Stack, Typography } from '@mui/material'; import type { VerificationDocument } from '@/services/verification/types'; import { useVerificationDocumentUrl } from '@/services/verification'; import AppButton from '../common/AppButton'; import AppIcon from '../common/AppIcon'; export interface DocumentViewerProps { document: VerificationDocument; } /** Human-readable file size. */ function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * A verification-document viewer that fetches its **signed URL on demand** (never the embedded one — those * are short-lived) via `useVerificationDocumentUrl`. Handles the full lifecycle: **loading** the link, * **error / expired → re-request** (the URL is short-lived, so a manual re-request re-signs it), and the * loaded state (inline image preview for images, otherwise an "open in a new tab" affordance). PII → only * the signed URL is ever surfaced, never a public asset (phase §5). * @component DocumentViewer */ const DocumentViewer: FunctionComponent = ({ document }) => { const t = useTranslations('admin'); const signed = useVerificationDocumentUrl(document.id); const isImage = document.contentType.startsWith('image/'); return ( {t('doc_file_meta', { name: document.originalFileName ?? `#${document.id}`, size: formatSize(document.fileSizeBytes) })} signed.refetch()} disabled={signed.isFetching} sx={{ m: 0, minWidth: 0 }} > {t('doc_reload')} {signed.isLoading || signed.isFetching ? ( ) : signed.isError ? ( {t('doc_error')} signed.refetch()} sx={{ m: 0 }}> {t('doc_reload')} ) : signed.data?.url ? ( isImage ? ( // Signed, short-lived, cross-host URL (not a static asset) — a plain via Box, not next/image. ) : ( {t('doc_open_new')} ) ) : null} ); }; export default DocumentViewer;