Remove unused trust center code

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-10-01 09:37:22 +02:00
parent dfa84a9b0d
commit 60d1238534
9 changed files with 0 additions and 1399 deletions

View File

@@ -1,415 +0,0 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { GraphQLError } from "graphql";
import { buildEndpoint } from "/providers/RelayProviders";
import { type CountryCode } from "@probo/helpers";
export interface TrustCenterDocument {
id: string;
title: string;
documentType: string;
}
export interface TrustCenterAudit {
id: string;
framework: {
name: string;
};
report: {
id: string;
filename: string;
} | null;
}
export interface TrustCenterVendor {
id: string;
name: string;
category: string;
privacyPolicyUrl?: string | null;
websiteUrl?: string | null;
countries: CountryCode[];
}
interface TrustCenterQueryData {
trustCenterBySlug: {
id: string;
active: boolean;
slug: string;
isUserAuthenticated: boolean;
hasAcceptedNonDisclosureAgreement: boolean;
ndaFileName: string | null;
ndaFileUrl: string | null;
organization: {
id: string;
name: string;
logoUrl: string | null;
};
documents: {
edges: Array<{
node: TrustCenterDocument;
}>;
};
audits: {
edges: Array<{
node: TrustCenterAudit;
}>;
};
vendors: {
edges: Array<{
node: TrustCenterVendor;
}>;
};
} | null;
}
interface GraphQLResponse<T = unknown> {
data?: T;
errors?: GraphQLError[];
}
interface ExportDocumentPDFData {
exportDocumentPDF: {
data: string;
};
}
interface ExportReportPDFData {
exportReportPDF: {
data: string;
};
}
interface CreateTrustCenterAccessData {
createTrustCenterAccess: {
trustCenterAccess: {
id: string;
email: string;
name: string;
};
};
}
interface TrustCenterQueryVariables {
slug: string;
}
interface ExportDocumentPDFVariables {
input: {
documentId: string;
};
}
interface CreateTrustCenterAccessVariables {
input: {
trustCenterId: string;
email: string;
name: string;
};
}
interface AcceptNonDisclosureAgreementData {
acceptNonDisclosureAgreement: {
success: boolean;
};
}
interface AcceptNonDisclosureAgreementVariables {
input: {
trustCenterId: string;
};
}
interface ExportReportPDFVariables {
input: {
reportId: string;
};
}
type GraphQLVariables = TrustCenterQueryVariables | ExportDocumentPDFVariables | ExportReportPDFVariables | CreateTrustCenterAccessVariables | AcceptNonDisclosureAgreementVariables | Record<string, never>;
async function trustCenterGraphQLRequest<T = unknown>(
operationName: string,
query: string,
variables: GraphQLVariables = {}
): Promise<GraphQLResponse<T>> {
const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), {
method: "POST",
credentials: "include",
headers: {
Accept: "application/graphql-response+json; charset=utf-8, application/json; charset=utf-8",
"Content-Type": "application/json",
},
body: JSON.stringify({
operationName,
query,
variables,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
}
function isCriticalError(error: GraphQLError): boolean {
const message = error.message?.toLowerCase() || '';
if (
message.includes('access denied') ||
message.includes('authentication required') ||
message.includes('user has not accepted nda') ||
message.includes('no nda file found')
) {
return false;
}
return true;
}
const TRUST_CENTER_QUERY = `
query PublicTrustCenterPageQuery($slug: String!) {
trustCenterBySlug(slug: $slug) {
id
active
slug
isUserAuthenticated
hasAcceptedNonDisclosureAgreement
ndaFileName
ndaFileUrl
organization {
id
name
logoUrl
description
websiteUrl
email
headquarterAddress
}
documents(first: 100) {
edges {
node {
id
title
documentType
}
}
}
audits(first: 100) {
edges {
node {
id
framework {
name
}
report {
id
filename
}
}
}
}
vendors(first: 100) {
edges {
node {
id
name
category
websiteUrl
privacyPolicyUrl
countries
}
}
}
references(first: 100) {
edges {
node {
id
name
description
websiteUrl
logoUrl
}
}
}
}
}
`;
const EXPORT_DOCUMENT_PDF_MUTATION = `
mutation PublicTrustCenterDocumentsExportPDFMutation(
$input: ExportDocumentPDFInput!
) {
exportDocumentPDF(input: $input) {
data
}
}
`;
const EXPORT_REPORT_PDF_MUTATION = `
mutation PublicTrustCenterAuditsExportReportPDFMutation(
$input: ExportReportPDFInput!
) {
exportReportPDF(input: $input) {
data
}
}
`;
const CREATE_TRUST_CENTER_ACCESS_MUTATION = `
mutation PublicTrustCenterAccessRequestDialogMutation(
$input: CreateTrustCenterAccessInput!
) {
createTrustCenterAccess(input: $input) {
trustCenterAccess {
id
email
name
}
}
}
`;
const ACCEPT_NDA_MUTATION = `
mutation AcceptNonDisclosureAgreementMutation(
$input: AcceptNonDisclosureAgreementInput!
) {
acceptNonDisclosureAgreement(input: $input) {
success
}
}
`;
export function useTrustCenterQuery(slug: string) {
return useQuery<TrustCenterQueryData>({
queryKey: ["trust-center", slug],
queryFn: async () => {
const result = await trustCenterGraphQLRequest<TrustCenterQueryData>(
"PublicTrustCenterPageQuery",
TRUST_CENTER_QUERY,
{ slug }
);
if (result.errors && result.errors.length > 0) {
const criticalErrors = result.errors.filter(isCriticalError);
if (criticalErrors.length > 0) {
throw new Error(
`GraphQL error: ${criticalErrors.map((e) => e.message).join(", ")}`
);
}
}
if (!result.data) {
throw new Error("No data returned from GraphQL query");
}
return result.data;
},
enabled: !!slug,
staleTime: 5 * 60 * 1000, // 5 minutes
retry: (failureCount, error) => {
if (error.message.includes("UNAUTHENTICATED") || error.message.includes("401")) {
return false;
}
return failureCount < 3;
},
});
}
export function useExportDocumentPDF() {
return useMutation<ExportDocumentPDFData, Error, string>({
mutationFn: async (documentId: string) => {
const result = await trustCenterGraphQLRequest<ExportDocumentPDFData>(
"PublicTrustCenterDocumentsExportPDFMutation",
EXPORT_DOCUMENT_PDF_MUTATION,
{ input: { documentId } }
);
if (result.errors && result.errors.length > 0) {
throw new Error(
`GraphQL error: ${result.errors.map((e) => e.message).join(", ")}`
);
}
if (!result.data) {
throw new Error("No data returned from mutation");
}
return result.data;
},
});
}
export function useExportReportPDF() {
return useMutation<ExportReportPDFData, Error, string>({
mutationFn: async (reportId: string) => {
const result = await trustCenterGraphQLRequest<ExportReportPDFData>(
"PublicTrustCenterAuditsExportReportPDFMutation",
EXPORT_REPORT_PDF_MUTATION,
{ input: { reportId } }
);
if (result.errors && result.errors.length > 0) {
throw new Error(
`GraphQL error: ${result.errors.map((e) => e.message).join(", ")}`
);
}
if (!result.data) {
throw new Error("No data returned from mutation");
}
return result.data;
},
});
}
export function useCreateTrustCenterAccess() {
return useMutation<CreateTrustCenterAccessData, Error, { trustCenterId: string; email: string; name: string }>({
mutationFn: async (input: { trustCenterId: string; email: string; name: string }) => {
const result = await trustCenterGraphQLRequest<CreateTrustCenterAccessData>(
"PublicTrustCenterAccessRequestDialogMutation",
CREATE_TRUST_CENTER_ACCESS_MUTATION,
{ input }
);
if (result.errors && result.errors.length > 0) {
throw new Error(
`GraphQL error: ${result.errors.map((e) => e.message).join(", ")}`
);
}
if (!result.data) {
throw new Error("No data returned from mutation");
}
return result.data;
},
});
}
export function useAcceptNonDisclosureAgreement() {
return useMutation<AcceptNonDisclosureAgreementData, Error, { trustCenterId: string }>({
mutationFn: async (input: { trustCenterId: string }) => {
const result = await trustCenterGraphQLRequest<AcceptNonDisclosureAgreementData>(
"AcceptNonDisclosureAgreementMutation",
ACCEPT_NDA_MUTATION,
{ input }
);
if (result.errors && result.errors.length > 0) {
throw new Error(
`GraphQL error: ${result.errors.map((e) => e.message).join(", ")}`
);
}
if (!result.data) {
throw new Error("No data returned from mutation");
}
return result.data;
},
});
}

View File

@@ -1,142 +0,0 @@
import { useTranslate } from "@probo/i18n";
import { useParams, useNavigate, useSearchParams } from "react-router";
import { useState, useEffect } from "react";
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
import { PageError } from "/components/PageError";
import { buildEndpoint } from "/providers/RelayProviders";
import { IconClock, IconWarning } from "@probo/ui";
function TokenErrorPage({ error }: { error: string }) {
const { __ } = useTranslate();
const isExpiredToken = error.toLowerCase().includes('expired');
return (
<div className="min-h-screen bg-level-0 flex items-center justify-center p-4">
<div className="max-w-md w-full text-center space-y-6">
<div className="space-y-4">
{isExpiredToken ? (
<div className="space-y-3">
<div className="inline-flex items-center justify-center w-16 h-16 bg-amber-100 rounded-full">
<IconClock size={32} className="text-amber-600" />
</div>
<h1 className="text-2xl font-semibold text-txt-primary">
{__("Access Link Expired")}
</h1>
<p className="text-txt-secondary">
{__("This access link has expired. Trust center access links are valid for 7 days for security reasons.")}
</p>
</div>
) : (
<div className="space-y-3">
<div className="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full">
<IconWarning size={32} className="text-red-600" />
</div>
<h1 className="text-2xl font-semibold text-txt-primary">
{__("Invalid Access Link")}
</h1>
<p className="text-txt-secondary">
{__("This access link is not valid. It may have been revoked or the link might be incorrect.")}
</p>
</div>
)}
</div>
<div className="bg-level-1 border border-border-low rounded-lg p-4 space-y-3">
<h3 className="font-medium text-txt-primary">{__("What can you do?")}</h3>
<ul className="text-sm text-txt-secondary space-y-2 text-left">
<li className="flex items-start gap-2">
<span className="text-primary-600 mt-1">•</span>
<span>{__("Contact the person who sent you this link to request a new access invitation")}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary-600 mt-1">•</span>
<span>{__("Check if you received a newer email with an updated access link")}</span>
</li>
<li className="flex items-start gap-2">
<span className="text-primary-600 mt-1">•</span>
<span>{__("Verify that you copied the entire link correctly from the email")}</span>
</li>
</ul>
</div>
</div>
</div>
);
}
export default function TrustCenterAccessPage() {
const { __ } = useTranslate();
const { slug } = useParams<{ slug: string }>();
const [searchParams] = useSearchParams();
const token = searchParams.get('token');
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!slug) {
setError(__("Invalid trust center"));
setLoading(false);
return;
}
if (!token) {
setError(__("Invalid or missing access token"));
setLoading(false);
return;
}
fetch(buildEndpoint('/api/trust/v1/auth/authenticate'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ token }),
})
.then(async response => {
if (!response.ok) {
try {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
} catch {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
return response.json();
})
.then(data => {
if (data.success) {
navigate(`/trust/${slug}`);
} else {
setError(data.message || __("Authentication failed"));
setLoading(false);
}
})
.catch((error) => {
setError(error.message || __("Authentication failed"));
setLoading(false);
});
}, [slug, token, __, navigate]);
if (loading) {
return <PageSkeleton />;
}
if (error) {
const isTokenError = error.toLowerCase().includes('token') ||
error.toLowerCase().includes('expired') ||
error.toLowerCase().includes('invalid') ||
error.toLowerCase().includes('401') ||
error.toLowerCase().includes('unauthorized');
if (isTokenError) {
return <TokenErrorPage error={error} />;
}
return <PageError error={error} />;
}
return <div>{__("Redirecting to trust center...")}</div>;
}

