3 blocker phases
This commit is contained in:
@@ -3,12 +3,16 @@ 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;
|
||||
/** Refetches the parent case — there is no per-document re-sign route, so a reopen re-fetches the whole
|
||||
* case to get a freshly-signed `document.url`. */
|
||||
onReload: () => void;
|
||||
/** True while the parent case is (re)loading — drives the skeleton. */
|
||||
reloading: boolean;
|
||||
}
|
||||
|
||||
/** Human-readable file size. */
|
||||
@@ -19,16 +23,15 @@ function formatSize(bytes: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* A verification-document viewer. `document.url` is the **short-lived signed GET URL the case detail
|
||||
* already carries** — there is no separate per-document re-sign route (b6 gap), so "reload" refetches the
|
||||
* whole case (`onReload`) rather than calling a document-specific endpoint. Handles the loading (parent
|
||||
* case fetching) and loaded states (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<DocumentViewerProps> = ({ document }) => {
|
||||
const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document, onReload, reloading }) => {
|
||||
const t = useTranslations('admin');
|
||||
const signed = useVerificationDocumentUrl(document.id);
|
||||
const isImage = document.contentType.startsWith('image/');
|
||||
|
||||
return (
|
||||
@@ -45,36 +48,27 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
|
||||
variant="text"
|
||||
color="primary"
|
||||
startIcon="refresh"
|
||||
onClick={() => signed.refetch()}
|
||||
disabled={signed.isFetching}
|
||||
onClick={onReload}
|
||||
disabled={reloading}
|
||||
sx={{ minWidth: 0 }}
|
||||
>
|
||||
{t('doc_reload')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
|
||||
{signed.isLoading || signed.isFetching ? (
|
||||
{reloading ? (
|
||||
<Skeleton variant="rounded" height={isImage ? 180 : 44} />
|
||||
) : signed.isError ? (
|
||||
<Stack sx={{ gap: 1, alignItems: 'flex-start' }}>
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-error)' }}>
|
||||
{t('doc_error')}
|
||||
</Typography>
|
||||
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={() => signed.refetch()}>
|
||||
{t('doc_reload')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
) : signed.data?.url ? (
|
||||
) : document.url ? (
|
||||
isImage ? (
|
||||
// Signed, short-lived, cross-host URL (not a static asset) — a plain <img> via Box, not next/image.
|
||||
<Box
|
||||
component="img"
|
||||
src={signed.data.url}
|
||||
src={document.url}
|
||||
alt={document.originalFileName ?? String(document.id)}
|
||||
sx={{ maxWidth: '100%', maxHeight: 320, borderRadius: 'var(--bal-radius-sm)', display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<AppButton variant="outlined" color="primary" endIcon="external" href={signed.data.url} openInNewTab>
|
||||
<AppButton variant="outlined" color="primary" endIcon="external" href={document.url} openInNewTab>
|
||||
{t('doc_open_new')}
|
||||
</AppButton>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user