mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
- 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>
213 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|