View File

@@ -111,18 +111,6 @@ const routes = [
},
],
},
{
path: "/trust/:slug",
ErrorBoundary: ErrorBoundary,
fallback: PageSkeleton,
Component: lazy(() => import("./trust/pages/PublicTrustCenterPage")),
},
{
path: "/trust/:slug/access",
ErrorBoundary: ErrorBoundary,
fallback: PageSkeleton,
Component: lazy(() => import("./pages/TrustCenterAccessPage")),
},
{
path: "/organizations/:organizationId",
Component: MainLayout,

View File

@@ -1,187 +0,0 @@
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
Button,
Checkbox,
IconLock,
IconArrowDown,
useToast,
useDialogRef
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useAcceptNonDisclosureAgreement } from "/hooks/useTrustCenterQueries";
import { buildEndpoint } from "/providers/RelayProviders";
import { sprintf } from "@probo/helpers";
type Props = {
trustCenterId: string;
organizationName: string;
ndaFileName?: string | null;
ndaFileUrl?: string | null;
};
export function NDAAcceptanceDialog({ trustCenterId, organizationName, ndaFileName, ndaFileUrl }: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const [isChecked, setIsChecked] = useState(false);
const dialogRef = useDialogRef();
const acceptNdaMutation = useAcceptNonDisclosureAgreement();
useEffect(() => {
dialogRef.current?.open();
}, []);
const handleLogout = async () => {
try {
const response = await fetch(buildEndpoint('/api/trust/v1/auth/logout'), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
if (!response.ok) {
throw new Error("Logout failed");
}
window.location.reload();
} catch (error) {
toast({
title: __("Error"),
description: __("Logout failed"),
variant: "error",
});
}
};
const handleAccept = () => {
if (!isChecked) {
toast({
title: __("Agreement Required"),
description: __("Please check the box to confirm your agreement"),
variant: "error",
});
return;
}
acceptNdaMutation.mutate(
{ trustCenterId },
{
onSuccess: () => {
window.location.reload();
},
onError: () => {
toast({
title: __("Error"),
description: __("Failed to accept the Non-Disclosure Agreement"),
variant: "error",
});
},
}
);
};
const handleCancel = () => {
handleLogout();
};
return (
<Dialog
ref={dialogRef}
closable={false}
onClose={handleCancel}
>
<DialogContent>
<div className="space-y-3 p-4">
<div className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-amber-50 border border-amber-200 mb-3">
<IconLock className="h-6 w-6 text-amber-600" />
</div>
<h2 className="text-lg font-semibold text-txt-primary mb-2">
{__("Non-Disclosure Agreement")}
</h2>
<p className="text-sm text-txt-secondary">
{sprintf(__("To access %s's trust center, you must accept the Non-Disclosure Agreement."), organizationName)}
</p>
</div>
<div className="bg-level-1 p-3 rounded-lg border border-border-subtle">
{ndaFileName && ndaFileUrl ? (
<div className="text-center">
<p className="text-sm font-medium text-txt-primary mb-3">
{__("Please review and download the Non-Disclosure Agreement:")}
</p>
<div className="flex justify-center">
<Button
variant="secondary"
icon={IconArrowDown}
onClick={() => {
const link = document.createElement('a');
link.href = ndaFileUrl;
link.download = ndaFileName || 'NDA.pdf';
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
{sprintf(__("Download %s"), ndaFileName)}
</Button>
</div>
</div>
) : (
<>
<p className="text-sm font-medium text-txt-primary mb-2">
{__("By accepting this agreement, you commit to:")}
</p>
<ul className="text-sm text-txt-secondary space-y-0.5">
<li className="flex items-start">
<span className="inline-block w-2 h-2 rounded-full bg-txt-tertiary mt-1.5 mr-2 flex-shrink-0"></span>
{__("Keep confidential information secure")}
</li>
<li className="flex items-start">
<span className="inline-block w-2 h-2 rounded-full bg-txt-tertiary mt-1.5 mr-2 flex-shrink-0"></span>
{__("Not share or disclose sensitive data")}
</li>
<li className="flex items-start">
<span className="inline-block w-2 h-2 rounded-full bg-txt-tertiary mt-1.5 mr-2 flex-shrink-0"></span>
{__("Use information only for authorized purposes")}
</li>
</ul>
</>
)}
</div>
<div className="flex items-start space-x-2 p-3 bg-level-0 rounded-lg border border-border-subtle">
<div className="mt-0.5">
<Checkbox
checked={isChecked}
onChange={setIsChecked}
/>
</div>
<label
className="text-sm text-txt-primary cursor-pointer flex-1"
onClick={() => setIsChecked(!isChecked)}
>
{__("I agree to the terms of the Non-Disclosure Agreement and will handle all information accordingly.")}
</label>
</div>
</div>
</DialogContent>
<DialogFooter exitLabel={__("Disconnect")}>
<Button
variant="primary"
onClick={handleAccept}
disabled={!isChecked || acceptNdaMutation.isPending}
>
{acceptNdaMutation.isPending ? __("Accepting...") : __("Accept & Continue")}
</Button>
</DialogFooter>
</Dialog>
);
}

View File

@@ -1,121 +0,0 @@
import { useState } from "react";
import {
Dialog,
DialogFooter,
DialogContent,
Button,
Field,
useToast,
useDialogRef,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { z } from "zod";
import { useCreateTrustCenterAccess } from "/hooks/useTrustCenterQueries";
type Props = {
trigger: React.ReactNode;
trustCenterId: string;
organizationName: string;
};
export function PublicTrustCenterAccessRequestDialog({
trigger,
trustCenterId,
organizationName
}: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const dialogRef = useDialogRef();
const mutation = useCreateTrustCenterAccess();
const schema = z.object({
name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")),
email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")),
});
const { register, handleSubmit, formState, reset } = useFormWithSchema(schema, {
defaultValues: { name: "", email: "" },
});
const onSubmit = handleSubmit(async (data) => {
setIsSubmitting(true);
mutation.mutate(
{
trustCenterId,
email: data.email,
name: data.name,
},
{
onSuccess: (result) => {
if (result.createTrustCenterAccess) {
toast({
title: __("Request Submitted"),
description: __("Your access request has been submitted. You will receive an email if your request is approved."),
variant: "success",
});
reset();
dialogRef.current?.close();
}
setIsSubmitting(false);
},
onError: (_: Error) => {
toast({
title: __("Error"),
description: __("An error occurred while submitting your request."),
variant: "error",
});
setIsSubmitting(false);
},
}
);
});
return (
<Dialog
ref={dialogRef}
trigger={trigger}
title={__("Request Access")}
>
<form onSubmit={onSubmit}>
<DialogContent padded className="space-y-4">
<div className="text-sm text-txt-secondary">
{sprintf(__("Request access to %s's Trust Center. Your request will be reviewed and you will receive an email notification with access instructions if approved."), organizationName)}
</div>
<Field
label={__("Your Name")}
required
error={formState.errors.name?.message}
{...register("name")}
placeholder={__("Enter your full name")}
disabled={isSubmitting}
/>
<Field
label={__("Email Address")}
required
type="email"
error={formState.errors.email?.message}
{...register("email")}
placeholder={__("Enter your email address")}
disabled={isSubmitting}
/>
</DialogContent>
<DialogFooter>
<Button
type="submit"
disabled={isSubmitting || mutation.isPending}
>
{isSubmitting || mutation.isPending ? __("Submitting...") : __("Submit Request")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -1,148 +0,0 @@
import {
Card,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
Button,
IconArrowDown,
IconLock,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { PublicTrustCenterAccessRequestDialog } from "./PublicTrustCenterAccessRequestDialog";
import { useExportReportPDF } from "../../hooks/useTrustCenterQueries";
import type { TrustCenterAudit } from "../pages/PublicTrustCenterPage";
type Props = {
audits: TrustCenterAudit[];
organizationName: string;
isAuthenticated: boolean;
trustCenterId: string;
};
export function PublicTrustCenterAudits({
audits,
organizationName,
isAuthenticated,
trustCenterId
}: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const mutation = useExportReportPDF();
const handleDownload = (report: NonNullable<TrustCenterAudit["report"]>) => {
mutation.mutate(report.id, {
onSuccess: (data) => {
if (data.exportReportPDF?.data) {
const link = window.document.createElement("a");
link.href = data.exportReportPDF.data;
link.download = `${report.filename}`;
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
}
},
onError: () => {
toast({
title: __("Download Failed"),
description: __("Unable to download the report. Please try again."),
variant: "error",
});
},
});
};
if (audits.length === 0) {
return (
<Card padded>
<div className="text-center py-8">
<h2 className="text-xl font-semibold text-txt-primary mb-2">
{__("Compliance")}
</h2>
<p className="text-txt-secondary">
{__("No compliance reports are currently available.")}
</p>
</div>
</Card>
);
}
return (
<Card padded className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-txt-primary">
{__("Compliance")}
</h2>
<p className="text-sm text-txt-secondary mt-1">
{sprintf(__("%s is compliant with the following frameworks"), organizationName)}
</p>
</div>
<Table>
<Thead>
<Tr>
<Th>{__("Framework")}</Th>
<Th>{__("Report")}</Th>
</Tr>
</Thead>
<Tbody>
{audits.map((audit) => {
const hasReport = audit.report !== null;
return (
<Tr key={audit.id}>
<Td>
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<div className="w-8 h-8 [&>img]:w-8 [&>img]:h-8 [&>div]:w-8 [&>div]:h-8">
<FrameworkLogo name={audit.framework.name} />
</div>
</div>
<div className="font-medium">
{audit.framework.name}
</div>
</div>
</Td>
<Td>
{!hasReport ? (
<span className="text-txt-tertiary text-sm">
{__("No report")}
</span>
) : !isAuthenticated ? (
<PublicTrustCenterAccessRequestDialog
trigger={
<Button
variant="secondary"
icon={IconLock}
>
{__("Request Access")}
</Button>
}
trustCenterId={trustCenterId}
organizationName={organizationName}
/>
) : (
<Button
variant="secondary"
icon={IconArrowDown}
onClick={() => handleDownload(audit.report!)}
disabled={mutation.isPending}
>
{mutation.isPending ? __("Downloading...") : __("Download")}
</Button>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,137 +0,0 @@
import {
Card,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
DocumentTypeBadge,
Button,
IconArrowDown,
IconLock,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useExportDocumentPDF, type TrustCenterDocument } from "/hooks/useTrustCenterQueries";
import { PublicTrustCenterAccessRequestDialog } from "./PublicTrustCenterAccessRequestDialog";
type Props = {
documents: TrustCenterDocument[];
isAuthenticated: boolean;
trustCenterId: string;
organizationName: string;
};
export function PublicTrustCenterDocuments({
documents,
isAuthenticated,
trustCenterId,
organizationName
}: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const mutation = useExportDocumentPDF();
const handleDownload = (document: TrustCenterDocument) => {
mutation.mutate(document.id, {
onSuccess: (data) => {
if (data.exportDocumentPDF?.data) {
const link = window.document.createElement("a");
link.href = data.exportDocumentPDF.data;
link.download = `${document.title}.pdf`;
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
}
},
onError: () => {
toast({
title: __("Download Failed"),
description: __("Unable to download the document. Please try again."),
variant: "error",
});
},
});
};
if (documents.length === 0) {
return (
<Card padded>
<div className="text-center py-8">
<h2 className="text-xl font-semibold text-txt-primary mb-2">
{__("Documents")}
</h2>
<p className="text-txt-secondary">
{__("No documents are currently available.")}
</p>
</div>
</Card>
);
}
return (
<Card padded className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-txt-primary">
{__("Documents")}
</h2>
<p className="text-sm text-txt-secondary mt-1">
{__("Security and compliance documentation")}
</p>
</div>
<Table>
<Thead>
<Tr>
<Th className="w-1/2">{__("Document")}</Th>
<Th className="w-1/4">{__("Type")}</Th>
<Th className="w-1/4">{__("Download")}</Th>
</Tr>
</Thead>
<Tbody>
{documents.map((document) => {
return (
<Tr key={document.id}>
<Td>
<div className="font-medium">
{document.title}
</div>
</Td>
<Td>
<DocumentTypeBadge type={document.documentType} />
</Td>
<Td>
{!isAuthenticated ? (
<PublicTrustCenterAccessRequestDialog
trigger={
<Button
variant="secondary"
icon={IconLock}
>
{__("Request Access")}
</Button>
}
trustCenterId={trustCenterId}
organizationName={organizationName}
/>
) : (
<Button
variant="secondary"
icon={IconArrowDown}
onClick={() => handleDownload(document)}
disabled={mutation.isPending}
>
{mutation.isPending ? __("Downloading...") : __("Download")}
</Button>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,122 +0,0 @@
import {
Card,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { faviconUrl, sprintf, getCountryName, type CountryCode } from "@probo/helpers";
import type { TrustCenterVendor } from "../pages/PublicTrustCenterPage";
type Props = {
vendors: TrustCenterVendor[];
organizationName: string;
};
export function PublicTrustCenterVendors({ vendors, organizationName }: Props) {
const { __ } = useTranslate();
const hasCountriesData = vendors.some(vendor => vendor.countries && vendor.countries.length > 0);
if (vendors.length === 0) {
return (
<Card padded>
<div className="text-center py-8">
<h2 className="text-xl font-semibold text-txt-primary mb-2">
{__("Subcontractors")}
</h2>
<p className="text-txt-secondary">
{__("No subcontractor information is currently available.")}
</p>
</div>
</Card>
);
}
return (
<Card padded className="space-y-4">
<div>
<h2 className="text-xl font-semibold text-txt-primary">
{__("Subcontractors")}
</h2>
<p className="text-sm text-txt-secondary mt-1">
{sprintf(__("Third-party subcontractors %s work with"), organizationName)}
</p>
</div>
<Table>
<Thead>
<Tr>
<Th>{__("Company")}</Th>
<Th>{__("Website")}</Th>
{hasCountriesData && <Th>{__("Countries")}</Th>}
</Tr>
</Thead>
<Tbody>
{vendors.map((vendor) => {
const url = vendor.privacyPolicyUrl || vendor.websiteUrl;
const logo = faviconUrl(vendor.websiteUrl);
const getCleanUrl = (url: string) => {
try {
const parsedUrl = new URL(url);
return parsedUrl.hostname + parsedUrl.pathname + parsedUrl.search;
} catch {
return url.replace(/^https?:\/\//, '');
}
};
const formatCountries = (countries: CountryCode[]) => {
return countries.map(code => getCountryName(__, code)).join(", ");
};
return (
<Tr key={vendor.id}>
<Td>
<div className="flex items-center space-x-3">
{logo && (
<img
src={logo}
alt={`${vendor.name} logo`}
className="w-8 h-8 object-contain rounded-full"
/>
)}
<div className="font-medium">
{vendor.name}
</div>
</div>
</Td>
<Td>
{url ? (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-txt-info hover:opacity-80 underline transition-opacity"
>
{getCleanUrl(url)}
</a>
) : (
<span className="text-txt-secondary text-sm">
{__("No website available")}
</span>
)}
</Td>
{hasCountriesData && (
<Td>
<span className="text-txt-secondary text-sm">
{formatCountries(vendor.countries)}
</span>
</Td>
)}
</Tr>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,115 +0,0 @@
import { useParams, Navigate } from "react-router";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { PublicTrustCenterLayout } from "/layouts/PublicTrustCenterLayout";
import { PublicTrustCenterAudits } from "../components/PublicTrustCenterAudits";
import { PublicTrustCenterVendors } from "../components/PublicTrustCenterVendors";
import { PublicTrustCenterDocuments } from "../components/PublicTrustCenterDocuments";
import { NDAAcceptanceDialog } from "../components/NDAAcceptanceDialog";
import { Spinner } from "@probo/ui";
import { useTrustCenterQuery, type TrustCenterDocument, type TrustCenterAudit, type TrustCenterVendor } from "/hooks/useTrustCenterQueries";
export type { TrustCenterDocument, TrustCenterAudit, TrustCenterVendor };
export default function PublicTrustCenterPage() {
const { __ } = useTranslate();
const { slug } = useParams<{ slug: string }>();
const { data, isLoading, error } = useTrustCenterQuery(slug || "");
const organization = data?.trustCenterBySlug?.organization;
const organizationName = organization?.name || "";
usePageTitle(
organizationName ? `${organizationName} - Trust Center` : "Trust Center"
);
if (!slug) {
return <Navigate to="/" replace />;
}
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Spinner />
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
{__("Error Loading Trust Center")}
</h1>
<p className="text-gray-600">
{__("There was an error loading the trust center. Please try again later.")}
</p>
</div>
</div>
);
}
if (!data?.trustCenterBySlug) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
{__("Trust Center Not Found")}
</h1>
<p className="text-gray-600">
{__("The trust center you're looking for doesn't exist.")}
</p>
</div>
</div>
);
}
const { trustCenterBySlug } = data;
const { documents, audits, vendors, isUserAuthenticated, hasAcceptedNonDisclosureAgreement } = trustCenterBySlug;
const trustCenterDocuments = documents.edges.map((edge) => edge.node) as TrustCenterDocument[];
const trustCenterAudits = audits.edges.map((edge) => edge.node) as TrustCenterAudit[];
const trustCenterVendors = vendors.edges.map((edge) => edge.node) as TrustCenterVendor[];
const showNdaDialog = isUserAuthenticated && !hasAcceptedNonDisclosureAgreement;
return (
<>
{showNdaDialog && (
<NDAAcceptanceDialog
trustCenterId={trustCenterBySlug.id}
organizationName={organizationName}
ndaFileName={trustCenterBySlug.ndaFileName}
ndaFileUrl={trustCenterBySlug.ndaFileUrl}
/>
)}
<PublicTrustCenterLayout
organizationName={organizationName}
organizationLogo={organization?.logoUrl}
isAuthenticated={isUserAuthenticated}
>
<div className="space-y-12">
<PublicTrustCenterAudits
audits={trustCenterAudits}
organizationName={organizationName}
isAuthenticated={isUserAuthenticated}
trustCenterId={trustCenterBySlug.id}
/>
<PublicTrustCenterDocuments
documents={trustCenterDocuments}
organizationName={organizationName}
isAuthenticated={isUserAuthenticated}
trustCenterId={trustCenterBySlug.id}
/>
<PublicTrustCenterVendors
vendors={trustCenterVendors}
organizationName={organizationName}
/>
</div>
</PublicTrustCenterLayout>
</>
);
}