Files
edr-platform/apps/edr-freight-web/portal/src/components/FaydaVerifyPanel.tsx
Nathnael f1431a8353 feat(freight-portal): fayda verify ui, owner/gm split, foreign passport
- FaydaVerifyPanel + /callback popup flow for owner and poa
- general manager is a plain typed role again, offers "same as
  verified owner" copy instead of being fayda-verified itself
- company step gates on owner verification (ethiopian) or typed
  passport number (foreign); poa step gates on poa verification
- settings tabs (company profile, general manager, poa) updated to
  match

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 06:57:27 +00:00

213 lines
6.1 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import {
Alert,
Badge,
Button,
Card,
Group,
SimpleGrid,
Stack,
Text,
} from "@mantine/core";
import { BadgeCheck, ShieldCheck, XCircle } from "lucide-react";
import {
verifaydaService,
type CompanyIdentityState,
type FaydaCallbackMessage,
type IdentitySubject,
type IdentityVerificationState,
} from "@/services/verifayda.service";
interface FaydaVerifyPanelProps {
subject: IdentitySubject;
/** Heading — "General Manager" / "Power of Attorney". */
title: string;
/** What this person's verification is currently known to be. */
state?: IdentityVerificationState;
/**
* False for a foreign company: verification is offered but nothing is gated
* on it, so the panel says so rather than nagging.
*/
required: boolean;
/** Called with the fresh company-wide state once a verification lands. */
onVerified: (next: CompanyIdentityState) => void;
disabled?: boolean;
}
function formatDate(iso: string | null): string {
if (!iso) return "";
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? "" : d.toLocaleDateString();
}
/**
* Verify one of the company's people through Fayda and show what came back.
*
* The identity is proved in an eSignet popup; that popup lands on /callback,
* which relays the code+state here by postMessage. This window then completes
* the exchange — once, in one place — and the API writes the person's name,
* phone, email and address from the verified payload. Nothing on this panel
* is typed.
*/
export default function FaydaVerifyPanel({
subject,
title,
state,
required,
onVerified,
disabled,
}: FaydaVerifyPanelProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// The listener closes over `subject`; keep it in a ref so remounting the
// panel between steps can't complete a verification against the wrong person.
const subjectRef = useRef(subject);
subjectRef.current = subject;
useEffect(() => {
const onMessage = async (event: MessageEvent<FaydaCallbackMessage>) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type !== "fayda-callback") return;
if (event.data.error) {
setLoading(false);
setError(event.data.errorDescription ?? event.data.error);
return;
}
if (!event.data.code || !event.data.state) return;
try {
const next = await verifaydaService.completeIdentity(
subjectRef.current,
event.data.code,
event.data.state,
);
setError(null);
onVerified(next);
} catch (err) {
setError(
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message ??
(err instanceof Error ? err.message : "Verification failed"),
);
} finally {
setLoading(false);
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const startVerification = async () => {
setError(null);
setLoading(true);
try {
const authorizationUrl = await verifaydaService.start();
const popup = window.open(
authorizationUrl,
"fayda-verify",
"width=480,height=760,noopener=no",
);
if (!popup) {
setLoading(false);
setError("Pop-up blocked — allow pop-ups for this site and try again.");
}
// Loading stays on until the popup posts back.
} catch (err) {
setLoading(false);
setError(
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message ??
(err instanceof Error ? err.message : "Could not start verification"),
);
}
};
const verified = state?.verified ?? false;
return (
<Card padding="md" radius="md" withBorder>
<Group justify="space-between" align="center" mb={verified ? "md" : "xs"}>
<Group gap="sm">
<ShieldCheck size={18} />
<Text fw={600} c="edr-text">
{title} identity
</Text>
{verified ? (
<Badge
size="sm"
variant="light"
color="green"
leftSection={<BadgeCheck size={11} />}
>
Fayda verified
</Badge>
) : (
required && (
<Badge size="sm" variant="light" color="amber">
Verification required
</Badge>
)
)}
</Group>
<Button
type="button"
variant="light"
size="xs"
loading={loading}
disabled={disabled}
onClick={startVerification}
>
{verified ? "Re-verify with Fayda" : "Verify with Fayda"}
</Button>
</Group>
{!verified && (
<Text c="edr-muted" size="xs">
{required
? "Verify this person with Fayda. Their name, phone and address come from the verification — there is nothing to fill in by hand."
: "Optional for a foreign company. If this person holds a Fayda ID, verifying it fills in their details."}
</Text>
)}
{verified && state && (
<SimpleGrid cols={2} spacing="xs">
<VerifiedField label="Name" value={state.name} />
<VerifiedField label="Phone" value={state.phone} />
<VerifiedField label="Email" value={state.email} />
<VerifiedField label="Address" value={state.address} />
<VerifiedField label="Verified" value={formatDate(state.verifiedAt)} />
</SimpleGrid>
)}
{error && (
<Alert mt="sm" color="red" variant="light" icon={<XCircle size={18} />}>
{error}
</Alert>
)}
</Card>
);
}
function VerifiedField({
label,
value,
}: {
label: string;
value: string | null;
}) {
if (!value) return null;
return (
<Stack gap={0}>
<Text size="xs" c="edr-muted">
{label}
</Text>
<Text size="sm" fw={500} c="edr-text">
{value}
</Text>
</Stack>
);
}