Files
probo/apps/trust/src/pages/NDAPage.tsx
Bryan Frimin c6e198854e Remove dead code
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-02-23 09:38:31 +01:00

318 lines
9.2 KiB
TypeScript

import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Button, Card, Field, IconCircleX, Logo, Spinner } from "@probo/ui";
import { startTransition, useEffect, useRef } from "react";
import {
type PreloadedQuery,
useMutation,
usePreloadedQuery,
useRefetchableFragment,
} from "react-relay";
import { Navigate, useNavigate } from "react-router";
import { graphql } from "relay-runtime";
import { useWindowSize } from "usehooks-ts";
import { z } from "zod";
import { PDFPreview } from "#/components/PDFPreview";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import type { NDAPageAcceptElectronicSignatureMutation } from "./__generated__/NDAPageAcceptElectronicSignatureMutation.graphql";
import type { NDAPageFragment$key } from "./__generated__/NDAPageFragment.graphql";
import type { NDAPageQuery as NDAPageQueryType } from "./__generated__/NDAPageQuery.graphql";
import type { NDAPageRecordSigningEventMutation } from "./__generated__/NDAPageRecordSigningEventMutation.graphql";
import type { NDAPageRefetchQuery } from "./__generated__/NDAPageRefetchQuery.graphql";
export const ndaPageQuery = graphql`
query NDAPageQuery {
viewer {
fullName
}
currentTrustCenter @required(action: THROW) {
organization {
name
}
nonDisclosureAgreement {
fileName
fileUrl
viewerSignature {
status
}
}
...NDAPageFragment
}
}
`;
const ndaPageFragment = graphql`
fragment NDAPageFragment on TrustCenter
@refetchable(queryName: "NDAPageRefetchQuery") {
nonDisclosureAgreement @required(action: THROW) {
viewerSignature @required(action: THROW) {
id
status
consentText
lastError
}
}
}
`;
const acceptElectronicSignatureMutation = graphql`
mutation NDAPageAcceptElectronicSignatureMutation(
$input: AcceptElectronicSignatureInput!
) {
acceptElectronicSignature(input: $input) {
signature {
id
status
}
}
}
`;
const recordSigningEventMutation = graphql`
mutation NDAPageRecordSigningEventMutation(
$input: RecordSigningEventInput!
) {
recordSigningEvent(input: $input) {
success
}
}
`;
const schema = z.object({
fullName: z.string().min(1),
});
export function NDAPage(props: {
queryRef: PreloadedQuery<NDAPageQueryType>;
}) {
const { __ } = useTranslate();
const navigate = useNavigate();
const documentViewedRef = useRef(false);
const queryData = usePreloadedQuery(ndaPageQuery, props.queryRef);
const trustCenter = queryData.currentTrustCenter;
const viewer = queryData.viewer;
const [data, refetch] = useRefetchableFragment<NDAPageRefetchQuery, NDAPageFragment$key>(
ndaPageFragment,
trustCenter,
);
const ndaSignature = data.nonDisclosureAgreement.viewerSignature;
const { width } = useWindowSize();
const isMobile = width < 1100;
const isDesktop = !isMobile;
const {
handleSubmit: handleSubmitWrapper,
register,
formState,
} = useFormWithSchema(schema, {
defaultValues: {
fullName: viewer?.fullName,
},
});
const [acceptSignature, isAccepting] = useMutation<NDAPageAcceptElectronicSignatureMutation>(
acceptElectronicSignatureMutation,
);
const [recordSigningEvent] = useMutation<NDAPageRecordSigningEventMutation>(
recordSigningEventMutation,
);
const isProcessing
= ndaSignature.status === "ACCEPTED"
|| ndaSignature.status === "PROCESSING";
const isFailed = ndaSignature.status === "FAILED";
const isCompleted = ndaSignature.status === "COMPLETED";
useEffect(() => {
if (isCompleted) {
void navigate("/overview", { replace: true });
}
}, [isCompleted, navigate]);
useEffect(() => {
if (!isProcessing) return;
const poll = () => startTransition(() => {
refetch({}, { fetchPolicy: "network-only" });
});
const interval = setInterval(poll, 1500);
return () => clearInterval(interval);
}, [isProcessing, refetch]);
useEffect(() => {
if (
ndaSignature
&& ndaSignature.status === "PENDING"
&& !documentViewedRef.current
) {
documentViewedRef.current = true;
recordSigningEvent({
variables: {
input: {
signatureId: ndaSignature.id,
eventType: "DOCUMENT_VIEWED",
},
},
});
}
}, [ndaSignature, recordSigningEvent]);
const handleSubmit = handleSubmitWrapper(({ fullName }) => {
if (!ndaSignature) return;
if (ndaSignature.status === "PENDING") {
recordSigningEvent({
variables: {
input: {
signatureId: ndaSignature.id,
eventType: "FULL_NAME_TYPED",
},
},
});
}
recordSigningEvent({
variables: {
input: {
signatureId: ndaSignature.id,
eventType: "CONSENT_GIVEN",
},
},
onCompleted: () => {
acceptSignature({
variables: {
input: {
signatureId: ndaSignature.id,
fullName,
},
},
});
},
});
});
const nda = trustCenter.nonDisclosureAgreement;
if (
!nda
|| !nda.viewerSignature
|| nda.viewerSignature.status === "COMPLETED"
) {
return <Navigate to="/overview" replace />;
}
const consentText = ndaSignature?.consentText
? ndaSignature.consentText
: __(
"By clicking Review & Sign, you agree to the terms of this NDA. If you have questions about the NDA, please contact security@probo.com.",
);
return (
<div className="bg-level-2 flex flex-col min-h-screen lg:h-screen">
<header className="flex items-center h-12 justify-between border-b border-border-solid px-4 flex-none">
<Logo />
</header>
<div className="grid lg:grid-cols-2 min-h-0 flex-1">
<div className="flex flex-col items-center overflow-y-auto">
<div className="max-w-[440px] w-full mx-auto px-4 py-12 lg:py-20 flex-1">
<h1 className="text-2xl font-semibold">
{__("Non-Disclosure Agreement")}
</h1>
<p className="text-txt-secondary mt-2">
{sprintf(
__(
"%s requires you to sign an NDA before accessing compliance documents.",
),
trustCenter.organization.name,
)}
</p>
{isMobile && nda?.fileUrl && (
<Card className="flex justify-between py-3 px-4 text-sm items-center mt-6">
{nda?.fileName}
<Button variant="secondary" asChild>
<a target="_blank" rel="noopener noreferrer" href={nda.fileUrl}>
{__("View document")}
</a>
</Button>
</Card>
)}
<form
onSubmit={e => void handleSubmit(e)}
className="mt-8"
>
<Field
required
label={__("Full name")}
placeholder="John Doe"
{...register("fullName")}
type="text"
disabled={isProcessing}
/>
<p className="text-xs text-txt-tertiary mt-6">
{consentText}
</p>
{isFailed && (
<div className="flex items-start gap-2 mt-4 rounded-md bg-red-50 border border-red-200 p-3 text-sm text-red-800">
<IconCircleX size={16} className="mt-0.5 shrink-0" />
<div>
<p className="font-medium">
{__("Signature processing failed")}
</p>
<p className="mt-0.5 text-red-600">
{ndaSignature?.lastError
?? __("We encountered an issue processing your signature. Please try again.")}
</p>
</div>
</div>
)}
{isProcessing
? (
<Button
type="button"
className="h-10 w-full mt-4"
disabled
icon={Spinner}
>
{__("Sealing your signature...")}
</Button>
)
: (
<Button
type="submit"
className="h-10 w-full mt-4"
disabled={formState.isSubmitting || !formState.isValid}
icon={isAccepting ? Spinner : undefined}
>
{isFailed
? __("Try again")
: __("Accept")}
</Button>
)}
</form>
</div>
<a
href="https://www.getprobo.com/"
className="flex gap-1 text-sm font-medium text-txt-tertiary items-center py-6"
>
Powered by
{" "}
<Logo withPicto className="h-6" />
</a>
</div>
{isDesktop && (
<div className="bg-subtle h-full border-l border-border-solid min-h-0">
{nda?.fileUrl && <PDFPreview src={nda.fileUrl} name={nda.fileName ?? ""} />}
</div>
)}
</div>
</div>
);
}