import { useEffect, useRef, useState } from "react"; import type { ExternalIdentityLink, ConfirmExternalIdentityLinkRequest, ExternalIdentityLinkPreview, } from "@opengeni/sdk"; import { useOpenGeniText } from "./messages/runtime"; export type IdentityLinkClient = { previewIdentityLink( workspaceId: string, linkId: string, challenge: string, ): Promise; confirmIdentityLink( workspaceId: string, linkId: string, input: ConfirmExternalIdentityLinkRequest, ): Promise; revokeIdentityLink( workspaceId: string, linkId: string, expectedRevision: number, ): Promise; }; export type IdentityLinkConsentProps = { client: IdentityLinkClient; workspaceId: string; linkId: string; challenge: string; /** Host label is explanatory, never proof of the requesting application. */ className?: string; onComplete?: (link: ExternalIdentityLink) => void; }; /** Optional native-login consent. Ordinary Connect does not use this surface. * Permission choices only narrow the server's request; no automatic approval. */ export function IdentityLinkConsent({ client, workspaceId, linkId, challenge, className, onComplete, }: IdentityLinkConsentProps) { const t = useOpenGeniText(); const [link, setLink] = useState(null); const [preview, setPreview] = useState(null); const [selected, setSelected] = useState([]); const [busy, setBusy] = useState(false); const [error, setError] = useState(false); const [retry, setRetry] = useState(0); const generation = useRef(0); useEffect(() => { const current = ++generation.current; setLink(null); setPreview(null); setSelected([]); setBusy(true); setError(false); void client .previewIdentityLink(workspaceId, linkId, challenge) .then( (result) => { if (generation.current !== current) return; setPreview(result); setLink(result.link); setSelected(result.link.permissions); }, () => { if (generation.current === current) setError(true); }, ) .finally(() => { if (generation.current === current) setBusy(false); }); return () => { // Invalidate live async work, rather than cleaning up a captured DOM node. // eslint-disable-next-line react-hooks/exhaustive-deps generation.current++; }; }, [client, workspaceId, linkId, challenge, retry]); async function mutate(revoke: boolean) { if (!link || busy) return; const current = generation.current; setBusy(true); setError(false); try { const result = revoke ? await client.revokeIdentityLink(workspaceId, linkId, link.revision) : await client.confirmIdentityLink(workspaceId, linkId, { challenge, expectedRevision: link.revision, permissions: selected, }); if (generation.current !== current) return; setLink(result); onComplete?.(result); } catch { if (generation.current === current) setError(true); } finally { if (generation.current === current) setBusy(false); } } return (

{t("identityLinkConsent.linkYourOpengeniAccount")}

{t("identityLinkConsent.onlyContinueIfYouStarted")}

{preview && (
{t("identityLinkConsent.organization")}
{preview.organizationId}
{t("identityLinkConsent.productUser")}
{preview.externalIdentity.externalId} ({preview.externalIdentity.source})
{t("identityLinkConsent.yourOpengeniIdentity")}
{preview.nativeSubjectId}
)} {error && (
{t("identityLinkConsent.thisRequestCouldNotBe")} {!link && ( )}
)} {!link && busy && (

{t("identityLinkConsent.loadingTheAccountLinkRequest")}

)} {link?.status === "pending" && ( <>

{link.expiresAt ? t("identityLinkConsent.accessEndsLocalestring", { localeString: new Date(link.expiresAt).toLocaleString(t.locale), }) : t("identityLinkConsent.accessLastsUntilYouOr")}{" "} {t("identityLinkConsent.yourCurrentPermissionsStillApply")}

{t("identityLinkConsent.allowThesePermissions")} {link.permissions.map((permission) => ( ))}

{t("identityLinkConsent.youCanCloseThisPage")}

)} {link?.status === "active" && ( <>

{t("identityLinkConsent.accountLinkedReturnToYour")}

)} {link?.status === "revoked" && (

{t("identityLinkConsent.linkRevokedYourOpengeniAccount")}

)} {link?.status === "expired" && (

{t("identityLinkConsent.thisRequestHasExpiredStart")}

)}
); }