mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
228 lines
6.7 KiB
TypeScript
228 lines
6.7 KiB
TypeScript
import { useState, type ReactNode } from "react";
|
|
import {
|
|
Alert,
|
|
Avatar,
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
Group,
|
|
Stack,
|
|
Text,
|
|
} from "@mantine/core";
|
|
import {
|
|
BadgeCheck,
|
|
Clock,
|
|
Mail,
|
|
MapPin,
|
|
Phone,
|
|
ShieldCheck,
|
|
XCircle,
|
|
} from "lucide-react";
|
|
|
|
import {
|
|
stashPendingVerification,
|
|
verifaydaService,
|
|
type IdentitySubject,
|
|
type IdentityVerificationState,
|
|
} from "@/services/verifayda.service";
|
|
import { isBypassEnv } from "@/utils/dev-bypass";
|
|
|
|
/** Sentinel code that skips the real eSignet exchange in dev/staging (see api's DEV_BYPASS_FAYDA_CODE). */
|
|
const DEV_BYPASS_FAYDA_CODE = "DEV_BYPASS";
|
|
|
|
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;
|
|
disabled?: boolean;
|
|
/**
|
|
* True when a fresh verification for this person is already staged in a
|
|
* pending change request. On an active company a re-verification never
|
|
* touches the live record — it's staged for review — so `state` alone
|
|
* would keep showing the OLD verified data with no sign anything happened.
|
|
*/
|
|
pendingReview?: boolean;
|
|
}
|
|
|
|
function getInitials(name: string | null): string {
|
|
if (!name) return "?";
|
|
const parts = name.trim().split(/\s+/);
|
|
const first = parts[0]?.[0] ?? "";
|
|
const last = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? "") : "";
|
|
return (first + last).toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Verify one of the company's people through Fayda and show what came back.
|
|
*
|
|
* The identity is proved on eSignet, which the whole tab navigates to — no
|
|
* popup, because a popup opened after the /start round-trip has lost its user
|
|
* activation and iOS Safari blocks it outright. eSignet redirects back to
|
|
* /fayda/callback, which completes the exchange and returns the user here; 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,
|
|
disabled,
|
|
pendingReview,
|
|
}: FaydaVerifyPanelProps) {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const startVerification = async () => {
|
|
setError(null);
|
|
setLoading(true);
|
|
try {
|
|
// Dev/staging only: send the tab straight to /fayda/callback with the
|
|
// sentinel code instead of round-tripping through eSignet — that page's
|
|
// existing completeIdentity()/navigate-back logic runs unchanged.
|
|
if (isBypassEnv()) {
|
|
stashPendingVerification({
|
|
subject,
|
|
returnTo:
|
|
window.location.pathname +
|
|
window.location.search +
|
|
window.location.hash,
|
|
});
|
|
window.location.assign(
|
|
`/fayda/callback?code=${DEV_BYPASS_FAYDA_CODE}&state=bypass`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const authorizationUrl = await verifaydaService.start();
|
|
// Record who is being verified and where to come back to before the tab
|
|
// leaves — /fayda/callback has no other way to know either.
|
|
stashPendingVerification({
|
|
subject,
|
|
returnTo:
|
|
window.location.pathname +
|
|
window.location.search +
|
|
window.location.hash,
|
|
});
|
|
window.location.assign(authorizationUrl);
|
|
} 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}
|
|
</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>
|
|
)
|
|
)}
|
|
{pendingReview && (
|
|
<Badge
|
|
size="sm"
|
|
variant="light"
|
|
color="amber"
|
|
leftSection={<Clock size={11} />}
|
|
>
|
|
Re-verification pending review
|
|
</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 && (
|
|
<Group align="flex-start" gap="sm" wrap="nowrap">
|
|
<Avatar radius="xl" size={44} color="edr-green" variant="light">
|
|
{getInitials(state.name)}
|
|
</Avatar>
|
|
<Stack gap={4} style={{ minWidth: 0, flex: 1 }}>
|
|
<Text fw={600} size="sm" c="edr-text" truncate>
|
|
{state.name}
|
|
</Text>
|
|
<Group gap="md" wrap="wrap">
|
|
<DataRow icon={<Phone size={13} />} value={state.phone} />
|
|
<DataRow icon={<Mail size={13} />} value={state.email} />
|
|
</Group>
|
|
<DataRow icon={<MapPin size={13} />} value={state.address} />
|
|
</Stack>
|
|
</Group>
|
|
)}
|
|
|
|
{error && (
|
|
<Alert mt="sm" color="red" variant="light" icon={<XCircle size={18} />}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function DataRow({ icon, value }: { icon: ReactNode; value: string | null }) {
|
|
if (!value) return null;
|
|
return (
|
|
<Group gap={6} wrap="nowrap">
|
|
<span
|
|
style={{
|
|
color: "var(--mantine-color-edr-muted-6)",
|
|
display: "flex",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{icon}
|
|
</span>
|
|
<Text size="xs" c="edr-muted" truncate>
|
|
{value}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
}
|