Add nda to trust center

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-09 16:17:24 +02:00
parent 28c41fa4ae
commit b06bd113f3
37 changed files with 2921 additions and 259 deletions

View File

@@ -26,6 +26,7 @@ export const trustCenterAccessesQuery = graphql`
email
name
active
hasAcceptedNonDisclosureAgreement
createdAt
}
}
@@ -48,6 +49,7 @@ export const createTrustCenterAccessMutation = graphql`
email
name
active
hasAcceptedNonDisclosureAgreement
createdAt
}
}
@@ -65,6 +67,7 @@ export const updateTrustCenterAccessMutation = graphql`
email
name
active
hasAcceptedNonDisclosureAgreement
createdAt
updatedAt
}

View File

@@ -12,6 +12,8 @@ export const trustCenterQuery = graphql`
id
active
slug
ndaFileName
ndaFileUrl
createdAt
updatedAt
}
@@ -66,3 +68,48 @@ export function useUpdateTrustCenterMutation() {
}
);
}
export const uploadTrustCenterNDAMutation = graphql`
mutation TrustCenterGraphUploadNDAMutation($input: UploadTrustCenterNDAInput!) {
uploadTrustCenterNDA(input: $input) {
trustCenter {
id
ndaFileName
updatedAt
}
}
}
`;
export function useUploadTrustCenterNDAMutation() {
return useMutationWithToasts(
uploadTrustCenterNDAMutation,
{
successMessage: "NDA uploaded successfully",
errorMessage: "Failed to upload NDA",
}
);
}
export const deleteTrustCenterNDAMutation = graphql`
mutation TrustCenterGraphDeleteNDAMutation($input: DeleteTrustCenterNDAInput!) {
deleteTrustCenterNDA(input: $input) {
trustCenter {
id
ndaFileName
updatedAt
}
}
}
`;
export function useDeleteTrustCenterNDAMutation() {
return useMutationWithToasts(
deleteTrustCenterNDAMutation,
{
successMessage: "NDA deleted successfully",
errorMessage: "Failed to delete NDA",
}
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<ae95aed7bc22c95f8cc2ab591039c810>>
* @generated SignedSource<<5ccaab70e4cb8c4b5bee7fbe6ba2c0b6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -27,6 +27,7 @@ export type TrustCenterAccessGraphCreateMutation$data = {
readonly active: boolean;
readonly createdAt: any;
readonly email: string;
readonly hasAcceptedNonDisclosureAgreement: boolean;
readonly id: string;
readonly name: string;
};
@@ -107,6 +108,13 @@ v3 = {
"name": "active",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasAcceptedNonDisclosureAgreement",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -186,16 +194,16 @@ return {
]
},
"params": {
"cacheID": "eafddcd0263963235d3249c22eb50593",
"cacheID": "fa88b100be7598cf46159cf79199c398",
"id": null,
"metadata": {},
"name": "TrustCenterAccessGraphCreateMutation",
"operationKind": "mutation",
"text": "mutation TrustCenterAccessGraphCreateMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccessEdge {\n cursor\n node {\n id\n email\n name\n active\n createdAt\n }\n }\n }\n}\n"
"text": "mutation TrustCenterAccessGraphCreateMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccessEdge {\n cursor\n node {\n id\n email\n name\n active\n hasAcceptedNonDisclosureAgreement\n createdAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "99676fee0b2de06a92cdad66c577eee7";
(node as any).hash = "6c6c1344730e7d908a5c6392f578ca81";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<2333a6a7d5415f1a08a5612dcaceee8f>>
* @generated SignedSource<<e9cdda9f586cee5ffe66f238216060f6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -22,6 +22,7 @@ export type TrustCenterAccessGraphQuery$data = {
readonly active: boolean;
readonly createdAt: any;
readonly email: string;
readonly hasAcceptedNonDisclosureAgreement: boolean;
readonly id: string;
readonly name: string;
};
@@ -163,6 +164,13 @@ v5 = [
"name": "active",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasAcceptedNonDisclosureAgreement",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -290,7 +298,7 @@ return {
]
},
"params": {
"cacheID": "5b83cd4ae2434ce2e00de7230d264432",
"cacheID": "0286ec03fb4ae012ada7bae8a2234152",
"id": null,
"metadata": {
"connection": [
@@ -307,11 +315,11 @@ return {
},
"name": "TrustCenterAccessGraphQuery",
"operationKind": "query",
"text": "query TrustCenterAccessGraphQuery(\n $trustCenterId: ID!\n) {\n node(id: $trustCenterId) {\n __typename\n ... on TrustCenter {\n id\n accesses(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n email\n name\n active\n createdAt\n __typename\n }\n }\n }\n }\n id\n }\n}\n"
"text": "query TrustCenterAccessGraphQuery(\n $trustCenterId: ID!\n) {\n node(id: $trustCenterId) {\n __typename\n ... on TrustCenter {\n id\n accesses(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n email\n name\n active\n hasAcceptedNonDisclosureAgreement\n createdAt\n __typename\n }\n }\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "af598fd2af198e63ed84fd618857a985";
(node as any).hash = "f837b96937f1397ac1a3065f58386e4d";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6de2ecb63a58c88c3008368061943b49>>
* @generated SignedSource<<d2f40d8fc2bd9c7627d308660dedb1e5>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -23,6 +23,7 @@ export type TrustCenterAccessGraphUpdateMutation$data = {
readonly active: boolean;
readonly createdAt: any;
readonly email: string;
readonly hasAcceptedNonDisclosureAgreement: boolean;
readonly id: string;
readonly name: string;
readonly updatedAt: any;
@@ -93,6 +94,13 @@ v1 = [
"name": "active",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasAcceptedNonDisclosureAgreement",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -132,16 +140,16 @@ return {
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "683ee01fb5173b49b0a7f3c2d99cf002",
"cacheID": "223169ee8a4f65008047097ecff68fc5",
"id": null,
"metadata": {},
"name": "TrustCenterAccessGraphUpdateMutation",
"operationKind": "mutation",
"text": "mutation TrustCenterAccessGraphUpdateMutation(\n $input: UpdateTrustCenterAccessInput!\n) {\n updateTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n email\n name\n active\n createdAt\n updatedAt\n }\n }\n}\n"
"text": "mutation TrustCenterAccessGraphUpdateMutation(\n $input: UpdateTrustCenterAccessInput!\n) {\n updateTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n email\n name\n active\n hasAcceptedNonDisclosureAgreement\n createdAt\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "cccd083b0047f6bc7b504b7494205e9d";
(node as any).hash = "0da1f737e6ea1db7a1b9930c4b6cc545";
export default node;

View File

@@ -0,0 +1,121 @@
/**
* @generated SignedSource<<43a8665719f251c1bae028ff7821b14a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteTrustCenterNDAInput = {
trustCenterId: string;
};
export type TrustCenterGraphDeleteNDAMutation$variables = {
input: DeleteTrustCenterNDAInput;
};
export type TrustCenterGraphDeleteNDAMutation$data = {
readonly deleteTrustCenterNDA: {
readonly trustCenter: {
readonly id: string;
readonly ndaFileName: string | null | undefined;
readonly updatedAt: any;
};
};
};
export type TrustCenterGraphDeleteNDAMutation = {
response: TrustCenterGraphDeleteNDAMutation$data;
variables: TrustCenterGraphDeleteNDAMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "DeleteTrustCenterNDAPayload",
"kind": "LinkedField",
"name": "deleteTrustCenterNDA",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenter",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustCenterGraphDeleteNDAMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustCenterGraphDeleteNDAMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "fa3ebf3593c396bf05819d357d9f909f",
"id": null,
"metadata": {},
"name": "TrustCenterGraphDeleteNDAMutation",
"operationKind": "mutation",
"text": "mutation TrustCenterGraphDeleteNDAMutation(\n $input: DeleteTrustCenterNDAInput!\n) {\n deleteTrustCenterNDA(input: $input) {\n trustCenter {\n id\n ndaFileName\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "9c055069b9e2e7432fed7509f6578a07";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<25550a93c5459c3b405c782be19ac888>>
* @generated SignedSource<<9dbea87f398ce8189cb2431d7c03912c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -37,6 +37,8 @@ export type TrustCenterGraphQuery$data = {
readonly active: boolean;
readonly createdAt: any;
readonly id: string;
readonly ndaFileName: string | null | undefined;
readonly ndaFileUrl: string | null | undefined;
readonly slug: string;
readonly updatedAt: any;
} | null | undefined;
@@ -114,6 +116,20 @@ v5 = {
"name": "slug",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileUrl",
"storageKey": null
},
(v4/*: any*/),
{
"alias": null,
@@ -544,16 +560,16 @@ return {
]
},
"params": {
"cacheID": "28c1a32327e2fee9320fd091100ec800",
"cacheID": "8dff30a639470418ba7151cb5743d3cc",
"id": null,
"metadata": {},
"name": "TrustCenterGraphQuery",
"operationKind": "query",
"text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n trustCenter {\n id\n active\n slug\n createdAt\n updatedAt\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...TrustCenterDocumentsCardFragment\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...TrustCenterAuditsCardFragment\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n name\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n showOnTrustCenter\n createdAt\n}\n\nfragment TrustCenterDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n showOnTrustCenter\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n"
"text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n trustCenter {\n id\n active\n slug\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...TrustCenterDocumentsCardFragment\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...TrustCenterAuditsCardFragment\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n name\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n showOnTrustCenter\n createdAt\n}\n\nfragment TrustCenterDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n showOnTrustCenter\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n"
}
};
})();
(node as any).hash = "260529b3ca03240b07989f278bb7a539";
(node as any).hash = "21b2915ae5002dd54c990013e94adf5d";
export default node;

View File

@@ -0,0 +1,123 @@
/**
* @generated SignedSource<<966d4aa638042469c717ddc96e5c5d78>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type UploadTrustCenterNDAInput = {
file: any;
fileName: string;
trustCenterId: string;
};
export type TrustCenterGraphUploadNDAMutation$variables = {
input: UploadTrustCenterNDAInput;
};
export type TrustCenterGraphUploadNDAMutation$data = {
readonly uploadTrustCenterNDA: {
readonly trustCenter: {
readonly id: string;
readonly ndaFileName: string | null | undefined;
readonly updatedAt: any;
};
};
};
export type TrustCenterGraphUploadNDAMutation = {
response: TrustCenterGraphUploadNDAMutation$data;
variables: TrustCenterGraphUploadNDAMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UploadTrustCenterNDAPayload",
"kind": "LinkedField",
"name": "uploadTrustCenterNDA",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenter",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustCenterGraphUploadNDAMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustCenterGraphUploadNDAMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "09a5dbf73f3e279173691b235f9da2e2",
"id": null,
"metadata": {},
"name": "TrustCenterGraphUploadNDAMutation",
"operationKind": "mutation",
"text": "mutation TrustCenterGraphUploadNDAMutation(\n $input: UploadTrustCenterNDAInput!\n) {\n uploadTrustCenterNDA(input: $input) {\n trustCenter {\n id\n ndaFileName\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "b9877f8a5b9c2c12addeb939360719f1";
export default node;

View File

@@ -34,6 +34,9 @@ interface TrustCenterQueryData {
active: boolean;
slug: string;
isUserAuthenticated: boolean;
hasAcceptedNonDisclosureAgreement: boolean;
ndaFileName: string | null;
ndaFileUrl: string | null;
organization: {
id: string;
name: string;
@@ -96,7 +99,19 @@ interface CreateTrustCenterAccessVariables {
};
}
type GraphQLVariables = TrustCenterQueryVariables | ExportDocumentPDFVariables | CreateTrustCenterAccessVariables | Record<string, never>;
interface AcceptNonDisclosureAgreementData {
acceptNonDisclosureAgreement: {
success: boolean;
};
}
interface AcceptNonDisclosureAgreementVariables {
input: {
trustCenterId: string;
};
}
type GraphQLVariables = TrustCenterQueryVariables | ExportDocumentPDFVariables | CreateTrustCenterAccessVariables | AcceptNonDisclosureAgreementVariables | Record<string, never>;
async function trustCenterGraphQLRequest<T = unknown>(
operationName: string,
@@ -127,12 +142,14 @@ async function trustCenterGraphQLRequest<T = unknown>(
function isCriticalError(error: GraphQLError): boolean {
const message = error.message?.toLowerCase() || '';
const path = error.path || [];
if (message.includes('access denied') || message.includes('authentication required')) {
if (path.length > 2) {
return false;
}
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;
@@ -145,6 +162,9 @@ const TRUST_CENTER_QUERY = `
active
slug
isUserAuthenticated
hasAcceptedNonDisclosureAgreement
ndaFileName
ndaFileUrl
organization {
id
name
@@ -213,6 +233,16 @@ const CREATE_TRUST_CENTER_ACCESS_MUTATION = `
}
`;
const ACCEPT_NDA_MUTATION = `
mutation AcceptNonDisclosureAgreementMutation(
$input: AcceptNonDisclosureAgreementInput!
) {
acceptNonDisclosureAgreement(input: $input) {
success
}
}
`;
export function useTrustCenterQuery(slug: string) {
return useQuery<TrustCenterQueryData>({
queryKey: ["trust-center", slug],
@@ -297,3 +327,27 @@ export function useCreateTrustCenterAccess() {
},
});
}
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

@@ -127,7 +127,9 @@ export default function TrustCenterAccessPage() {
if (error) {
const isTokenError = error.toLowerCase().includes('token') ||
error.toLowerCase().includes('expired') ||
error.toLowerCase().includes('invalid');
error.toLowerCase().includes('invalid') ||
error.toLowerCase().includes('401') ||
error.toLowerCase().includes('unauthorized');
if (isTokenError) {
return <TokenErrorPage error={error} />;

View File

@@ -16,6 +16,7 @@ import {
useDialogRef,
IconTrashCan,
IconPencil,
IconCheckmark1,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useOutletContext } from "react-router";
@@ -85,6 +86,7 @@ export default function TrustCenterAccessTab() {
email: string;
name: string;
active: boolean;
hasAcceptedNonDisclosureAgreement: boolean;
createdAt: Date;
};
@@ -95,6 +97,7 @@ export default function TrustCenterAccessTab() {
email: edge.node.email,
name: edge.node.name,
active: edge.node.active,
hasAcceptedNonDisclosureAgreement: edge.node.hasAcceptedNonDisclosureAgreement,
createdAt: new Date(edge.node.createdAt)
})) ?? [];
@@ -203,6 +206,7 @@ export default function TrustCenterAccessTab() {
<Th>{__("Email")}</Th>
<Th>{__("Date")}</Th>
<Th>{__("Active")}</Th>
<Th>{__("NDA")}</Th>
<Th></Th>
</Tr>
</Thead>
@@ -220,6 +224,11 @@ export default function TrustCenterAccessTab() {
onChange={(active) => handleToggleActive(access.id, active)}
/>
</Td>
<Td>
{access.hasAcceptedNonDisclosureAgreement && (
<IconCheckmark1 size={16} className="text-txt-success" />
)}
</Td>
<Td noLink width={160} className="text-end">
<div className="flex gap-2 justify-end">
<Button

View File

@@ -4,6 +4,7 @@ import {
Button,
Card,
Checkbox,
Dropzone,
Field,
Input,
PageHeader,
@@ -12,9 +13,10 @@ import {
Tabs,
TabLink,
TabItem,
IconTrashCan,
} from "@probo/ui";
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { trustCenterQuery, useUpdateTrustCenterMutation } from "/hooks/graph/TrustCenterGraph";
import { trustCenterQuery, useUpdateTrustCenterMutation, useUploadTrustCenterNDAMutation, useDeleteTrustCenterNDAMutation } from "/hooks/graph/TrustCenterGraph";
import type { TrustCenterGraphQuery } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
import { useState } from "react";
import { useOrganizationId } from "/hooks/useOrganizationId";
@@ -32,6 +34,8 @@ export default function TrustCenterPage({ queryRef }: Props) {
const { organization } = usePreloadedQuery(trustCenterQuery, queryRef);
const [updateTrustCenter, isUpdating] = useUpdateTrustCenterMutation();
const [uploadNDA, isUploadingNDA] = useUploadTrustCenterNDAMutation();
const [deleteNDA, isDeletingNDA] = useDeleteTrustCenterNDAMutation();
const [isActive, setIsActive] = useState(organization.trustCenter?.active || false);
const [slug, setSlug] = useState(organization.trustCenter?.slug || "");
const [isUpdatingSlug, setIsUpdatingSlug] = useState(false);
@@ -101,6 +105,57 @@ export default function TrustCenterPage({ queryRef }: Props) {
});
};
const handleNDAUpload = async (files: File[]) => {
if (!organization.trustCenter?.id) {
toast({
title: __("Error"),
description: __("Trust center not found"),
variant: "error",
});
return;
}
if (files.length === 0) return;
const file = files[0];
await uploadNDA({
variables: {
input: {
trustCenterId: organization.trustCenter.id,
fileName: file.name,
file: null,
},
},
uploadables: {
"input.file": file,
},
});
};
const handleNDADelete = async () => {
if (!organization.trustCenter?.id) {
toast({
title: __("Error"),
description: __("Trust center not found"),
variant: "error",
});
return;
}
if (!confirm(__("Are you sure you want to delete the NDA file?"))) {
return;
}
await deleteNDA({
variables: {
input: {
trustCenterId: organization.trustCenter.id,
},
},
});
};
const trustCenterUrl = organization.trustCenter?.slug
? `${window.location.origin}/trust/${organization.trustCenter.slug}`
: null;
@@ -211,6 +266,68 @@ export default function TrustCenterPage({ queryRef }: Props) {
</div>
</Card>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Non-Disclosure Agreement")}</h2>
{(isUploadingNDA || isDeletingNDA) && <Spinner />}
</div>
<Card padded className="space-y-4">
<div className="space-y-2">
{!organization.trustCenter?.ndaFileName ? (
<p className="text-sm text-txt-tertiary">
{__("Upload a Non-Disclosure Agreement that visitors must accept before accessing your trust center")}
</p>
) : (<></>)}
{organization.trustCenter?.ndaFileName ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">
{organization.trustCenter.ndaFileName || __("Non-Disclosure Agreement")}
</p>
</div>
<p className="text-xs text-txt-tertiary">
{__("Visitors will need to accept this NDA before accessing your trust center")}
</p>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="secondary"
onClick={() => {
if (organization.trustCenter?.ndaFileUrl) {
window.open(organization.trustCenter.ndaFileUrl, '_blank');
}
}}
>
{__("Download PDF")}
</Button>
<Button
variant="quaternary"
icon={IconTrashCan}
onClick={handleNDADelete}
disabled={isDeletingNDA}
/>
</div>
</div>
</div>
) : (
<Dropzone
description={__("Upload PDF files up to 10MB")}
isUploading={isUploadingNDA}
onDrop={handleNDAUpload}
accept={{
"application/pdf": [".pdf"],
}}
maxSize={10}
/>
)}
</div>
</Card>
</div>
<div className="space-y-4">
<Tabs>
<TabItem

View File

@@ -0,0 +1,187 @@
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

@@ -5,6 +5,7 @@ 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";
@@ -66,36 +67,49 @@ export default function PublicTrustCenterPage() {
}
const { trustCenterBySlug } = data;
const { documents, audits, vendors, isUserAuthenticated } = trustCenterBySlug;
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 (
<PublicTrustCenterLayout
organizationName={organizationName}
organizationLogo={organization?.logoUrl}
isAuthenticated={isUserAuthenticated}
>
<div className="space-y-12">
<PublicTrustCenterAudits
audits={trustCenterAudits}
organizationName={organizationName}
isAuthenticated={isUserAuthenticated}
<>
{showNdaDialog && (
<NDAAcceptanceDialog
trustCenterId={trustCenterBySlug.id}
/>
<PublicTrustCenterDocuments
documents={trustCenterDocuments}
organizationName={organizationName}
isAuthenticated={isUserAuthenticated}
trustCenterId={trustCenterBySlug.id}
ndaFileName={trustCenterBySlug.ndaFileName}
ndaFileUrl={trustCenterBySlug.ndaFileUrl}
/>
<PublicTrustCenterVendors
vendors={trustCenterVendors}
organizationName={organizationName}
/>
</div>
</PublicTrustCenterLayout>
)}
<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>
</>
);
}