Plug trust center part 1

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-09 19:14:17 +01:00
committed by Bryan Frimin
parent 663400c4f6
commit 7322201dab
41 changed files with 1959 additions and 1033 deletions

View File

@@ -1,11 +1,11 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
import type { PropsWithChildren } from "react";
import { use, type PropsWithChildren } from "react";
import { domain } from "@probo/helpers";
import { AuditRowAvatar } from "./AuditRow";
import { RequestAccessDialog } from "./RequestAccessDialog";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
import { Viewer } from "/providers/Viewer";
export function OrganizationSidebar({
trustCenter,
@@ -13,7 +13,7 @@ export function OrganizationSidebar({
trustCenter: TrustGraphQuery$data["trustCenterBySlug"];
}) {
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
const isAuthenticated = !!use(Viewer);
if (!trustCenter) {
return null;

View File

@@ -15,9 +15,9 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { useTrustCenter } from "/hooks/useTrustCenter";
import { type FormEventHandler, type PropsWithChildren } from "react";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated.ts";
import { use, type FormEventHandler, type PropsWithChildren } from "react";
import { InvalidError } from "/providers/RelayProviders";
import { Viewer } from "/providers/Viewer";
type Props = PropsWithChildren<{
documentId?: string;
@@ -27,7 +27,7 @@ type Props = PropsWithChildren<{
}>;
const schema = z.object({
name: z.string(),
fullName: z.string(),
email: z.string().email(),
});
@@ -41,18 +41,28 @@ export function RequestAccessDialog({
const trustCenter = useTrustCenter();
const { toast } = useToast();
const { __ } = useTranslate();
const { handleSubmit, register, setError, formState } = useFormWithSchema(schema, {
defaultValues: {
name: "",
email: "",
const viewer = use(Viewer);
const { handleSubmit, register, setError, formState } = useFormWithSchema(
schema,
{
defaultValues: {
fullName: "",
email: "",
},
},
});
const isAuthenticated = useIsAuthenticated();
);
const dialogRef = useDialogRef();
const [commitMutation, isMutating] = useMutation({ documentId, reportId, trustCenterFileId });
const [commitMutation, isMutating] = useMutation({
documentId,
reportId,
trustCenterFileId,
});
const submitCallback = (data: z.infer<typeof schema> | null) => {
commitMutation(data)
commitMutation({
email: data?.email ?? viewer?.email ?? "",
fullName: data?.fullName ?? viewer?.fullName ?? "",
})
.then(() => {
onSuccess?.();
toast({
@@ -65,7 +75,7 @@ export function RequestAccessDialog({
.catch((error) => {
if (error instanceof InvalidError) {
if (error.field === "email") {
setError(error.field, {message: error.message})
setError(error.field, { message: error.message });
}
return;
}
@@ -77,7 +87,7 @@ export function RequestAccessDialog({
});
};
const onSubmit: FormEventHandler<HTMLFormElement> = isAuthenticated
const onSubmit: FormEventHandler<HTMLFormElement> = viewer
? (e) => {
e.preventDefault();
submitCallback(null);
@@ -102,12 +112,12 @@ export function RequestAccessDialog({
trustCenter.organization.name,
)}
</p>
{!isAuthenticated && (
{!viewer && (
<div className="space-y-4">
<Field
label={__("Full name")}
placeholder="John Doe"
{...register("name")}
{...register("fullName")}
type="text"
/>
<Field
@@ -195,8 +205,10 @@ function useMutation({
useMutationWithToasts(requestDocumentAccessMutation);
const [commitRequestReportAccess, isRequestingReportAccess] =
useMutationWithToasts(requestReportAccessMutation);
const [commitRequestTrustCenterFileAccess, isRequestingTrustCenterFileAccess] =
useMutationWithToasts(requestTrustCenterFileAccessMutation);
const [
commitRequestTrustCenterFileAccess,
isRequestingTrustCenterFileAccess,
] = useMutationWithToasts(requestTrustCenterFileAccessMutation);
if (trustCenterFileId) {
return [

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5344ac5d6a506485d45e399361b92299>>
* @generated SignedSource<<5113feef2c7f7b13a2d6348202086916>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,8 +11,8 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestDocumentAccessInput = {
documentId: string;
email?: any | null | undefined;
name?: string | null | undefined;
email: any;
fullName: string;
trustCenterId: string;
};
export type RequestAccessDialogDocumentMutation$variables = {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<19ada29ea9b8f557b4fcc39f57bc3aec>>
* @generated SignedSource<<294b6a25a178cf3bd9269591ef61381f>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestAllAccessesInput = {
email?: any | null | undefined;
name?: string | null | undefined;
email: any;
fullName: string;
trustCenterId: string;
};
export type RequestAccessDialogMutation$variables = {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<858effd78b88a123430edcf6fcb100ed>>
* @generated SignedSource<<d3209530c326da3c7db6535cef428ded>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestReportAccessInput = {
email?: any | null | undefined;
name?: string | null | undefined;
email: any;
fullName: string;
reportId: string;
trustCenterId: string;
};

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<32242c5e0b4dc9c9368c0e2f4034d95e>>
* @generated SignedSource<<1e744bd11b2d5dd4d982bf1ce8b2a721>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestTrustCenterFileAccessInput = {
email?: any | null | undefined;
name?: string | null | undefined;
email: any;
fullName: string;
trustCenterFileId: string;
trustCenterId: string;
};

View File

@@ -1,6 +0,0 @@
import { useContext } from "react";
import { AuthContext } from "/providers/AuthProvider";
export function useIsAuthenticated(): boolean {
return useContext(AuthContext).isAuthenticated;
}

View File

@@ -6,8 +6,8 @@ import { useTranslate } from "@probo/i18n";
import { OrganizationSidebar } from "/components/OrganizationSidebar";
import { Outlet } from "react-router";
import { NDADialog } from "/components/NDADialog";
import { AuthProvider } from "/providers/AuthProvider";
import { TrustCenterProvider } from "/providers/TrustCenterProvider";
import { Viewer } from "/providers/Viewer";
type Props = {
queryRef: PreloadedQuery<TrustGraphCurrentQuery>;
@@ -26,7 +26,7 @@ export function MainLayout(props: Props) {
!trustCenter.hasAcceptedNonDisclosureAgreement &&
trustCenter.ndaFileUrl;
return (
<AuthProvider isAuthenticated={trustCenter.isUserAuthenticated}>
<Viewer value={data.viewer}>
<TrustCenterProvider trustCenter={trustCenter}>
{showNDADialog && (
<NDADialog
@@ -41,12 +41,8 @@ export function MainLayout(props: Props) {
<main>
<Tabs className="mb-8">
<TabLink to="/overview">{__("Overview")}</TabLink>
<TabLink to="/documents">
{__("Documents")}
</TabLink>
<TabLink to="/subprocessors">
{__("Subprocessors")}
</TabLink>
<TabLink to="/documents">{__("Documents")}</TabLink>
<TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>
</Tabs>
<Outlet context={{ trustCenter }} />
</main>
@@ -59,6 +55,6 @@ export function MainLayout(props: Props) {
{__("Powered by")} <Logo withPicto className="h-6" />
</a>
</TrustCenterProvider>
</AuthProvider>
</Viewer>
);
}

View File

@@ -0,0 +1,11 @@
import { createContext } from "react";
interface ViewerContextValue {
fullName: string;
email: string;
}
export const Viewer = createContext<ViewerContextValue | undefined | null>({
email: "",
fullName: "",
});

View File

@@ -82,6 +82,10 @@ export const trustVendorsQuery = graphql`
// Queries for custom domain (subdomain) approach
export const currentTrustGraphQuery = graphql`
query TrustGraphCurrentQuery {
viewer {
email
fullName
}
currentTrustCenter {
id
slug

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<ec32dbbc5f596dc323b1b4ff8df21fef>>
* @generated SignedSource<<1f5a4da3a77a893daebeceb249612a72>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -37,6 +37,10 @@ export type TrustGraphCurrentQuery$data = {
readonly slug: string;
readonly " $fragmentSpreads": FragmentRefs<"OverviewPageFragment">;
} | null | undefined;
readonly viewer: {
readonly email: any;
readonly fullName: string;
} | null | undefined;
};
export type TrustGraphCurrentQuery = {
response: TrustGraphCurrentQuery$data;
@@ -48,115 +52,122 @@ var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"name": "email",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "slug",
"name": "fullName",
"storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthenticated",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasAcceptedNonDisclosureAgreement",
"name": "slug",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileName",
"name": "isUserAuthenticated",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileUrl",
"name": "hasAcceptedNonDisclosureAgreement",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "ndaFileName",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"name": "ndaFileUrl",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"name": "name",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"name": "description",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"name": "websiteUrl",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
},
v12 = [
v13 = [
{
"kind": "Literal",
"name": "first",
"value": 50
}
],
v13 = {
v14 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
v14 = [
v15 = [
{
"kind": "Literal",
"name": "first",
"value": 5
}
],
v15 = {
v16 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthorized",
"storageKey": null
},
v16 = {
v17 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -170,6 +181,19 @@ return {
"metadata": null,
"name": "TrustGraphCurrentQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -178,12 +202,12 @@ return {
"name": "currentTrustCenter",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
@@ -192,12 +216,12 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/)
(v11/*: any*/),
(v0/*: any*/),
(v12/*: any*/)
],
"storageKey": null
},
@@ -208,7 +232,7 @@ return {
},
{
"alias": null,
"args": (v12/*: any*/),
"args": (v13/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
@@ -230,7 +254,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
@@ -258,6 +282,20 @@ return {
"kind": "Operation",
"name": "TrustGraphCurrentQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -266,12 +304,12 @@ return {
"name": "currentTrustCenter",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
@@ -280,13 +318,13 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v0/*: any*/)
(v0/*: any*/),
(v12/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
@@ -320,10 +358,10 @@ return {
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v6/*: any*/),
(v9/*: any*/),
(v8/*: any*/)
(v2/*: any*/),
(v8/*: any*/),
(v11/*: any*/),
(v10/*: any*/)
],
"storageKey": null
}
@@ -363,7 +401,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
@@ -371,9 +409,9 @@ return {
"name": "countries",
"storageKey": null
},
(v6/*: any*/),
(v13/*: any*/),
(v8/*: any*/),
(v14/*: any*/),
(v10/*: any*/),
{
"alias": null,
"args": null,
@@ -392,7 +430,7 @@ return {
},
{
"alias": null,
"args": (v14/*: any*/),
"args": (v15/*: any*/),
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "documents",
@@ -414,7 +452,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
@@ -422,8 +460,8 @@ return {
"name": "title",
"storageKey": null
},
(v15/*: any*/),
(v16/*: any*/),
(v17/*: any*/),
{
"alias": null,
"args": null,
@@ -442,7 +480,7 @@ return {
},
{
"alias": null,
"args": (v14/*: any*/),
"args": (v15/*: any*/),
"concreteType": "TrustCenterFileConnection",
"kind": "LinkedField",
"name": "trustCenterFiles",
@@ -464,11 +502,11 @@ return {
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v13/*: any*/),
(v6/*: any*/),
(v15/*: any*/),
(v16/*: any*/)
(v2/*: any*/),
(v14/*: any*/),
(v8/*: any*/),
(v16/*: any*/),
(v17/*: any*/)
],
"storageKey": null
}
@@ -480,7 +518,7 @@ return {
},
{
"alias": null,
"args": (v12/*: any*/),
"args": (v13/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
@@ -502,7 +540,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
@@ -511,7 +549,7 @@ return {
"name": "report",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
@@ -519,8 +557,8 @@ return {
"name": "filename",
"storageKey": null
},
(v15/*: any*/),
(v16/*: any*/)
(v16/*: any*/),
(v17/*: any*/)
],
"storageKey": null
},
@@ -532,8 +570,8 @@ return {
"name": "framework",
"plural": false,
"selections": [
(v0/*: any*/),
(v6/*: any*/),
(v2/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -566,16 +604,16 @@ return {
]
},
"params": {
"cacheID": "f098177c07810cbe7609821d63c22b0b",
"cacheID": "9d22288e0159409cf43bece191e3dc47",
"id": null,
"metadata": {},
"name": "TrustGraphCurrentQuery",
"operationKind": "query",
"text": "query TrustGraphCurrentQuery {\n currentTrustCenter {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n countries\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n trustCenterFiles(first: 5) {\n edges {\n node {\n id\n category\n ...TrustCenterFileRowFragment\n }\n }\n }\n}\n\nfragment TrustCenterFileRowFragment on TrustCenterFile {\n id\n name\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
"text": "query TrustGraphCurrentQuery {\n viewer {\n email\n fullName\n id\n }\n currentTrustCenter {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n countries\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n trustCenterFiles(first: 5) {\n edges {\n node {\n id\n category\n ...TrustCenterFileRowFragment\n }\n }\n }\n}\n\nfragment TrustCenterFileRowFragment on TrustCenterFile {\n id\n name\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
}
};
})();
(node as any).hash = "2fc7c45c2636a551c13d806f89e53c4a";
(node as any).hash = "dd2040667d5cc7b9af9659e993ba6a5b";
export default node;

View File

@@ -56,7 +56,7 @@ const routes = [
{
path: "/overview",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {})
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
@@ -72,7 +72,7 @@ const routes = [
{
path: "/documents",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {})
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
@@ -81,7 +81,7 @@ const routes = [
{
path: "",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {})
loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}),
),
Fallback: TabSkeleton,
Component: withQueryRef(DocumentsPage),
@@ -91,7 +91,7 @@ const routes = [
{
path: "/subprocessors",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {})
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
@@ -100,7 +100,7 @@ const routes = [
{
path: "",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustVendorsQuery, {})
loadQuery(consoleEnvironment, currentTrustVendorsQuery, {}),
),
Fallback: TabSkeleton,
Component: withQueryRef(SubprocessorsPage),